ブラウザ標準機能の翻訳を用いたテキストデータ翻訳支援ツール
英文などの改行を自動でキレイに整え、ブラウザの翻訳機能を使って効率よく大量のテキストを翻訳・保存していくための個人専用ツールを作りました。
※注意
私は長文のドキュメントを翻訳する際に、一度テキスト分割ツールでテキストを500行ずつなどに分割しており、それらをまとめて翻訳をかけたいときなどに使う想定で作っています。なので、本一冊のテキストデータをそのまま翻訳することは想定して実装してません。
どんなツール?(特徴とメリット)
ドラッグ&ドロップで一括読み込み: 翻訳したいテキストファイルを複数まとめて放り込むだけで、順番に処理するリスト(キュー)が作られます。
賢い「改行のバスター」: 1つの段落の中にある途切れ途切れの改行を自動で合体させ、段落と段落の間の「空行」だけを正しく残します。これにより、ブラウザの翻訳機能が文脈を正しく理解できるようになります。
タブ切り替えでサクサク進む: 読み込んだファイルはタブで管理され、今どれが完了してどれが未完了かが一目でわかります。
1クリックで「保存して次へ」: 画面上の文章をブラウザ翻訳した状態でボタンを押すと、翻訳後のテキストを自動でファイル保存し、次のファイルへ自動で切り替えてくれます。
導入のしかた(準備は30秒で終わります)
このツールはクラウドにアップロードしたりインストールしたりする必要はありません。あなたのパソコン内だけで動く安全なツールです。
1. コードを保存するデスクトップなどの分かりやすい場所で右クリックし、「新規作成」 ➔ 「テキスト ドキュメント」 を作ります。作成したテキストファイルを開き、上記の <!DOCTYPE html> から </html> までのコードをすべてコピーして貼り付けます。ファイルの名前を 翻訳ヘルパー.html に変更します。注意: 末尾の .txt を .html に書き換えてください(拡張子を変更すると警告が出ますが、「はい」を選んで進めて大丈夫です)。
2. ブラウザで開く作成した 翻訳ヘルパー.html を、普段使っているブラウザ(Google Chrome や Microsoft Edge など)にドラッグ&ドロップするか、ダブルクリックして開きます。アドレスバーが file:///C:/... のようになっていれば、正しくローカル環境で起動できています。
使い方(実際の流れ)
ステップ 1:ファイルをまとめて読み込む翻訳したいテキストファイル(.txt)を、上部の点線枠(ドロップゾーン)にまとめてドラッグ&ドロップします(クリックして選択も可能です)。 英語などの単語の間にスペースが必要な言語の場合は、「改行を結合する際にスペースを入れる」にチェックを入れておきます。
ステップ 2:ブラウザの機能で翻訳するファイルを読み込むと、中央のエリアに改行が綺麗に整えられたテキストが表示されます。 画面の何も無いところで右クリック ➔ 「日本語に翻訳」(あるいは「〇〇語に翻訳」)を選択し、ブラウザ全体の翻訳機能をオンにします。
ステップ 3:保存して次のファイルへ画面が翻訳されたら、一番下にある 「翻訳結果をログ保存 ➔ 次のファイルへ」 ボタンを押します。 これだけで、自動的に「元のファイル名_翻訳ログ.txt」という名前で翻訳後のテキストがダウンロードされ、タブが「完了(緑色)」になり、自動的に次のファイルのテキストが表示されます。
2つ目以降のファイルは、ブラウザの翻訳機能がオンのままであれば、タブが切り替わった瞬間に自動で翻訳されます。あとは「ボタンを押す」を繰り返すだけです。
💡 知っておくと便利なワンポイント
手動での追加も可能: 「テキストを直接貼り付けて追加する場合はこちら」という詳細メニューを開くと、ファイルを作ることなく、コピーしたテキストに名前をつけてその場でリストに追加することもできます。
翻訳が追従しないとき: タブを切り替えたときにブラウザの自動翻訳がついてこない(外国語のままになってしまう)場合は、もう一度画面を右クリックして「翻訳」を押し直してください。
code:html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>翻訳ヘルパー</title>
<style>
:root{
--paper:#f7f5f0;
--ink:#2a2620;
--line:#d8d2c4;
--accent:#4a6fa5;
--accent-dark:#33507d;
--mono:#8a7f68;
--done:#5c8a5c;
}
*{box-sizing:border-box;}
body{
margin:0;
background:var(--paper);
color:var(--ink);
font-family:"Yu Mincho","Hiragino Mincho ProN",serif;
line-height:1.9;
padding:2.5rem 1.5rem 4rem;
}
.wrap{max-width:820px;margin:0 auto;}
h1{
font-size:1.4rem;
letter-spacing:.05em;
margin:0 0 .3rem;
border-bottom:2px solid var(--ink);
padding-bottom:.6rem;
}
.sub{
font-family:"Courier New",monospace;
font-size:.75rem;
color:var(--mono);
letter-spacing:.03em;
margin:0 0 2rem;
}
.panel{
background:#fff;
border:1px solid var(--line);
border-radius:4px;
padding:1.2rem 1.4rem;
margin-bottom:1.4rem;
}
.panel h2{
font-size:.85rem;
font-family:"Courier New",monospace;
color:var(--accent-dark);
letter-spacing:.05em;
margin:0 0 .8rem;
text-transform:uppercase;
}
.row{display:flex;flex-wrap:wrap;gap:.8rem;align-items:center;margin-bottom:.6rem;}
#dropzone{
border:2px dashed var(--line);
border-radius:6px;
padding:2rem 1rem;
text-align:center;
font-family:"Courier New",monospace;
font-size:.8rem;
color:var(--mono);
cursor:pointer;
transition:.15s;
}
#dropzone.drag{
border-color:var(--accent);
background:#eef2f8;
color:var(--accent-dark);
}
#fileInput{display:none;}
textarea{
width:100%;
min-height:90px;
font-family:"Courier New",monospace;
font-size:.82rem;
padding:.6rem;
border:1px solid var(--line);
border-radius:3px;
resize:vertical;
}
inputtype=text{
font-family:"Courier New",monospace;
font-size:.8rem;
padding:.4rem .6rem;
border:1px solid var(--line);
border-radius:3px;
}
label.opt{
font-family:"Courier New",monospace;
font-size:.78rem;
color:var(--ink);
display:flex;align-items:center;gap:.4rem;
cursor:pointer;
}
button{
font-family:"Courier New",monospace;
font-size:.8rem;
letter-spacing:.03em;
background:var(--accent);
color:#fff;
border:none;
padding:.6rem 1rem;
border-radius:3px;
cursor:pointer;
}
button:hover{background:var(--accent-dark);}
button.secondary{background:transparent;color:var(--accent-dark);border:1px solid var(--accent);}
button.secondary:hover{background:#eef2f8;}
button.danger{background:transparent;color:#a24a4a;border:1px solid #d9baba;padding:.3rem .5rem;font-size:.7rem;}
button:disabled{background:#c9c3b4;cursor:not-allowed;}
.progress{
font-family:"Courier New",monospace;
font-size:.78rem;
color:var(--mono);
margin-bottom:.6rem;
}
.tabs{
display:flex;
flex-wrap:wrap;
gap:.4rem;
margin-bottom:1rem;
max-height:9rem;
overflow-y:auto;
padding:.2rem;
}
.tab{
font-family:"Courier New",monospace;
font-size:.72rem;
border:1px solid var(--line);
background:#faf9f6;
color:var(--mono);
padding:.35rem .6rem;
border-radius:3px;
cursor:pointer;
display:flex;
align-items:center;
gap:.4rem;
max-width:220px;
}
.tab .name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.tab.active{border-color:var(--accent);color:var(--accent-dark);background:#eef2f8;font-weight:bold;}
.tab.done{color:var(--done);border-color:var(--done);}
.tab .dot{width:.5em;height:.5em;border-radius:50%;background:var(--line);flex-shrink:0;}
.tab.done .dot{background:var(--done);}
.tab .x{color:#a24a4a;font-weight:bold;padding:0 .2rem;}
#textDisplay{white-space:normal;font-size:1rem;min-height:4rem;}
#textDisplay p{margin:0 0 1.1em;}
#textDisplay:empty::before{
content:"ファイルをアップロードするとここに表示されます";
color:var(--mono);
font-family:"Courier New",monospace;
font-size:.8rem;
}
.note{
font-family:"Courier New",monospace;
font-size:.72rem;
color:var(--mono);
line-height:1.7;
border-left:3px solid var(--line);
padding-left:.8rem;
margin-top:.8rem;
}
.step{
display:inline-block;
font-family:"Courier New",monospace;
font-size:.7rem;
background:var(--ink);
color:var(--paper);
padding:.1rem .4rem;
border-radius:2px;
margin-right:.4rem;
}
.currentName{font-family:"Courier New",monospace;font-size:.85rem;color:var(--accent-dark);margin-bottom:.6rem;}
</style>
</head>
<body>
<div class="wrap">
<h1>翻訳ヘルパー</h1>
<p class="sub">multi-file drag&amp;drop / tab switch / translate-and-log queue</p>
<div class="panel">
<h2><span class="step">1</span>テキストファイルをまとめて読み込む</h2>
<div id="dropzone">
ここにテキストファイルをドラッグ&ドロップ<br>(クリックしてファイル選択も可・複数選択可)
<input type="file" id="fileInput" accept=".txt,text/plain" multiple>
</div>
<div class="row" style="margin-top:1rem;">
<label class="opt"><input type="checkbox" id="addSpace"> 改行を結合する際にスペースを入れる(英語など向け)</label>
</div>
<details style="margin-top:.6rem;">
<summary class="note" style="cursor:pointer;display:inline;border:none;padding:0;">テキストを直接貼り付けて追加する場合はこちら</summary>
<div class="row" style="margin-top:.6rem;">
<input type="text" id="pasteName" placeholder="この内容の名前(例:memo1)" style="flex:1;min-width:160px;">
</div>
<div class="row">
<textarea id="pasteArea" placeholder="ここに貼り付け"></textarea>
</div>
<div class="row">
<button class="secondary" id="addPasteBtn">キューに追加</button>
</div>
</details>
</div>
<div class="panel">
<h2><span class="step">2</span>タブを切り替えながら翻訳</h2>
<div class="progress" id="progressLabel">ファイル未読み込み</div>
<div class="tabs" id="tabs"></div>
<div class="currentName" id="currentName"></div>
<div id="textDisplay" contenteditable="false"></div>
<p class="note">
ブラウザの翻訳機能で現在のタブの内容を翻訳してください(自動翻訳バーが出ない場合は右クリック→「〇〇語に翻訳」)。<br>
このHTMLは<strong>ダウンロードしてブラウザで直接開いた状態</strong>で使うと翻訳機能が確実に動作します。<br>
翻訳を一度オンにすれば、タブ切り替えで新しく表示される文章も自動的に翻訳され続けるはずです(されない場合は再度右クリックで翻訳)。
</p>
</div>
<div class="panel">
<h2><span class="step">3</span>ログ保存して次へ</h2>
<div class="row">
<button id="saveNextBtn" disabled>翻訳結果をログ保存 → 次のファイルへ</button>
<button class="secondary" id="saveOnlyBtn" disabled>保存のみ(移動しない)</button>
</div>
<p class="note">
現在表示中のテキスト(=翻訳後の文章)を「ファイル名_翻訳ログ.txt」として保存し、自動的に次の未完了ファイルへ切り替えます。
</p>
</div>
</div>
<script>
let queue = []; // {id, name, raw, done}
let currentIndex = -1;
const dropzone = document.getElementById('dropzone');
const fileInput = document.getElementById('fileInput');
const addSpaceChk = document.getElementById('addSpace');
const tabsEl = document.getElementById('tabs');
const progressLabel = document.getElementById('progressLabel');
const currentNameEl = document.getElementById('currentName');
const textDisplay = document.getElementById('textDisplay');
const saveNextBtn = document.getElementById('saveNextBtn');
const saveOnlyBtn = document.getElementById('saveOnlyBtn');
const pasteName = document.getElementById('pasteName');
const pasteArea = document.getElementById('pasteArea');
const addPasteBtn = document.getElementById('addPasteBtn');
function uid(){ return Math.random().toString(36).slice(2,10); }
// --- drag & drop ---
dropzone.addEventListener('click', () => fileInput.click());
'dragenter','dragover'.forEach(evt=>{
dropzone.addEventListener(evt, e=>{ e.preventDefault(); e.stopPropagation(); dropzone.classList.add('drag'); });
});
'dragleave','drop'.forEach(evt=>{
dropzone.addEventListener(evt, e=>{ e.preventDefault(); e.stopPropagation(); dropzone.classList.remove('drag'); });
});
dropzone.addEventListener('drop', e=>{
const dtFiles = e.dataTransfer.files;
if (dtFiles && dtFiles.length) addFiles(dtFiles);
});
fileInput.addEventListener('change', ()=>{
if (fileInput.files.length) addFiles(fileInput.files);
fileInput.value = '';
});
function addFiles(fileList){
const arr = Array.from(fileList);
let remaining = arr.length;
arr.forEach(file=>{
const reader = new FileReader();
reader.onload = (e)=>{
queue.push({ id: uid(), name: file.name, raw: e.target.result, done:false });
remaining--;
if (remaining === 0){
if (currentIndex === -1) currentIndex = 0;
renderTabs();
renderCurrent();
}
};
reader.readAsText(file, 'UTF-8');
});
}
addPasteBtn.addEventListener('click', ()=>{
const text = pasteArea.value;
if (!text.trim()) return;
const name = (pasteName.value.trim() || ('memo_' + (queue.length+1))) + '.txt';
queue.push({ id: uid(), name, raw: text, done:false });
pasteArea.value = '';
pasteName.value = '';
if (currentIndex === -1) currentIndex = 0;
renderTabs();
renderCurrent();
});
function normalizeText(text, addSpace){
text = text.replace(/\r\n/g,'\n').replace(/\r/g,'\n');
const paragraphs = text.split(/\n\s*\n/);
return paragraphs
.map(p=>{
const lines = p.split('\n').map(l=>l.trim()).filter(l=>l.length>0);
return lines.join(addSpace ? ' ' : '');
})
.filter(p=>p.length>0);
}
function escapeHtml(str){
return str.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
function renderTabs(){
tabsEl.innerHTML = '';
queue.forEach((item, idx)=>{
const tab = document.createElement('div');
tab.className = 'tab' + (idx===currentIndex ? ' active' : '') + (item.done ? ' done' : '');
tab.innerHTML = <span class="dot"></span><span class="name">${escapeHtml(item.name)}</span><span class="x" data-idx="${idx}">×</span>;
tab.addEventListener('click', (e)=>{
if (e.target.classList.contains('x')) return;
currentIndex = idx;
renderTabs();
renderCurrent();
});
tab.querySelector('.x').addEventListener('click', (e)=>{
e.stopPropagation();
removeItem(idx);
});
tabsEl.appendChild(tab);
});
const doneCount = queue.filter(q=>q.done).length;
progressLabel.textContent = queue.length
? ${queue.length}件中 ${doneCount}件完了
: 'ファイル未読み込み';
}
function removeItem(idx){
queue.splice(idx,1);
if (queue.length === 0){
currentIndex = -1;
} else if (currentIndex >= queue.length){
currentIndex = queue.length - 1;
} else if (idx < currentIndex){
currentIndex--;
}
renderTabs();
renderCurrent();
}
function renderCurrent(){
if (currentIndex === -1 || !queuecurrentIndex){
currentNameEl.textContent = '';
textDisplay.innerHTML = '';
saveNextBtn.disabled = true;
saveOnlyBtn.disabled = true;
return;
}
const item = queuecurrentIndex;
currentNameEl.textContent = 現在のファイル: ${item.name};
const paragraphs = normalizeText(item.raw, addSpaceChk.checked);
textDisplay.innerHTML = paragraphs.map(p=><p>${escapeHtml(p)}</p>).join('');
saveNextBtn.disabled = false;
saveOnlyBtn.disabled = false;
}
addSpaceChk.addEventListener('change', renderCurrent);
function downloadLog(item, content){
const baseName = item.name.replace(/\.^/.+$/, '');
const filename = ${baseName}_翻訳ログ.txt;
const blob = new Blob(content, { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
function goToNextUndone(){
const n = queue.length;
for (let step=1; step<=n; step++){
const idx = (currentIndex + step) % n;
if (!queueidx.done){ currentIndex = idx; return; }
}
// all done, stay put
}
saveOnlyBtn.addEventListener('click', ()=>{
if (currentIndex === -1) return;
const item = queuecurrentIndex;
downloadLog(item, textDisplay.innerText);
item.done = true;
renderTabs();
});
saveNextBtn.addEventListener('click', ()=>{
if (currentIndex === -1) return;
const item = queuecurrentIndex;
downloadLog(item, textDisplay.innerText);
item.done = true;
renderTabs();
if (queue.some(q=>!q.done)){
goToNextUndone();
renderTabs();
renderCurrent();
}
});
</script>
</body>
</html>