Communitygithub.com

Web i18n

プロダクションレベルの国際化・地域化スキル。複数言語対応、UI翻訳、ハードコード文字列の抽出、複数形・性別・ICUメッセージ処理、ロケールルーティング、RTLレイアウト、日付・数値・通貨のローカライズ、hreflang/SEO、翻訳カタログ検証に対応。React、Next.js、Vue、Svelte、Astro、Remix、バニラJS向けのフレームワーク非依存ガイダンスを提供し、next-intl、react-i18next、FormatJS、Linguiの例を含みます。

Web i18n とは?

このスキルは、Webアプリケーションのプロダクションレベルの国際化(i18n)を実現するための総合的なガイドです。翻訳の最終工程ではなく、アーキテクチャそのものとして国際化を設計することを目的としています。 このスキルが役立つのは、複数言語の追加、言語スイッチャーの実装、翻訳ワークフローの構築、複数形/性別によるメッセージの分岐、日付や数値、通貨のロケール対応書式設定、RTL(右横書き)レイアウト対応、hreflangやローカライズされたメタデータによるSEO対策、既存のi18n設定のレビュー、あるいはnext-intl、react-i18next、FormatJS、Linguiなどのライブラリ選定に悩んでいる場合です。 このスキルでは、フレームワーク非依存のベストプラクティスを示しつつ、React、Next.js、Vue、Svelte、Astro、Remix、およびバニラJSそれぞれの具体的なセットアップや注意点を説明します。ICU MessageFormatによるメッセージ構文、Intl APIによるロケール依存の書式設定、ページ分割によるカタログの遅延ロード、CIでのカタログ検証など、保守性と拡張性を重視したアプローチを取ります。 徹底的なチェックリストとアンチパターンの提示により、開発者がよく陥る落とし穴(文字列の連結、英語をキーに使う、ハードコードされた書式など)を回避し、本番環境に耐えるi18nシステムを構築できるよう支援します。

対応~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/Nelsutro/agent-skills/tree/HEAD/web-i18n

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

Web i18n は何をしますか?

国際化はアーキテクチャであり、最後に行う翻訳作業ではありません。目的は、特別なケースを設けずに、どのロケールでも正しく表示されるUIを実現することです。すなわち、連結された文、ハードコードされた書式、ドイツ語で35%長くなったりアラビア語で方向が反転したりすると壊れるレイアウトを排除します。

このスキルを使うタイミング

ユーザーが以下を依頼してきた場合に使用します:

  • 言語、言語スイッチャー、またはロケールルーティングの追加
  • アプリの翻訳、文字列の抽出、翻訳ワークフローのセットアップ
  • 複数形、性別、または「1 item / 2 items」バグの修正
  • ロケールに応じた日付、数値、通貨、相対時間、リストの書式設定
  • RTL(アラビア語、ヘブライ語、ペルシア語、ウルドゥー語)のサポート
  • hreflang、ローカライズされたメタデータ、または多言語サイトマップの追加
  • 既存のi18n設定、欠落キー、または翻訳のずれのレビュー
  • next-intl、react-i18next、FormatJS、Lingui、またはTMSの選択

関連するリファレンスは references/ に、スクリプトは scripts/ にあります。タスクに必要なものだけを読んでください。パスはこのスキルのディレクトリからの相対パスです(エージェントランタイムはスキルが読み込まれたときにそのパスを表示します)。

絶対条件

  1. 翻訳された断片を連結して文を構築しないこと。 プレースホルダーを含むメッセージ全体を翻訳します: "{count} 件の新着メッセージ" 。t('new') + ' ' + count + ' ' + t('messages') は不可。
  2. キーとして英語テキストを使用しないこと。 キーは安定した識別子(inbox.unreadCount)であり、コピーの編集によってルックアップが壊れたり、すべての翻訳が無効になったりしないようにします。
  3. ユーザー向けのすべての文字列は外部化する。aria-label、alt、title、placeholder、バリデーションエラー、空の状態、トースト、メール、プッシュ通知、PDF出力を含みます。
  4. 書式をハードコードしないこと。 toFixed(2) + '$'、手動での MM/DD/YYYY、英語の月名、24h の仮定は禁止。Intl を使用すること。
  5. ロケールはBCP 47タグであり、言語ではない。 pt-BR と pt-PT、zh-Hans と zh-Hant、es-MX と es-ES は別のプロダクトです。完全なタグを保存すること。
  6. サーバー上では、ロケール状態はリクエストスコープでなければならない。 SSR中に現在のロケールをモジュールレベルの可変変数に決して入れないでください。同時リクエストが互いに言語を漏洩させます。
  7. TMS、コミュニティ、CMSから来た翻訳は信頼できない入力として扱う。 サニタイズせずに innerHTML/dangerouslySetInnerHTML に渡さないこと。

ワークフロー

