zukucode
主にWEB関連の情報を技術メモとして発信しています。

JavaScript 翻訳ファイルのキーの追加漏れを自動で見つける

日本語の画面に項目を追加したのに、英語の翻訳ファイルを更新し忘れることがあります。

毎回すべての画面を言語ごとに開いて確認する代わりに、翻訳ファイルのキーがそろっているかを機械で確認できます。

今回は、追加のライブラリを使わず、Node.jsで2つのJSONファイルを比較する方法を紹介します。文字列の内容は言語ごとに違ってよいものとし、項目名だけを比べます。

入れ子を含むキーを比較する

locales/ja.json
{
  "profile": {
    "title": "プロフィール",
    "save": "保存"
  }
}
locales/en.json
{
  "profile": {
    "title": "Profile"
  }
}

この例では英語側のprofile.saveが不足しています。

一番上のキーだけなら、どちらにもprofileがあるため違いを見つけられません。入れ子をたどり、文字列に到達するまでのキーをprofile.saveのようにまとめます。

ファイルを配置する

  • project
    • locales
      • ja.json
      • en.json
    • scripts
      • check-locale-keys.mjs

今回は値が文字列、または入れ子のオブジェクトである形式だけを扱います。キー自体に.を含めないルールにし、配列や数値はエラーとして検出します。

比較するスクリプトを書く

scripts/check-locale-keys.mjs
import { readFileSync } from 'node:fs';

const collectKeys = (value, prefix = '', keys = new Set()) => {
  if (typeof value === 'string' && prefix !== '') {
    keys.add(prefix);
    return keys;
  }

  if (value === null || typeof value !== 'object' || Array.isArray(value)) {
    throw new Error(`文字列またはオブジェクトが必要です: ${prefix || '(root)'}`);
  }

  for (const [key, child] of Object.entries(value)) {
    if (key === '' || key.includes('.')) {
      throw new Error(`空のキーやドットを含むキーは使えません: ${key}`);
    }
    collectKeys(child, prefix ? `${prefix}.${key}` : key, keys);
  }
  return keys;
};

const readKeys = (relativePath) => {
  const file = new URL(relativePath, import.meta.url);
  return collectKeys(JSON.parse(readFileSync(file, 'utf8')));
};

const ja = readKeys('../locales/ja.json');
const en = readKeys('../locales/en.json');
const errors = [];

for (const key of [...ja].sort()) {
  if (!en.has(key)) errors.push(`en: ${key} がありません`);
}
for (const key of [...en].sort()) {
  if (!ja.has(key)) errors.push(`ja: ${key} がありません`);
}

if (errors.length > 0) {
  console.error(errors.join('\n'));
  process.exitCode = 1;
} else {
  console.log('翻訳キーが一致しています。');
}

Setには比較したいキーを入れ、hasで相手側に存在するか確認します。日本語から英語、英語から日本語の両方向を比べるため、どちらにだけ追加された場合も検出できます。

ファイルの位置はスクリプト自身を基準にしています。実行した作業ディレクトリの違いで、別のファイルを読むことを避けるためです。

コマンドで実行する

プロジェクトのルートから実行します。Node.js 20以降で使える例です。

翻訳キーの確認
node scripts/check-locale-keys.mjs

最初のJSONの例なら、次の内容が表示され、終了コードは1になります。

不足がある場合
en: profile.save がありません

英語へ"save": "Save"を追加すると成功します。JSONの構文が不正な場合やファイルがない場合も、例外により失敗します。

npmスクリプトから呼ぶ場合は、既存のpackage.jsonのscriptsへ追加できます。

package.jsonへ追加する項目
{
  "scripts": {
    "i18n:check-keys": "node scripts/check-locale-keys.mjs"
  }
}

既存のscripts全体を置き換えるのではなく、項目を追加します。ビルド前やCIで実行すれば、翻訳キーの追加漏れを早めに見つけられます。

キーが同じでも翻訳が正しいとは限らない

このチェックが確認するのは項目名です。英語側へ日本語をコピーした場合や、値が空文字列の場合でも、キーが一致すれば成功します。

また、{{name}}のような差し込み変数の一致も検査していません。

問題このスクリプトで検出できるか
英語側だけキーがないできる
日本語側だけキーがないできる
値に配列や数値がある形式エラーになる
訳文の意味が間違っているできない
差し込み変数が不足しているできない
使われていないキーが残っているできない

言語ごとに複数形のキー構成が異なるなど、翻訳ライブラリの仕様で差が必要な場合は、完全一致のルールをそのまま使えません。例外にする範囲を明示するか、その形式に対応した検証を使います。

この例は文字列を持たない空のオブジェクトをキーとして数えません。空の翻訳グループも検出したい場合は、別の条件を追加します。

使用箇所の抽出とは分けて考える

ソースコードから翻訳キーを抽出する処理と、言語間のキーを比較する処理は目的が違います。

抽出では「どのキーを使っているか」を確認し、今回の比較では「言語間で項目がそろっているか」を確認します。両方を組み合わせると、未使用の翻訳と追加漏れを別々に扱えます。

動的に選ぶ翻訳キーを抽出対象へ含める方法

小さなスクリプトでも、手作業で見比べていた構造の違いを検出できます。訳文の品質は人が確認し、機械で比べられるキーの有無は自動化すると、確認作業を分担できます。


関連記事