Mery 翻訳

2026年8月12日 (水) 09:04時点におけるYuko (トーク | 投稿記録)による版

概要

「Mery 翻訳」は、Mery エディタ上で選択したテキストやカーソル位置の行を翻訳するためのマクロです。

Google 翻訳のエンドポイントを curl.exe から呼び出し、翻訳結果を元の文章の直下へ挿入します。英語のコメントやドキュメントを読みながら、その場ですぐ日本語訳を確認したい場合などに便利です。

例えば、次のような英文を選択してマクロを実行すると、

Returns the current user information.

直下に翻訳結果が挿入されます。

Returns the current user information.
----- 翻訳: en -> ja -----
現在のユーザー情報を返します。
--------------------------

翻訳元の言語は標準で自動判定されます。

また、判定された言語が翻訳先の言語と同じ場合は、自動的に逆方向へ翻訳します。標準設定では、英語などの文章は日本語へ、日本語の文章は英語へ翻訳されます。

プログラムのソースコードを読む用途も想定しており、camelCase や snake_case などの変数名を単独で翻訳した場合は、適宜単語へ分割してから翻訳します。

例えば、

currentUserName

は内部的に、

current User Name

として翻訳されるため、一般的な英文だけでなく、変数名やメソッド名の意味を確認したい場合にも利用できます。

本マクロをショートカットキーに設定し、いつでも起動できるようにしておくと、英文コメントやエラーメッセージ、変数名などを確認するときに便利です。

注意事項

Mery Ver 3.8.8 で動作確認しています。

本マクロでは、Google 翻訳の非公式エンドポイントへアクセスしています。公式 API として提供しているものではないため、エンドポイントの仕様変更などによって将来的に利用できなくなる可能性があります。

また、翻訳するテキストは Google 翻訳へ送信されます。機密情報、個人情報、パスワード、API キーなど、外部サービスへ送信してはいけない情報には使用しないでください。

Google 翻訳へのアクセスには Windows 10 以降で搭載されている curl.exe を使用します。

なお、短時間に大量の翻訳を実行すると、Google 側のレート制限によって一時的に利用できなくなる可能性があります。

使い方

0. 事前準備

本マクロでは外部ライブラリのインストールは必要ありません。

Google 翻訳への通信に Windows の curl.exe を利用するため、あらかじめ curl.exe が利用可能であることを確認してください。

また、マクロ冒頭にある CONFIG を変更することで、翻訳する言語や表示方式などを設定できます。

標準設定は次のとおりです。

const CONFIG = {
  /** 翻訳元言語。空文字または "auto" で自動判定 */
  sourceLanguage: "auto",
  /** 翻訳先言語 */
  targetLanguage: "ja",
  /** 検出言語が targetLanguage と同じときの翻訳先 */
  reverseLanguage: "en",
  /** curl のタイムアウト(秒) */
  timeoutSec: 10,

  /**
   * 翻訳結果の表示方法(複数同時に有効化可)
   * - editor: エディターへ挿入(選択範囲の直下)
   * - outputBar: アウトプットバーへ出力
   * - tip: ツールチップで表示
   */
  display: {
    editor: true,
    outputBar: false,
    tip: false,
  },

  /**
   * アウトプットバー出力時に自動で表示するか
   * true で表示、false で表示しない
   */
  outputBarVisible: true,

  /**
   * tip 表示時の位置フラグ
   * meShowTipPosCaret (キャレット位置) | meShowTipPosMouse (マウス位置)
   */
  tipFlags: meShowTipPosCaret,
};

標準では、翻訳元の言語を自動判定し、日本語以外の文章を日本語へ翻訳します。

日本語が検出された場合は reverseLanguage が使用され、日本語から英語へ翻訳されます。

1. 選択範囲を翻訳する場合

翻訳したいテキストを選択します。

The request was successfully completed.

マクロを実行すると、選択範囲の最終行の直下に翻訳結果が挿入されます。

The request was successfully completed.
----- 翻訳: en -> ja -----
リクエストは正常に完了しました。
--------------------------

複数行をまとめて選択して翻訳することもできます。

翻訳中はカーソル付近に「選択範囲を翻訳中...」と表示され、翻訳が完了するとステータスバーに「翻訳完了」と表示されます。

2. カーソル位置の行を翻訳する場合

テキストを選択していない状態でマクロを実行すると、カーソルがある行全体を翻訳します。

