JSON形式の翻訳ファイルをアルファベット順にソートしたい

国際化対応のつらみポイント

自分の関わっているプロダクトでは国際化対応(internalization = i18n対応)を行なっている。 国際化対応はi18nと名前に入っているライブラリで実施することが多い。1

大体どのライブラリでも、翻訳の流れは決まっている。

  1. テキスト定義をJSON形式の翻訳ファイルに記載する
  2. レンダリングしたい箇所で描画したい定義のキーを指定する
  3. 定義ファイルにキーがあれば指定の言語でレンダリングできる

ここまでは分かりやすくていいのだが、問題はアプリの運用保守期間が長くなったとき。 機能追加やヘルプテキストを差し込んでいく量に比例して定義量も増えてしまう。すると、次のようなつらみポイントが出てくる。

  • キーをどこに書くか迷う
  • キー名を探すのが大変
  • キーの被りがないか探すのが大変

Multiple Translationのような機能を使いつつファイルを分ければいいかとも思ったが、結局keyのソートに迷う問題は解決しない。 大体定義はJSONの形で持つことが多いので、コミット前に翻訳ファイルをいい感じにソートしたい。

prettier-plugin-sort-jsonを使う

最新バージョンはPrettier v3以降でしか利用できないが、Gudahttさんが作ったprettier-plugin-sort-jsonを使うという手段がある。

https://www.npmjs.com/package/prettier-plugin-sort-json

方法

ライブラリをインストールする。

npm install --save-dev prettier-plugin-sort-json

prettierrcpluginsセクションに追記する。

"plugins": ["prettier-plugin-sort-json"]

あとは通常通りPrettierを実行する。

npx prettier --write JSONファイルのパス

huskyとlint-stagedを組み合わせてセットアップしておけば、コミット前に実行できる。

// package.json
"lint-staged": {
  "*.json": "prettier --write" // コミット前にJSONファイルにPrettierタスクを実行する
},

所感

単純で良いし、プラグインをセットするだけでいいので楽。 ただ、長期メンテをする際はライブラリのバージョンアップ対応が必要なのが面倒か。

参考URL

sort-package.jsonでソートする

package.jsonのkey情報でソートするライブラリがあるので、それを使ってJSONファイルもソートしてしまう案。

https://www.npmjs.com/package/sort-package-json

方法

ライブラリをインストールする。

npm install --save-dev sort-package-json

scriptsにsort-package-jsonの実行と「JSONファイルに対してPrettierを実行する」タスクを登録する。

"scripts": {
  "prettify-json": "prettier --write \"**/*.json\"", // JSONファイルにはPrettierを上書き実行する
  "sort-json": "sort-package-json" // sort-package-jsonで並べ替えする
}

sort-package-jsonの実行スクリプトをpackage.jsonに登録する。

"scripts": {
  "format-json": "npm run sort-json && npm run prettify-json" // 2つのタスクを実行する = ソートして上書き
}

あとはscriptsを実行すれば良い。

npm run format-json

lint-stagedを使うなら次のようにしておく。

// package.json
"lint-staged": {
  "*.json": "npm run sort-json && npm run prettify-json" // コミット前にJSONファイルにPrettierタスクを実行する
},

所感

package.jsonをソートする用のライブラリで翻訳ファイルをソートするのは直感的ではないなと思った。

翻訳ファイルのネストが深いのであればこちらを使うほうが良いかもしれない。 package.jsonはscriptsキーにさらに設定の名前のキーを入れて…のようにネストを深くするタイプである。 オブジェクトのネストが深いものをプログラムでソートするのは面倒なので、最初から対応しているものを使うのもいいかなと思った。

参考URL

Node.jsのAPIを使ってJavaScriptでソートする

Node.jsのFile Stream APIを使って翻訳ファイルを読み取り、Array.prototype.sort()した結果をObjectに変換する。 その結果をPrettierでフォーマットしてから上書きしてやれば良い。

  1. fs.readFileSyncを使って多言語定義ファイルを読み取り
  2. PrettierのAPIでJSON形式に変換
  3. 結果をfs.writeFileSyncで上書きする。

prettier.formatAPIが非同期に動作するため、この結果を待って書き込むようにする。 こうしないと処理が終わる前に書き込みを試みてしまい、うまくいかない。

sortObjectKeysの処理で落ちてしまわないように、一度JSONをオブジェクト形式に変換しておく。 適当に定義を書いてコミットしても、変換をかけてから実行するので安全。

上書き前は文字列からJSONに戻す。

const fs = require('fs');
const prettier = require('prettier');

// 翻訳ファイルのパスを配列で指定する
const FILE_PATHS = ['./src/i18n/translation/ja.json', './src/i18n/translation/en.json'];

// オブジェクトのキーをソートする関数
const sortObjectKeys = (unsortedObj) => {
  return Object.keys(unsortedObj)
    .sort() // 引数がないのでUTF-16 コード単位でソートされる
    .reduce((obj, key) => {
      obj[key] = unsortedObj[key]; // ソートした結果でkeyを並び替える。valueはそのまま
      return obj;
    }, {});
}

// ファイルが2つあるので繰り返し処理する
for (const FILE_PATH of FILE_PATHS) {
    // ファイルを読み込む
    const rawJson = fs.readFileSync(FILE_PATH, 'utf8');
    const json = JSON.parse(rawJson);

    // キーをソート
    const sortedJson = sortObjectKeys(json);

    // Prettierでフォーマット
    const formattedJson = prettier.format(JSON.stringify(sortedJson), { parser: 'json' });

    // JSONのフォーマット完了を待ってからファイルに書き込む
    formattedJson.then((formattedJson) => {
        fs.writeFileSync(FILE_PATH, formattedJson);
    });
}

ファイルを作れたら、package.jsonにスクリプトを登録する。

// 配置先が`<rootDir>/scripts/sort-json.js`だった場合
"scripts": {
  "sort-json": "node scripts/sort-json.js", // 追加
}

実行すればソートされた状態で上書きされる。

npm run sort-json

lint-stagedを使うなら次のようにしておく。

"scripts": {
"./src/i18n/translation/*.json": "npm run sort-json"
}

所感

ライブラリのメンテを考慮しなくて良いのは気楽。 ただ、Node.jsのバージョンアップでAPIが変わってしまうとメンテは必要。JavaScript系はメンテを仲良くやる必要があるので、どの手段を取っても手入れは必要か。

ソートロジックにテストを書けると良いが、ESModules⇆CommonJSの壁にぶち当たってしまう。 また、ファイルを読み取って書き込むというところはテストでは再現できない。ソートロジックとJSON変換くらいしかテストできなさそう。

参考URL

まとめ

意外と簡単にソートできてよかった。やることが単純なのでいいのかもしれない。 ネストが深くなればなるほどソートが面倒になるので、翻訳ファイルもシンプルに保つようになりそう。

ライブラリ系は配信やメンテが止まってしまうと代替手段を探すなり、メンテするなりが必要。 APIを使って自分でソートしても良いが、これもメンテは必要。今はGitHub Copilotなんかもあるしスクリプトくらいなら自分で作ってもいいかもしれない。


  1. i18nextやフレームワーク用のライブラリを使うことが多いと思う。