📘

翻訳データを TypeScript で自動補完&ミス防止しながら扱う

2023/08/27に公開

グローバルにアクセスされる Web サイトの制作などで TypeScript を使用するとき、サイト中の単語や文章の翻訳はどのように実現しているでしょうか? i18next などのライブラリを使う、フレームワークの機能を使うなどの方法があると思います。

以下では私が TypeScript で多言語化対応をしていたとき欲しいと感じた API とコンセプトの説明のために実装したライブラリを紹介します。

ライブラリの使用感としては動画のような感じです。以降でモチベーションとライブラリの機能について説明します。

https://www.youtube.com/watch?v=HRZUUtAOc_c

API に求めること

私は Web フロントエンドエンジニアで HTML も書くので React や Svelte などをよく使います。そのコンテキストで考えます。

単語や文章のキーと翻訳後の値を対応させたデータを扱うことになります (例: greet の日本語 → こんにちは)。言語切り替えはユーザー単位で行うため暗黙のパラメーターとして translate("greet") のように呼び出すことができる API が良いでしょう (i18next と同様)。

翻訳は外部にお願いすることになるので、非プログラマーとのやり取りがスムーズにできる形が望ましいです。基本的には表 (テーブル) で扱えるデータフォーマット、せいぜい JSON くらいが限界でしょう。
つまり翻訳データを用意する人はソースコードを触らない前提とします。翻訳データをエクセルなどのフォーマットで受け取り、プロダクトに反映するのが手軽にできなければなりません。

タイポなどの簡単なミスをなるべく減らしたいため TypeScript の型チェックを活用したいです。
translate("greed") などのミスを検知できるようにしたいです。

パラメーターも埋め込めるようにしたいです。言語・地域ごとに数字と単位の並び順が異なる場合があるなど。たとえば fee → "手数料 {{ amount }}円", translate("fee", { amount: 1000 }) など。
またこれもミスを減らすために型チェックを活用したいです。「翻訳者がプログラマが想定していたパラメーターのみ使っているか (手数料 {{ anount }}円 などのスペルミスがないか)」と「プログラマがパラメーターを漏れなく指定できているか (translate("fee", {}) などの指定忘れがないか)」を TypeScript で自動的にチェックできると良いです。
TypeScript で型チェックができると自動補完もできるようになるはずなので便利です。

あると便利レベルですが、リファクタリングや不具合調査のために使用箇所が調査できると良いです。たとえば greet の翻訳データを使っている箇所をすべてリストアップするなど。IDE の機能で関数の呼び出し箇所を一覧化することがよくあると思いますが、あれが翻訳データに対してもできると嬉しいです。

作ったもの

以上を踏まえて、こういうのがあったらいいなという感じでライブラリを作りました。
実用目的というよりは「コンセプトの説明」&「面白そうだからやった」という感じになります。

https://www.npmjs.com/package/@koharakazuya/typescript-i18n

npm でインストールできます。リンク先にもありますが、サンプルコードは以下のようになります。

import {
  checkSchemaParams,
  i18n,
  type Dictionary,
  type DictionarySchema,
} from "@koharakazuya/typescript-i18n";

// 翻訳データは `{ system: { title: "タイトル" } }` のように JSON のような形式で
// 構造で定義します。末端が文字列となっていれば自由にネストできます。
// また、`"年齢は{{age}}です。"` のように `{{変数名}}` で変数を埋め込むことができます。
// ここでは翻訳データの構造を定義します。データの構造とパラメーターについて定義してください。
// `extends DictionarySchema` は必須です。
interface MyDictionarySchema extends DictionarySchema {
  title: { brand: string; year: number };
  message: {
    submit: {};
    error: { amount: number };
    ok: {
      alert: {};
    };
  };
}

// 各言語ごとの翻訳データを定義します。翻訳データの構造に沿って定義してください。
// 変数自体に型は宣言せず、`as const satisfies Dictionary<MyDictionarySchema>`
// をつけなければならないことに注意してください。
const ja = {
  title: "{{brand}} from {{year}}",
  message: {
    submit: "送信しました",
    error: "エラーが発生しました: {{amount}}",
    ok: { alert: "OK" },
  },
} as const satisfies Dictionary<MyDictionarySchema>;
// 翻訳データ中で使ったパラメーターがデータ構造中に存在するかチェックします。
// 不足しているとコンパイルエラーになります。関数の中身は空なので実行時は何も実行しません。
checkSchemaParams<typeof ja, MyDictionarySchema>();

// 各言語ごとに翻訳関数を定義します。
const t = i18n<MyDictionarySchema>(ja);

// 翻訳関数を使って翻訳した文字列を取得します。
// どのデータを使用するか第一引数で指定します。このときエディタの補完機能が使えます。
// また、間違ったデータを指定した場合はコンパイルエラーになります。
// 第二引数には翻訳データのパラメーターを指定します。同様にエディタの補完機能が使えます。
const s1 = t((x) => x.message.error, { amount: 100 });
const s2 = t((x) => x.message.submit, {});
const s3 = t((x) => x.title, { brand: "Example", year: 2023 });

console.log({ s1, s2, s3 });

翻訳者から JSON (テーブル → JSON の変換は容易なはず) を受け取り、TypeScript に埋め込む流れを想定しています。JSON は TypeScript のサブセットなので単純なコピペで埋め込めます。

プログラマーがどのような翻訳データが必要か、そこに埋め込むパラメーターが何かを宣言し、翻訳者から受け取ったデータを TypeScript が自動チェックする形です。翻訳者が未知のパラメーターを使っていればエラーとなります。

プログラマは翻訳データを選択し (自動補完が効きます)、それに応じたパラメーターを指定します。ここでもパラメーターがエラーとなります。

実装のソースコードは数行だけです。

まとめ

TypeScript で翻訳データを扱うときに求めることと、それを実現するためのライブラリを紹介しました。手軽にミスなく翻訳データを扱えるようにしたいですね。もっと良さげなライブラリがあったらぜひ教えてください。

Discussion