例えば、次の行にカーソルを置きます。

This method returns the current configuration.

マクロを実行すると、次のようになります。

This method returns the current configuration.
----- 翻訳: en -> ja -----
このメソッドは現在の設定を返します。
--------------------------

空行でマクロを実行した場合は翻訳を行わず、ステータスバーに「翻訳するテキストがありません」と表示されます。

3. 日本語を英語へ翻訳する場合

標準設定では翻訳先言語に日本語 (ja) が設定されています。

翻訳元の言語を自動判定している場合、Google 翻訳によって日本語が検出されると、自動的に reverseLanguage で指定した言語へ翻訳します。

標準では reverseLanguage が英語 (en) のため、例えば、

ユーザー情報を取得する

を翻訳すると、

ユーザー情報を取得する
----- 翻訳: ja -> en -----
Get user information
--------------------------

のように英語へ翻訳されます。

このため、マクロを切り替えたり設定を書き換えたりすることなく、日本語と英語を相互に翻訳できます。

4. 翻訳する言語を固定する場合

翻訳元の言語を固定したい場合は、sourceLanguage を変更します。

例えば、英語から日本語への翻訳に固定する場合は次のように設定します。

sourceLanguage: "en",
targetLanguage: "ja",

この場合、言語の自動判定や逆方向への翻訳は行われません。

翻訳元を自動判定する場合は、次のいずれかを指定します。

sourceLanguage: "auto",

または、

sourceLanguage: "",

翻訳先を変更したい場合は targetLanguage を変更します。

例えば、中国語へ翻訳する場合は、

targetLanguage: "zh-CN",

のように設定できます。

5. 変数名を翻訳する場合

本マクロでは、プログラムで使用される変数名やメソッド名を翻訳しやすくするため、単独の識別子を自動的に単語へ分割する機能があります。

例えば、

currentUserName

を翻訳すると、内部的には、

current User Name

として Google 翻訳へ送信されます。

同様に、

current_user_name

や、

current-user-name

も、

current user name

として翻訳されます。

次のような形式に対応しています。

  • camelCase
  • PascalCase
  • snake_case
  • kebab-case
  • XMLHttpRequest のように大文字が連続する識別子

例えば、ソースコード中に、

const requestTimeoutSec = 10;

という変数があり、requestTimeoutSec の意味を確認したい場合、その部分だけを選択してマクロを実行できます。

通常の英文や文章については、この変換は行われません。

6. 翻訳結果の表示形式

翻訳結果は、元の文章の直下に次の形式で挿入されます。

----- 翻訳: 翻訳元言語 -> 翻訳先言語 -----
翻訳結果
------------------------------------------

例えば英語から日本語へ翻訳した場合は、

----- 翻訳: en -> ja -----
翻訳された文章
--------------------------

と表示されます。

翻訳元の言語は Google 翻訳による自動判定結果が使用されます。

複数行の文章を翻訳した場合も、翻訳結果全体をヘッダーとフッターで囲んだ状態で挿入します。

翻訳後は、挿入された翻訳結果の次の行へカーソルが移動するため、そのまま文章の編集を続けられます。

7. エラーが発生した場合

通信エラーなどが発生した場合は、翻訳結果を挿入せず、エラーメッセージを表示します。

主に次のようなエラーを検出します。

  • Google 翻訳への接続がタイムアウトした
  • curl.exe の実行に失敗した
  • Google 翻訳から空の応答が返された
  • Google 翻訳から HTML のエラーページが返された
  • Google 翻訳から想定していない形式のデータが返された
  • Google 翻訳のレート制限に達した
  • 翻訳結果が空だった

通信のタイムアウト時間は CONFIG.timeoutSec で変更できます。

例えば、20 秒に変更する場合は次のように設定します。

timeoutSec: 20,

通常は標準値の 10 秒のままで問題ありません。

ソースコード

#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 を編集してください。
 * display は複数同時に true にできます(組み合わせ可)。
 */

