戻る

翻訳ファイルなしのi18n

Jackie Chen avatarJackie Chen
guideinternationalizationnextjsi18ngt-nexttranslation-filesdeveloper-experience

JavaScriptアプリを国際化したことがある人なら、誰でもこのワークフローを知っています。i18nライブラリをインストールし、en.jsonファイルを作成して、ユーザー向けの文字列をすべてコンポーネントから取り出し、それぞれにキーを割り当て、文字列があった場所ではそのキーを参照するようにします。次に、そのJSONファイルを対応するすべての言語向けに複製します。es.jsonfr.jsonja.jsonといった具合です。

最初のうちは問題ありません。文字列が30個、言語が3つなら、90エントリです。

ところが、アプリは成長します。6か月後には、文字列が400個、言語が12になります。十数個のファイルにまたがって4,800エントリです。開発者が新機能を追加し、5つの新しい文字列を書いたのに、3つの翻訳ファイルの更新を忘れます。東京のユーザーが日本語インターフェースの中に英語のフォールバックを見つけるまで、誰も気づきません。誰かが翻訳管理システムの導入を提案します。

ファイル管理の負担

翻訳ファイルは、対応する言語が増えるほど保守の負担も大きくなります。文字列を1つ変えるだけで、十数個のファイルを更新しなければなりません。そうしたファイルは次第に同期がずれていきます。足並みをそろえて保つには、ツールや自動化、あるいは徹底した運用が必要です。

そうした回避策の数々が、その実情を物語っています。翻訳キーを自動生成する VS Code 拡張機能、TypeScript インターフェースに対して JSON を検証する型生成ツール、未使用キーを検出するリンター、すべてのロケールの sync が取れていることを確認する CI チェック。うまく機能しているワークフローのために、こうしたツールをわざわざ作ることはありません。

UI では1つのことを示しているのに、翻訳ファイルには別のことが書かれていて、その2つをキー ベースのマッピングでつなぎ留めています。

function CheckoutSummary({ itemCount, discount }) {
  const { t } = useTranslation('checkout');

  return (
    <div>
      <h2>{t('summary.title')}</h2>
      <p>{t('summary.item_count', { count: itemCount })}</p>
      {discount && <p>{t('summary.discount_applied', { percent: discount })}</p>}
      <p>{t('summary.total_label')}</p>
      <small>{t('summary.tax_notice')}</small>
    </div>
  );
}

このコンポーネントが何をレンダリングするのかを把握するには、checkout.json を開き、summary 名前空間を探し、5 つのキーを突き合わせる必要があります。これが何百ものコンポーネントにまたがると、コードレビューはファイルをあちこち渡り歩く作業になってしまいます。