1. コードを変更する前に監査する

  • スタック、ルーター、既存のi18nライブラリを何かを提案する前に特定する。
  • ハードコードされた文字列を見つける:
    node scripts/scan-hardcoded.mjs src
    
    出力は候補として扱い、絶対視しない:テストフィクスチャ、定数、URL、開発者向けログはスキップすること。
  • 現在出荷されているすべてのロケールと、カタログの保存場所を一覧化する。
  • リスクの高い領域を最初に特定する:認証、チェックアウト、エラー、メール、法的コピー。

2. アーキテクチャを決定する(文書化する)

コードを書く前に以下に答える:

決定事項デフォルト備考
ロケール戦略パスプレフィックス /es/SEOとキャッシュ可能性に最適。サブドメイン/ccTLDは理由がある場合のみ。
デフォルトロケールソース言語初回訪問者を推測したロケールに自動リダイレクトしない。オーバーライド手段を提供すること。
ロケール検出URL → Cookie → Accept-Language → デフォルトURLが優先されるため、共有リンクは言語を変更しない。
カタログ形式ロケールごと、名前空間ごとのJSON名前空間を遅延ロードする。すべてのロケールをクライアントに出荷しない。
メッセージ構文ICU MessageFormatライブラリやTMS間で移植可能。複数形/選択/序数をサポート。
キースタイルfeature.component.element安定しており、grep可能で、英語テキストを含まない。
翻訳の信頼できるソースTMSまたはリポジトリ、両方ではない一つ選び、CIで一方向に同期する。
フォールバックロケールチェーン es-MX → es → en開発環境でフォールバックをログ出力し、ギャップを検出する。

3. メッセージを外部化し構造化する

  • 文字列をセマンティックキーに置き換え、コピーをベースロケールカタログに移動する。
  • 完全な文ごとに1つのキーを使用する。文にリンクや太字の単語が含まれる場合は、分割せずにリッチテキスト補間を使用する。
  • プレースホルダーには名前を付ける({userName})。位置指定({0})は禁止。
  • ライブラリが明示的に型付きタグをサポートしていない限り、カタログにHTMLを入れない。
  • ページ単位ではなく、プロダクトサーフェス領域ごとにグループ化する: checkout.json、auth.json、errors.json。

複数形、選択、序数、リッチテキストのルールについては references/icu.md を参照。

4. ロケールルーティングとレンダリングを配線する

  • ロケールをルートから導出し、サポートされているリストに対して検証し、未知のロケールは静かにフォールバックするのではなく404を返す。
  • 現在のパスとクエリ文字列を保持する言語スイッチャーを提供する。
  • サーバーでレンダリングされたドキュメントに <html lang="..."> と <html dir="..."> を設定する。
  • アクティブなロケールのカタログとデフォルトのフォールバックのみを読み込む。
  • 同じカタログをサーバーとクライアントのレンダリングで利用できるようにし、重複したり分岐した翻訳ロジックを避ける。

フレームワーク固有のセットアップと注意点:references/frameworks.md。 ルーティング、検出、hreflang、サイトマップ:references/routing-seo.md。

5. ロケール依存の値を処理する

手動での書式設定は行わず、Intl(またはフレームワークのラッパー)を使用する:

new Intl.NumberFormat(locale, { style: 'currency', currency }).format(amount)
new Intl.DateTimeFormat(locale, { dateStyle: 'medium', timeZone }).format(date)
new Intl.RelativeTimeFormat(locale, { numeric: 'auto' }).format(-3, 'day')
new Intl.ListFormat(locale, { type: 'conjunction' }).format(items)
new Intl.PluralRules(locale).select(count)
new Intl.Collator(locale).compare(a, b)
  • タイムスタンプはUTCで保存し、ユーザーのタイムゾーンでレンダリングする(サーバーのタイムゾーンではない)。
  • 通貨はロケールと同じではない:フランス人のユーザーがUSDで支払う可能性がある。
  • ソート/検索には Intl.Collator を使用し、< やロケールを指定しない localeCompare() は使用しない。
  • 大文字小文字変換:toLocaleUpperCase(locale)。トルコ語の i は İ になるため。

6. 拡張とRTLに備えた設計

  • テキストの長さが +30~40% 増加する(ドイツ語、フィンランド語、ロシア語)ことを想定し、固定幅のボタンや単一行のラベルは避ける。
  • コンテナは拡張可能にし、折り返しを許可し、擬似ローカライズでテストする:
    node scripts/pseudo-localize.mjs locales/en.json --out locales/en-XA.json
    
  • 論理CSSプロパティ(margin-inline-start、padding-inline、inset-inline)を left/right の代わりに使用する。
  • 方向性のあるアイコン(矢印、シェブロン、進行状況)はミラーリングする。ロゴ、時計、メディアコントロール、チェックマークはミラーリングしない。
  • ユーザー生成のインラインコンテンツは <bdi> または dir="auto" でラップする。

完全なRTLチェックリスト:references/rtl.md。

7. CIで翻訳QAを設定する

最低限、カタログが構造的に無効な場合にビルドを失敗させる:

node scripts/check-locales.mjs locales --base en