const CONFIG = {
  /** 翻訳元言語。空文字または "auto" で自動判定 */
  sourceLanguage: "auto",
  /** 翻訳先言語 */
  targetLanguage: "ja",
  /** 検出言語が targetLanguage と同じときの翻訳先 */
  reverseLanguage: "en",
  /** curl のタイムアウト(秒) */
  timeoutSec: 10,

  /**
   * 翻訳結果の表示方法(複数同時に有効化可)
   * - editor: エディターへ挿入(選択範囲の直下)
   * - outputBar: アウトプットバーへ出力
   * - tip: ツールチップで表示
   */
  display: {
    editor: true,
    outputBar: false,
    tip: false,
  },

  /**
   * アウトプットバー出力時に自動で表示するか
   * true で表示、false で表示しない
   */
  outputBarVisible: true,

  /**
   * tip 表示時の位置フラグ
   * meShowTipPosCaret (キャレット位置) | meShowTipPosMouse (マウス位置)
   */
  tipFlags: meShowTipPosCaret,
};

const ENDPOINT = "https://translate.google.com/translate_a/single";

const sel = document.selection;

main();

function main() {
  let keepResultTip = false;
  try {
    const target = resolveTranslateTarget();
    if (!target) {
      Status = "翻訳するテキストがありません";
      return;
    }

    if (!hasAnyDisplayTarget()) {
      throw new Error(
        "CONFIG.display のいずれか(editor / outputBar / tip)を true にしてください。"
      );
    }

    let tipMessage = "";
    if (sel.IsEmpty) {
      tipMessage = "この行を翻訳中...";
    } else {
      tipMessage = "選択範囲を翻訳中...";
    }

    ShowTip(tipMessage, meShowTipPosCaret);
    Status = "翻訳中…";

    const outcome = runTranslation(target.text);
    keepResultTip = presentTranslation(
      outcome.translatedText,
      outcome.sourceLanguage,
      outcome.targetLanguage
    );

    Status = "翻訳完了";
  } catch (error) {
    const message =
      error && error.message ? error.message : String(error);
    Status = "";
    alert("Translator: " + message);
  } finally {
    // tip 結果表示を残す場合は非表示にしない
    if (!keepResultTip) {
      ShowTip("", meShowTipHide);
    }
  }
}

function hasAnyDisplayTarget() {
  const display = CONFIG.display || {};
  return !!(display.editor || display.outputBar || display.tip);
}

/**
 * CONFIG.display に従って翻訳結果を出力する。
 * tip を使った場合は true を返し、進捗チップの非表示を抑止する。
 */
function presentTranslation(translatedText, sourceLanguage, targetLanguage) {
  const display = CONFIG.display || {};
  const block = formatTranslationBlock(
    translatedText,
    sourceLanguage,
    targetLanguage
  );

  if (display.editor) {
    insertTranslationBelow(translatedText, sourceLanguage, targetLanguage);
  }

  if (display.outputBar) {
    writeTranslationToOutputBar(block);
  }

  if (display.tip) {
    const flags =
      CONFIG.tipFlags == null ? meShowTipPosCaret : CONFIG.tipFlags;
    ShowTip(
      formatTranslationTip(translatedText, sourceLanguage, targetLanguage),
      flags
    );
    return true;
  }

  return false;
}

function formatTranslationBlock(translatedText, sourceLanguage, targetLanguage) {
  const header =
    "----- 翻訳: " +
    sourceLanguage +
    " -> " +
    targetLanguage +
    " -----";
  const footer = repeatChar("-", header.length);
  return {
    header: header,
    footer: footer,
    translatedText: translatedText,
    full: header + "\n" + translatedText + "\n" + footer,
  };
}

/** ツールチップ用の短い表示形式 */
function formatTranslationTip(translatedText, sourceLanguage, targetLanguage) {
  return (
    "[翻訳: " +
    sourceLanguage +
    " -> " +
    targetLanguage +
    "]\n" +
    translatedText
  );
}

function writeTranslationToOutputBar(block) {
  // Visible は CONFIG.outputBarVisible が true のときだけ立てる
  if (CONFIG.outputBarVisible) {
    OutputBar.Visible = true;
  }
  OutputBar.Writeln(block.full);
}

/**
 * 選択があればそれを使う。未選択ならカーソル行を選択してから返す。
 */
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 block = formatTranslationBlock(
    translatedText,
    sourceLanguage,
    targetLanguage
  );
  const body = "\n" + block.full;

  // 文書末尾なら、フッター直後に空行を残せるよう末尾改行を付ける
  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, '\\"') + '"';
}

更新履歴

  • 2026-08-12 出力方法を設定できるようにしました
  • 2026-08-11 公開
スポンサーリンク