「
Mery 翻訳
」を編集中
2026年8月11日 (火) 21:44時点における
Yuko
(
トーク
|
投稿記録
)
による版
(ページの作成:「== 概要 == 「Mery 翻訳」は、Mery エディタ上で選択したテキストやカーソル位置の行を翻訳するためのマクロです。 Google 翻訳のエンドポイントを ''curl.exe'' から呼び出し、翻訳結果を元の文章の直下へ挿入します。英語のコメントやドキュメントを読みながら、その場ですぐ日本語訳を確認したい場合などに便利です。 例えば、次のような英文を選…」)
(差分) ← 古い版 |
最新版
(
差分
) |
新しい版 →
(
差分
)
ナビゲーションに移動
検索に移動
警告: このページの古い版を編集しています。
公開すると、この版以降になされた変更がすべて失われます。
警告:
ログインしていません。編集を行うと、あなたの IP アドレスが公開されます。
ログイン
または
アカウントを作成
すれば、あなたの編集はその利用者名とともに表示されるほか、さまざまなメリットもあります。
スパム攻撃防止用のチェックです。 決して、ここには、値の入力は
しない
でください!
== 概要 == 「Mery 翻訳」は、Mery エディタ上で選択したテキストやカーソル位置の行を翻訳するためのマクロです。 Google 翻訳のエンドポイントを ''curl.exe'' から呼び出し、翻訳結果を元の文章の直下へ挿入します。英語のコメントやドキュメントを読みながら、その場ですぐ日本語訳を確認したい場合などに便利です。 例えば、次のような英文を選択してマクロを実行すると、 <pre> Returns the current user information. </pre> 直下に翻訳結果が挿入されます。 <pre> Returns the current user information. ----- 翻訳: en -> ja ----- 現在のユーザー情報を返します。 -------------------------- </pre> 翻訳元の言語は標準で自動判定されます。 また、判定された言語が翻訳先の言語と同じ場合は、自動的に逆方向へ翻訳します。標準設定では、英語などの文章は日本語へ、日本語の文章は英語へ翻訳されます。 プログラムのソースコードを読む用途も想定しており、''camelCase'' や ''snake_case'' などの変数名を単独で翻訳した場合は、適宜単語へ分割してから翻訳します。 例えば、 <pre> currentUserName </pre> は内部的に、 <pre> current User Name </pre> として翻訳されるため、一般的な英文だけでなく、変数名やメソッド名の意味を確認したい場合にも利用できます。 本マクロをショートカットキーに設定し、いつでも起動できるようにしておくと、英文コメントやエラーメッセージ、変数名などを確認するときに便利です。 == 注意事項 == Mery Ver 3.8.8 で動作確認しています。 本マクロでは、Google 翻訳の非公式エンドポイントへアクセスしています。公式 API として提供しているものではないため、エンドポイントの仕様変更などによって将来的に利用できなくなる可能性があります。 また、翻訳するテキストは Google 翻訳へ送信されます。機密情報、個人情報、パスワード、API キーなど、外部サービスへ送信してはいけない情報には使用しないでください。 Google 翻訳へのアクセスには Windows 10 以降で搭載されている ''curl.exe'' を使用します。 なお、短時間に大量の翻訳を実行すると、Google 側のレート制限によって一時的に利用できなくなる可能性があります。 == 使い方 == === 0. 事前準備 === 本マクロでは外部ライブラリのインストールは必要ありません。 Google 翻訳への通信に Windows の ''curl.exe'' を利用するため、あらかじめ ''curl.exe'' が利用可能であることを確認してください。 また、マクロ冒頭にある ''CONFIG'' を変更することで、翻訳する言語などを設定できます。 標準設定は次のとおりです。 <pre> const CONFIG = { /** 翻訳元言語。空文字または "auto" で自動判定 */ sourceLanguage: "auto", /** 翻訳先言語 */ targetLanguage: "ja", /** 検出言語が targetLanguage と同じときの翻訳先 */ reverseLanguage: "en", /** curl のタイムアウト(秒) */ timeoutSec: 10, }; </pre> 標準では、翻訳元の言語を自動判定し、日本語以外の文章を日本語へ翻訳します。 日本語が検出された場合は ''reverseLanguage'' が使用され、日本語から英語へ翻訳されます。 === 1. 選択範囲を翻訳する場合 === 翻訳したいテキストを選択します。 <pre> The request was successfully completed. </pre> マクロを実行すると、選択範囲の最終行の直下に翻訳結果が挿入されます。 <pre> The request was successfully completed. ----- 翻訳: en -> ja ----- リクエストは正常に完了しました。 -------------------------- </pre> 複数行をまとめて選択して翻訳することもできます。 翻訳中はカーソル付近に「選択範囲を翻訳中...」と表示され、翻訳が完了するとステータスバーに「翻訳完了」と表示されます。 === 2. カーソル位置の行を翻訳する場合 === テキストを選択していない状態でマクロを実行すると、カーソルがある行全体を翻訳します。 例えば、次の行にカーソルを置きます。 <pre> This method returns the current configuration. </pre> マクロを実行すると、次のようになります。 <pre> This method returns the current configuration. ----- 翻訳: en -> ja ----- このメソッドは現在の設定を返します。 -------------------------- </pre> 空行でマクロを実行した場合は翻訳を行わず、ステータスバーに「翻訳するテキストがありません」と表示されます。 === 3. 日本語を英語へ翻訳する場合 === 標準設定では翻訳先言語に日本語 (''ja'') が設定されています。 翻訳元の言語を自動判定している場合、Google 翻訳によって日本語が検出されると、自動的に ''reverseLanguage'' で指定した言語へ翻訳します。 標準では ''reverseLanguage'' が英語 (''en'') のため、例えば、 <pre> ユーザー情報を取得する </pre> を翻訳すると、 <pre> ユーザー情報を取得する ----- 翻訳: ja -> en ----- Get user information -------------------------- </pre> のように英語へ翻訳されます。 このため、マクロを切り替えたり設定を書き換えたりすることなく、日本語と英語を相互に翻訳できます。 === 4. 翻訳する言語を固定する場合 === 翻訳元の言語を固定したい場合は、''sourceLanguage'' を変更します。 例えば、英語から日本語への翻訳に固定する場合は次のように設定します。 <pre> sourceLanguage: "en", targetLanguage: "ja", </pre> この場合、言語の自動判定や逆方向への翻訳は行われません。 翻訳元を自動判定する場合は、次のいずれかを指定します。 <pre> sourceLanguage: "auto", </pre> または、 <pre> sourceLanguage: "", </pre> 翻訳先を変更したい場合は ''targetLanguage'' を変更します。 例えば、中国語へ翻訳する場合は、 <pre> targetLanguage: "zh-CN", </pre> のように設定できます。 === 5. 変数名を翻訳する場合 === 本マクロでは、プログラムで使用される変数名やメソッド名を翻訳しやすくするため、単独の識別子を自動的に単語へ分割する機能があります。 例えば、 <pre> currentUserName </pre> を翻訳すると、内部的には、 <pre> current User Name </pre> として Google 翻訳へ送信されます。 同様に、 <pre> current_user_name </pre> や、 <pre> current-user-name </pre> も、 <pre> current user name </pre> として翻訳されます。 次のような形式に対応しています。 * ''camelCase'' * ''PascalCase'' * ''snake_case'' * ''kebab-case'' * ''XMLHttpRequest'' のように大文字が連続する識別子 例えば、ソースコード中に、 <pre> const requestTimeoutSec = 10; </pre> という変数があり、''requestTimeoutSec'' の意味を確認したい場合、その部分だけを選択してマクロを実行できます。 通常の英文や文章については、この変換は行われません。 === 6. 翻訳結果の表示形式 === 翻訳結果は、元の文章の直下に次の形式で挿入されます。 <pre> ----- 翻訳: 翻訳元言語 -> 翻訳先言語 ----- 翻訳結果 ------------------------------------------ </pre> 例えば英語から日本語へ翻訳した場合は、 <pre> ----- 翻訳: en -> ja ----- 翻訳された文章 -------------------------- </pre> と表示されます。 翻訳元の言語は Google 翻訳による自動判定結果が使用されます。 複数行の文章を翻訳した場合も、翻訳結果全体をヘッダーとフッターで囲んだ状態で挿入します。 翻訳後は、挿入された翻訳結果の次の行へカーソルが移動するため、そのまま文章の編集を続けられます。 === 7. エラーが発生した場合 === 通信エラーなどが発生した場合は、翻訳結果を挿入せず、エラーメッセージを表示します。 主に次のようなエラーを検出します。 * Google 翻訳への接続がタイムアウトした * ''curl.exe'' の実行に失敗した * Google 翻訳から空の応答が返された * Google 翻訳から HTML のエラーページが返された * Google 翻訳から想定していない形式のデータが返された * Google 翻訳のレート制限に達した * 翻訳結果が空だった 通信のタイムアウト時間は ''CONFIG.timeoutSec'' で変更できます。 例えば、20 秒に変更する場合は次のように設定します。 <pre> timeoutSec: 20, </pre> 通常は標準値の 10 秒のままで問題ありません。 == ソースコード == <syntaxhighlight lang="javascript" copy> #language = "quickjs" #title = "Mery 翻訳" /** * Mery 翻訳マクロ * * VSCode 拡張「Comment Translate」と同じ Google 翻訳の非公式エンドポイントを、 * shell.Exec + curl で呼び出して、選択範囲の直下に翻訳結果を挿入します。 * (q は encodeURIComponent して渡します。stdin の q@- は Mery 経由だと欠落します) * * 使い方: * 1. 翻訳したい文字列を選択(未選択ならカーソル行を選択) * 2. このマクロを実行 * * 必要環境: * - Mery Ver 3.8.8 以降(左記バージョンにて動作確認) * - PATH 上の curl.exe(Windows 10 以降に標準搭載) * * 設定は下の CONFIG を編集してください。 */ const CONFIG = { /** 翻訳元言語。空文字または "auto" で自動判定 */ sourceLanguage: "auto", /** 翻訳先言語 */ targetLanguage: "ja", /** 検出言語が targetLanguage と同じときの翻訳先 */ reverseLanguage: "en", /** curl のタイムアウト(秒) */ timeoutSec: 10, }; const ENDPOINT = "https://translate.google.com/translate_a/single"; const sel = document.selection; main(); function main() { try { const target = resolveTranslateTarget(); if (!target) { Status = "翻訳するテキストがありません"; return; } let tipMessage = ""; if (sel.IsEmpty) { tipMessage = "この行を翻訳中..."; } else { tipMessage = "選択範囲を翻訳中..."; } ShowTip(tipMessage, meShowTipPosCaret); Status = "翻訳中…"; const outcome = runTranslation(target.text); insertTranslationBelow( outcome.translatedText, outcome.sourceLanguage, outcome.targetLanguage ); Status = "翻訳完了"; } catch (error) { const message = error && error.message ? error.message : String(error); Status = ""; alert("Translator: " + message); } finally { ShowTip("", meShowTipHide); } } /** * 選択があればそれを使う。未選択ならカーソル行を選択してから返す。 */ function resolveTranslateTarget() { if (!sel.IsEmpty) { const text = normalizeSelectionText(sel.Text); if (!text.trim()) { return null; } return { text: text }; } const y = sel.GetActivePointY(mePosLogical); const line = document.GetLine(y, 0).trim(); if (!line || !String(line).trim()) { return null; } return { text: normalizeSelectionText(line) }; } function normalizeSelectionText(text) { return String(text).replace(/\r\n$/, "").replace(/\n$/, "").replace(/\r$/, ""); } function runTranslation(rawText) { const sourceLanguage = normalizeSourceLanguage(CONFIG.sourceLanguage); const text = prepareTextForTranslation(rawText); const first = translate(text, sourceLanguage, CONFIG.targetLanguage); let translatedText = first.text; let direction = resolveTranslationDirection(first.detectedLanguage); if (needsReverseTranslation(first.detectedLanguage)) { const second = translate(text, "auto", CONFIG.reverseLanguage); translatedText = second.text; direction = { sourceLanguage: second.detectedLanguage || first.detectedLanguage || "auto", targetLanguage: CONFIG.reverseLanguage, }; } return { translatedText: translatedText, sourceLanguage: direction.sourceLanguage, targetLanguage: direction.targetLanguage, }; } function translate(text, sourceLanguage, targetLanguage) { // q@-(stdin)は Mery の shell.Exec 経由だと curl に届かず、 // Google が HTML 400 を返して JSON パースに失敗するため使わない。 // q は encodeURIComponent 済みで渡し、コマンドラインへの生テキスト埋め込みを避ける。 const cmd = [ "curl.exe", "-sS", "-G", "-A", quoteArg("Mozilla/5.0"), "--max-time", String(CONFIG.timeoutSec), quoteArg(ENDPOINT), "--data-urlencode", quoteArg("client=gtx"), "--data-urlencode", quoteArg("sl=" + normalizeSourceLanguage(sourceLanguage)), "--data-urlencode", quoteArg("tl=" + targetLanguage), "--data-urlencode", quoteArg("dt=t"), "--data-urlencode", quoteArg("ie=UTF-8"), "--data-urlencode", quoteArg("oe=UTF-8"), "--data", quoteArg("q=" + encodeURIComponent(text)), ].join(" "); const exec = shell.Exec(cmd, "", true); const exitCode = Number( exec.ExitCode != null ? exec.ExitCode : exec.exitCode ); const stdout = exec.StdOut != null ? exec.StdOut : exec.stdOut; const stderr = exec.StdErr != null ? exec.StdErr : exec.stdErr; const raw = String(stdout == null ? "" : stdout) .replace(/^\uFEFF/, "") .trim(); if (exitCode === 28) { throw new Error( "Google Translate への接続がタイムアウトしました(" + CONFIG.timeoutSec + " 秒)。" ); } if (exitCode !== 0) { throw new Error( "curl が失敗しました (exit " + exitCode + ")。" + (stderr ? "\n" + String(stderr).trim() : "") ); } if (raw.indexOf("429") === 0) { throw new Error("Google Translate のレート制限に達しました。"); } if (!raw) { throw new Error("Google Translate から空の応答が返されました。"); } if (raw.charAt(0) === "<") { throw new Error( "Google Translate が HTML エラーを返しました(リクエストが拒否された可能性があります)。" ); } let data; try { data = JSON.parse(raw); } catch (error) { const preview = raw.replace(/\s+/g, " ").slice(0, 180); throw new Error( "Google Translate が不正な JSON を返しました。\n" + preview ); } const result = parseGoogleResponse(data); if (!result.text.trim()) { throw new Error("翻訳結果が空でした。"); } return result; } /** * 非公式エンドポイントのレスポンスをパースする。 * 形の概略: [[["訳文","原文",...], ...], null, "detectedLang", ...] */ function parseGoogleResponse(data) { if (!Array.isArray(data)) { throw new Error("Google Translate の応答が配列ではありません。"); } const segments = data[0]; if (!Array.isArray(segments)) { throw new Error("翻訳セグメントがありません。"); } const parts = []; for (const segment of segments) { if (!Array.isArray(segment) || typeof segment[0] !== "string") { throw new Error("不正な翻訳セグメントが含まれています。"); } parts.push(segment[0]); } return { text: parts.join(""), detectedLanguage: extractDetectedLanguage(data), }; } function extractDetectedLanguage(data) { const candidate = data[2]; if (typeof candidate === "string" && candidate.trim() !== "") { return candidate; } const nested = data[8]; if (Array.isArray(nested) && Array.isArray(nested[0])) { const first = nested[0][0]; if (typeof first === "string" && first.trim() !== "") { return first; } } return undefined; } function resolveTranslationDirection(detectedLanguage) { if (normalizeSourceLanguage(CONFIG.sourceLanguage) !== "auto") { return { sourceLanguage: CONFIG.sourceLanguage, targetLanguage: CONFIG.targetLanguage, }; } const detected = detectedLanguage && detectedLanguage.trim(); if (detected && languagesEqual(detected, CONFIG.targetLanguage)) { return { sourceLanguage: detected, targetLanguage: CONFIG.reverseLanguage, }; } return { sourceLanguage: detected || "auto", targetLanguage: CONFIG.targetLanguage, }; } function needsReverseTranslation(detectedLanguage) { if (normalizeSourceLanguage(CONFIG.sourceLanguage) !== "auto") { return false; } const detected = detectedLanguage && detectedLanguage.trim(); if (!detected) { return false; } return languagesEqual(detected, CONFIG.targetLanguage); } function languagesEqual(a, b) { return a.trim().toLowerCase() === b.trim().toLowerCase(); } function normalizeSourceLanguage(value) { const trimmed = String(value == null ? "" : value).trim(); return trimmed === "" ? "auto" : trimmed; } /** * 単独の変数風識別子(camelCase / snake_case 等)なら単語に分割する。 */ function prepareTextForTranslation(text) { const trimmed = text.trim(); if (!isVariableStyleIdentifier(trimmed)) { return text; } const words = splitVariableIdentifier(trimmed); return words.length > 0 ? words.join(" ") : text; } function isVariableStyleIdentifier(text) { if (!text || !/^[A-Za-z_][A-Za-z0-9_-]*$/.test(text)) { return false; } return ( text.includes("_") || text.includes("-") || /[a-z][A-Z]/.test(text) || /[A-Z]{2,}[a-z]/.test(text) ); } function splitVariableIdentifier(text) { return text .replace(/([a-z0-9])([A-Z])/g, "$1 $2") .replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2") .split(/[\s_-]+/) .filter(Boolean); } /** * 選択範囲の最終行末の直後へ、vscode-translator と同じ形式で挿入する。 */ function insertTranslationBelow(translatedText, sourceLanguage, targetLanguage) { let endY = sel.GetBottomPointY(mePosLogical); const endX = sel.GetBottomPointX(mePosLogical); // 行全体選択で次行先頭 (x=1) に終わっている場合は前の行末へ if (endX === 1 && endY > sel.GetTopPointY(mePosLogical)) { endY = endY - 1; } sel.SetActivePoint(mePosLogical, 1, endY); sel.EndOfLine(false, mePosLogical); const header = "----- 翻訳: " + sourceLanguage + " -> " + targetLanguage + " -----"; const footer = repeatChar("-", header.length); const body = "\n" + header + "\n" + translatedText + "\n" + footer; // 文書末尾なら、フッター直後に空行を残せるよう末尾改行を付ける const lineCount = document.GetLines(); const appendTrailingNewline = endY >= lineCount; sel.Text = appendTrailingNewline ? body + "\n" : body; // フッターの次の行先頭へカーソルを置く const translationLineCount = String(translatedText).split("\n").length; const cursorY = endY + 3 + translationLineCount; sel.SetActivePoint(mePosLogical, 1, cursorY, false); } function repeatChar(ch, count) { let out = ""; for (let i = 0; i < count; i++) { out += ch; } return out; } function quoteArg(value) { // Windows のコマンドライン向けにダブルクォートで囲む return '"' + String(value).replace(/"/g, '\\"') + '"'; } </syntaxhighlight> == 更新履歴 == * 2026-08-11 公開
編集内容の要約:
MeryWikiへの投稿はすべて、他の投稿者によって編集、変更、除去される場合があります。 自分が書いたものが他の人に容赦なく編集されるのを望まない場合は、ここに投稿しないでください。
また、投稿するのは、自分で書いたものか、パブリック ドメインまたはそれに類するフリーな資料からの複製であることを約束してください(詳細は
MeryWiki:著作権
を参照)。
著作権保護されている作品は、許諾なしに投稿しないでください!
このページを編集するには、下記の数式を計算してその答えを欄に入力してください (
ヘルプ
):
いちたすには =
キャンセル
編集ヘルプ
(新しいウィンドウで開きます)
スポンサーリンク
ナビゲーション メニュー
個人用ツール
ログインしていません
トーク
投稿記録
アカウント作成
ログイン
名前空間
ページ
議論
日本語
表示
閲覧
編集
履歴表示
その他
検索
スポンサーリンク
スポンサーリンク
案内
メインページ
ヘルプ
よくある質問
マクロリファレンス
マクロライブラリ
プラグインライブラリ
構文ファイル
テーマ
寄付・開発支援
練習用ページ
開発室
開発者のブログ
ツール
スポンサーリンク