これは、欠落/余分なキー、空の値、プレースホルダーの不一致、壊れたICU構文をチェックします。利用可能な場合は、ライブラリのCLIを介してICUコンパイルを追加する:

npx formatjs compile-folder --ast locales/en locales/compiled/en

複数形カテゴリ、固定タイムゾーンでの日付/数値出力、RTLレンダリングのテストを追加する。references/testing.md を参照。

主要な決定事項と落とし穴

ライブラリの選択

ニーズ選択
Next.js App Router、サーバーコンポーネントnext-intl
React SPA、成熟したエコシステム、多くのプラグインreact-i18next
ICUファースト、フレームワーク非依存、CLI抽出FormatJS (react-intl)
コンパイル時カタログ、小さなランタイム、型安全性Lingui
Vuevue-i18n
Sveltesvelte-i18n または paraglide
Astro組み込みのi18nルーティング + メッセージライブラリ
バニラ / 最小限Intl + 小型カタログローダー

すでにi18nライブラリが存在するアプリに、2つ目のi18nライブラリを導入しないでください。既存の設定を拡張すること。

複数形

複数形カテゴリは言語固有です。英語には one/other があり、アラビア語には zero/one/two/few/many/other があり、日本語には other しかありません。count === 1 ? ... : ... は絶対に書かないでください。

"inbox.unread": "{count, plural, =0 {未読メッセージはありません} one {# 件の未読メッセージ} other {# 件の未読メッセージ}}"

性別と文法上の一致

性別変数に対して select を使用し、変数の値は言語に依存しない値(female、male、neutral、unknown)に保ち、ローカライズされた単語は使用しない:

"profile.greeting": "{gender, select, female {いらっしゃいませ、{name}様} male {いらっしゃいませ、{name}様} other {いらっしゃいませ、{name}様}}"

言語が形容詞、名詞、冠詞の一致を必要とする場合は、文字列をハックする代わりに、文法的な性/格を引数として公開する。

リッチテキスト

"Read our " + link + " terms" を3つのキーに分割しないでください。タグ補間を使用する:

"signup.terms": "<link>利用規約</link>に同意します"
t.rich('signup.terms', { link: (chunks) => <a href="/terms">{chunks}</a> })

SEO

  • ロケールごとに1つのインデックス可能なURL。同じURLの背後で機械翻訳されたコンテンツを提供しないこと。
  • 自己参照 hreflang に加えて x-default。絶対URLを使用し、すべてのロケールを含めること。
  • <title>、メタディスクリプション、OGタグ、構造化データをローカライズする。
  • ローカライズされたページで hreflang と noindex を組み合わせないこと。

詳細は references/routing-seo.md を参照。

キャッシュ

  • パスにロケールを含めると、CDNキャッシュが簡単になります。ヘッダーでネゴシエーションする場合は、Vary: Accept-Language を送信し、キャッシュの断片化を予期すること。
  • 共有CDNレスポンスに個人設定された言語設定をキャッシュしない。
  • Content-Language をレンダリングされたロケールと一致させる。

レビューチェックリスト

i18nが完了したと宣言する前に:

  • コンポーネント、メール、エラーにハードコードされたユーザー向け文字列がない
  • 文の連結がない。すべてのメッセージは完全な翻訳可能単位である
  • キーはセマンティックかつ安定しており、キーとして英語テキストを使用していない
  • すべてのプレースホルダーがすべてのロケールに存在し、ICU構文がコンパイルされる
  • 複数形は Intl.PluralRules/ICU を使用し、count === 1 ではない
  • 性別/一致は select で処理し、"/a" 接尾辞ではない
  • 日付、数値、通貨、相対時間は Intl を明示的なロケール/タイムゾーンで使用している
  • <html lang> と dir が初回ペイント(SSR)で正しく設定され、エフェクトで設定されていない
  • 言語スイッチャーはパス/クエリを保持し、ディープリンクで動作する
  • 未知のロケールは静かなフォールバックではなく404を返す
  • レイアウトが+35%の拡張とRTLに耐えられる
  • hreflang、canonical、サイトマップ、ローカライズされたメタデータが正しい
  • カタログ検証がCIで実行される
  • フォールバック動作が意図的であり、観察可能である

アンチパターン

  • t('hello') + ', ' + name — 断片化された文
  • key={englishText} — キーとしての翻訳テキスト
  • count === 1 ? 'item' : 'items' — 英語のみの複数形ロジック
  • ロケールやタイムゾーンを指定しない new Date().toLocaleDateString()
  • 金額に対する value.toFixed(2)
  • ドキュメントレベルの dir の代わりにコンポーネント全体に locale === 'ar' && { direction: 'rtl' } を散在させる
  • useEffect 内で <html lang> を設定する — フラッシュが発生し、クローラーが壊れる
  • すべてのロケールを初期バンドルに読み込む
  • オーバーライド手段なしにIPで自動リダイレクトする
  • 固有名詞、製品名、コード識別子、URLを翻訳する
  • 未レビューの機械翻訳文字列を出荷する
  • 1つのカタログファイルが数百キーを超えてもスケールすると想定する

関連スキル