ICU メッセージ構文は、この問題をさらに深刻にします。i18nextreact-intlnext-intl などのフォーマットライブラリが複数形、性別、補間に使う形式は、JSON 文字列の中に埋め込まれた独自のミニ言語です。{count, plural, one {# item} other {# items}} では、波かっこを 1 つ置き間違えただけでも、問題が表面化するのは Runtime になってからです。これをチェックするリンターもありますが、ほとんどのチームは使っていません。対照的に、inline コードなら、入力したその場で TypeScript compiler と IDE にチェックしてもらえます。

コードを直接翻訳する

そもそも文字列を抽出しないとしたらどうでしょうか?

import { T } from 'gt-react';

function CheckoutSummary({ itemCount, discount }) {
  return (
    <T>
      <div>
        <h2>Order Summary</h2>
        <p>You have {itemCount} items in your cart.</p>
        {discount && <p>{discount}% discount applied!</p>}
        <p>Total (before tax):</p>
        <small>Tax calculated at checkout.</small>
      </div>
    </T>
  );
}

gt-react<T> コンポーネントは、JSX のブロックを翻訳対象としてマークします。英語はそのままコンポーネント内に残ります。ユーザーがスペイン語でアクセスすると、<T> 内の内容は対応するスペイン語に置き換えられます。構造や書式もそのまま引き継がれます。

t('checkout.summary.title')en.json も、ロケールごとに sync しておく JSON ファイルも必要ありません。

ビルド出力として生成される翻訳

これらはデプロイ時に生成されます。GT CLI はコードベース内の <T> コンポーネントの中身をスキャンし、対象言語ごとの翻訳を生成します。出力先は、コンパイル済みの CSS やバンドル済みの JavaScript と同じように、.gitignore で除外されたディレクトリです。

{
  "defaultLocale": "en",
  "locales": ["es", "fr", "ja", "de", "ko", "zh"],
  "files": {
    "gt": {
      "output": "public/_gt/[locale].json"
    }
  }
}
public/_gt/

開発時の翻訳は on-demand で行われます。文字列を変更してリフレッシュすれば、すぐに日本語で確認できます。本番環境では、すべて事前生成され、CDN から配信されます。翻訳ファイル自体は引き続きディスク上に存在しますが、それらは出力成果物であり、自分で作成・保守するものではありません。

実際に何が変わるのか

その違いは、アーキテクチャ図よりも日々の実務の中でよりはっきり表れます。

ページに新しいセクションを追加するPRは、ただページに新しいセクションを追加するPRとして読めます。レビュアーの目に入るのは実際の文言であり、checkout.summary.discount_applied_noticeではありません。コードレビューのたびにJSONファイルと横に並べて確認する必要もなくなります。

リファクタリングの負担も減ります。コンポーネント名を変える、移動する、分割する。キーを付け替える必要がないため、翻訳はコンテンツにそのまま追従します。言語の追加も、設定を1行変えるだけで済みます。コンポーネントを削除すれば、その翻訳は自然に生成されなくなり、誰も片付けたがらない孤立したキーが15個のファイルに散らばって残ることもありません。

i18nライブラリのこれから

エコシステム全体として、流れはインライン文字列へ向かっています。i18nextreact-intl のような初期の i18n ライブラリは、機械翻訳がまだ実用的ではなく、すべての文字列を人手で翻訳に回す必要があった時代に作られました。そのため、辞書は受け渡し用の形式として理にかなっていました。しかし、そうした制約はすでになくなっており、文字列ファイルを別管理で維持することによる開発体験上のコストは、ますます見合わなくなっています。

next-intl は、辞書モードに加えて、辞書を使わない t() 呼び出しも導入しました。Lingui's Compiler は、インラインの tagged template からビルド時にメッセージを抽出します。Paraglide は別のアプローチを取り、メッセージファイルをロケールごとの tree-shakeable な関数へコンパイルします。アプローチはそれぞれ異なりますが、いずれもコンテンツはコンポーネントの近くへ移ってきています。GT はその流れを突き詰めたものです。つまり、JSX が信頼できる唯一のソースであり、翻訳はコンパイル工程の一部になります。

トレードオフ

コンテンツを インライン で記述すると、自然と自分の母語で書くことになります。コンポーネントの構造、文の組み立て方、UI の流れはすべて、英語 (あるいはソース言語が何であれ) で考える発想を反映します。一方、next-intl のような dictionary ベースのアプローチは、設計上、より言語に依存しません。コンポーネント内にはどの言語の実際の文も含まれず、別の場所を参照するキーだけを置くからです。

ただし、ほとんどの開発者は UI を作るとき、すでにひとつの言語で考えています。レイアウト、文言、ボタンラベルは、どれもまず英語で発想されます。そのバイアスは、strings が インライン にあるか JSON ファイルにあるかに関係なく、設計に織り込まれています。私たちは、i18n フレームワークは実際の開発スタイルに合わせるべきだと考えています。言語中立性のためにコンテンツをキーに抽象化するのではなく、自然な形でアプリを構築し、翻訳はフレームワークに任せるべきです。

はじめに

npx gt@latest init

セットアップウィザードは、プロジェクトを設定し、依存関係をインストールし、開発時の翻訳のホットリロードを設定します。詳しい手順は Quickstart ガイド を参照してください。

gt-react はオープンソースです。Next.js App Router 向けには gt-next があります。React Native 向けには gt-react-native があります。