i18n-rtl-l10n について
複数のロケールや右から左へ読む言語に対応する Flutter アプリ向けのローカライズスキルです。ARB の翻訳手順、ロケールに応じた表示、正しく反転するレイアウトを扱います。
主な機能
- 01ARB キーとプレースホルダーの整合性
- 02ICU の複数形・選択メッセージ
- 03方向性を考慮したレイアウト
- 04数値と日付のローカライズ表示
- 05RTL と双方向テキストの処理
- 06ロケール対応とフォント範囲の検査
活用シーン
- 01ARB キーを揃えて翻訳を追加
- 02アラビア語やペルシア語向け UI の構築
- 03ローカライズされた数字と日付の書式設定
- 04方向が混在する UI で技術テキストを扱う
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ zakariaf/ flutter-skills --skill i18n-rtl-l10nインストール後、Claude Code で/i18n-rtl-l10nを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
examples/directional_widget.dartnumeral_normalizer.dartunsupported_locale_delegates.dartreferences/arb-and-icu.mdnumerals-and-calendars.mdrtl-and-bidi.mdunsupported-locales.mdscripts/check_arb_parity.shcheck_i18n_bans.sh詳細
i18n、RTL、ローカライズ
基本原則は一つです。正規の値を保存し、描画時にローカライズします。 すべての変換と書式設定は、一つの l10n レイヤーに集約します。機能ウィジェットには値オブジェクトとローカライズ済みの文字列を渡し、生の書式設定ロジックは渡しません。RTL は翻訳の課題ではなく、レイアウトの規律です。論理的な幾何表現を使って記述すれば、自然に反転します。複数のロケール、または RTL ロケール(アラビア語、ペルシア語、ヘブライ語、ソラニー・クルド語、ウルドゥー語)をサポートするすべてのアプリに適用されます。
作業内容に応じて、以下のリファレンスを参照してください。
references/arb-and-icu.md— ARB の追加・翻訳ワークフロー、l10n.yaml、ICU の複数形・選択、プレースホルダーの型指定。references/rtl-and-bidi.md— 方向に応じた幾何表現の許可・禁止表、アイコンの反転、双方向テキストの分離、LTR 固定の CustomPainter、RTL チャート。references/numerals-and-calendars.md— 数字体系、区切り文字の落とし穴、書式設定・解析、カレンダーへの投影。references/unsupported-locales.md—flutter_localizationsがデータを持たないロケールの提供方法:プローブ、ベンダー提供のデリゲート 3 点セット、デリゲートの順序、フォントの対応範囲。
PR を送る前に scripts/check_arb_parity.sh と scripts/check_i18n_bans.sh を実行してください。
必須ルール
- ユーザーに表示される文字列はすべて gen-l10n を通します。 まずテンプレート
app_en.arbにキーを追加し、その後AppLocalizations.of(context)で読み込みます。getter を非 null にするため、l10n.yamlでnullable-getter: falseを設定します。キーの不足や誤記が、ウィジェットを何も表示しないままにするのではなく、コンパイルエラーになるようにします。Text、tooltip:、label:、hint:、semanticsLabel:にリテラルをハードコードしないでください。runAppの前にmain()で呼び出すなど、Localizationsのスコープ外でAppLocalizations.ofを呼び出さないでください。 - すべてのロケールでキーとプレースホルダーを一致させます。 テンプレートにあるキーがロケールにないと、テンプレートの言語がそのまま気づかれずに配信されます。プレースホルダー名を変更すると、実行時にその翻訳が壊れます。すべてのキーと
{placeholder}の名前を一致させてください。check_arb_parity.shでチェックしています。 - 文と複数形は連結せず、ICU で組み立てます。 ARB では
{count, plural, ...}と{x, select, ...}を使います。語順や複数形のカテゴリは言語によって異なります(アラビア語には CLDR 形式が 6 つあります:zero/one/two/few/many/other)。連結すると文法的に不自然になり、RTL の並べ替えにも対応できません。 - 方向はロケールによって決まり、ハードコードしません。
MaterialAppにlocalizationsDelegatesとsupportedLocalesを設定し、アクティブなロケールは状態から制御します。ルートにDirectionality(TextDirection.rtl)を指定すると、物理方向に基づくバグが隠れ、LTR の領域が壊れます。RTL はGlobalWidgetsLocalizationsを介して、ウィジェットごとのコードなしでロケールに応じて設定されます。ただし、flutter_localizationsに実際に含まれているロケールに限ります(ルール 10)。それ以外のロケールでは、方向が気づかれないまま LTR にフォールバックします。想定に頼らず、ロケールごとにDirectionality.ofをアサートしてください。 - 最初のコミットから方向対応のジオメトリのみを使います。
EdgeInsetsDirectional、AlignmentDirectional、PositionedDirectional、TextAlign.start/end、MainAxisAlignment.start/end、BorderRadiusDirectional、Icons.adaptive.*を使います。EdgeInsets.only(left/right)、Alignment.centerLeft/Right、Positioned(left/right:)、TextAlign.left/right、Icons.arrow_backは使わないでください。grep のチェックで PR を却下します。 - 保存形式は UTC エポックと ASCII 数字を正規形とします。
dateTime.toUtc().millisecondsSinceEpochと西洋/ラテン数字を保存します。カレンダーと数字体系は表示上の変換に限ります。エポックまたはDateTimeに基づいてスケジュールし、ジャラーリー暦/ヒジュラ暦の計算には基づけないでください。 うるう年や月の長さに関する特性を、カレンダーに依存させないようにします。 - 解析または保存の前に、すべての数値入力を ASCII に正規化します。 数字と区切り記号の両方が対象です。
double.parse(normalizeToAscii(text))を使い、生の入力に対してint.parse(text)を使わないでください(ペルシャ語/アラビア語のソフトキーボードでは۱۲۳/١٢٣や1٫5が入力されます)。 - 強い LTR の技術情報(アカウント番号、注文番号、参照番号、電話番号、メールアドレス、URL、製品コード)は、ビュー層の専用 FSI/PDI ヘルパーだけで分離します。 分離文字(
U+2066–U+2069)をストレージ、検索、エクスポートに渡さないでください。境界で取り除きます。 - 実行時にフォントを取得せず、対象の文字体系をカバーするフォントをフォールバック付きでバンドルします。 テーマに
fontFamilyとfontFamilyFallbackを設定し、アラビア語/ペルシャ語の文字が豆腐表示にならないようにします。オフライン対応でテレメトリを行わないアプリではgoogle_fontsを避けてください。実行時にフォントを取得するためです。見本ではなく、フォント自体のcmapで対応状況を確認します。 文字体系への対応は、言語への対応を意味しません。ペルシャ語を表示できるフォントでも、ソラニー語で追加された 7 文字が欠けていることがあります。また、アラビア語の見本ではその文字が使われないため、問題を発見できません。 flutter_localizationsに含まれていないロケールには、ベンダー提供の 3 つのデリゲートが必要です。最初に配置してください。 gen-l10n がローカライズするのは アプリ独自 の文字列です。Flutter 独自の UI 表示と、暗黙的なTextDirectionは別のカタログから提供されます。近隣の文字体系を借用したMaterialLocalizations、CupertinoLocalizations、およびWidgetsLocalizationsをベンダー提供し、それぞれが該当する言語コードだけを受け持つようにします。AppLocalizations.localizationsDelegatesにすでに含まれているGlobal*デリゲートより前に配置してください。理由:Material デリゲートがないと最初のTooltipでアサートが発生します。また、Widgets のフォールバックはすべてのロケールを受け持ち、TextDirection.ltrをハードコードしています。そのため Material 側だけを修正すると、RTL アプリの表示が逆方向に読まれる一方で、クラッシュは発生しません。references/unsupported-locales.mdを参照してください。
ARB ワークフロー(キーの追加または翻訳)
- テンプレートの
app_en.arbに、キーとその@keyメタデータ(型付きのplaceholders、翻訳者向けのdescription)を追加します。 - 同じキーを、他のすべてのロケールファイルにも実際の翻訳とともに追加します。プレースホルダー名と ICU 構造は同一にします(分岐の本文は異なっても構いませんが、分岐の形は同じにします)。
scripts/check_arb_parity.shを実行します。キーの不足や余分なキー、プレースホルダーの不一致があると失敗します。- 再生成(
flutter gen-l10n)してflutter analyzeを実行します。キーが不足している場合、コンパイルエラーになります。
// app_en.arb (template) — declare the key + @metadata here first.
{
"@@locale": "en",
"itemsRemaining": "{count, plural, =0{No items left} =1{1 item left} other{{count} items left}}",
"@itemsRemaining": {
"description": "Count shown on the order summary card.",
"placeholders": { "count": { "type": "int" } }
}
}
// app_ar.arb — same key; Arabic's SIX CLDR categories are a translation contract.
{
"@@locale": "ar",
"itemsRemaining": "{count, plural, zero{لا عناصر متبقية} one{عنصر واحد متبقٍ} two{عنصران متبقيان} few{{count} عناصر متبقية} many{{count} عنصرًا متبقيًا} other{{count} عنصر متبقٍ}}"
}
方向性のあるジオメトリ — 構造によってミラーリング
// Reads correctly under LTR and RTL with zero conditionals.
Padding(
padding: const EdgeInsetsDirectional.only(start: 16, end: 8), // never left/right
child: Row(
children: [
Expanded(child: Text(task.title, textAlign: TextAlign.start)), // never .left
IconButton(
icon: Icon(Icons.adaptive.arrow_forward), // auto-mirrors in RTL
onPressed: onNext,
),
],
),
)
戻る/次へ、山形記号、進行状況のキャレットなど、方向を示すグリフは Icons.adaptive.* を使ってミラーリングします。時計、チェックマーク、メディアの再生/一時停止、ロゴなど、意味が固定されたグリフはミラーリングしません。許可/禁止の一覧と手動で反転する手順については、references/rtl-and-bidi.md を参照してください。
数字 — 表示時にフォーマットし、フォーマッターを統一する
各ロケール固有の NumberFormat を使用します。intl は -u-nu- 拡張ではなく、ロケールの記号データに基づいて現地の数字を出力します(フォールバック時に Unicode の -u 拡張が除去されます)。intl に数値記号がないロケール(例:ckb)は、気づかないうちにラテン数字にフォールバックするため、数字体系を共有する対応済みの基本ロケール(ckb→fa)に固定し、テストで出力された数字ブロックを検証します。同じフォーマッターを UI とキャンバスのペインターの両方に使用します。
// One formatter per locale. Persian and Arabic digits are DISTINCT Unicode blocks
// (۴۵۶ U+06Fx vs ٤٥٦ U+066x) — not interchangeable. Format with a locale intl ships
// symbols for; a `-u-nu-` extension is dropped, not honored.
NumberFormat numberFormatFor(Locale locale) => switch (locale.languageCode) {
'fa' => NumberFormat.decimalPattern('fa'), // ۰۱۲۳۴۵۶۷۸۹ (arabext)
// ckb has no intl number symbols — borrow fa (same arabext block + separators).
'ckb' => NumberFormat.decimalPattern('fa'),
'ar' => NumberFormat.decimalPattern('ar'), // ٠١٢٣٤٥٦٧٨٩ (arab)
_ => NumberFormat.decimalPattern('en'), // Western (en, de, fr)
};
解析の前に正規化します。数字の両方の範囲に加え、ペルシア語/アラビア語の区切り記号も正規化します。
String normalizeToAscii(String input) {
final sb = StringBuffer();
for (final r in input.runes) {
if (r >= 0x0660 && r <= 0x0669) { // Eastern-Arabic ٠-٩
sb.writeCharCode(0x30 + (r - 0x0660));
} else if (r >= 0x06F0 && r <= 0x06F9) { // Persian ۰-۹
sb.writeCharCode(0x30 + (r - 0x06F0));
} else if (r == 0x066B) { // ٫ decimal separator -> '.'
sb.write('.');
} else if (r == 0x066C) { // ٬ grouping separator -> drop
continue;
} else {
sb.writeCharCode(r);
}
}
return sb.toString();
}
// double.parse(normalizeToAscii(text)) — never int.parse on raw input.
1٫5 は 15 ではなく 1.5 を意味します。数字だけを正規化して区切り記号を正規化しないと、入力された金額が気づかないうちに破損します。数字、区切り記号、暦の一覧については、references/numerals-and-calendars.md を参照してください。
双方向テキストの分離 — 混在する文字体系のテキスト
const _lri = ''; // LEFT-TO-RIGHT ISOLATE
const _rli = ''; // RIGHT-TO-LEFT ISOLATE
const _fsi = ''; // FIRST STRONG ISOLATE (unknown direction)
const _pdi = ''; // POP DIRECTIONAL ISOLATE
String isolate(String run) => '$_fsi$run$_pdi'; // unknown direction
String isolateLtr(String run) => '$_lri$run$_pdi'; // known LTR (codes, numbers)
String isolateRtl(String run) => '$_rli$run$_pdi'; // known RTL
// Prefer known-direction over FSI: first-strong mis-guesses on leading punctuation.
// The isolated value is an ARB placeholder, never a hard-spliced substring:
Text(l10n.orderRef(isolateLtr(order.reference))); // "{ref} — order" reads correctly in RTL
分離はビュー層でのみ行います。単独のフィールドでは、代わりにウィジェットに方向を設定します。Text(code, textDirection: TextDirection.ltr, textAlign: TextAlign.end)。古い LRE/RLE/LRO/RLO 埋め込みは使用せず、分離制御文字(UAX #9)を使用します。
暦と日付
保存済みの UTC 時刻を、表示のためだけにユーザーが選択した暦へ変換します。暦固有のフィールドは保存せず、Jalali/グレゴリオ暦の変換を独自に実装しないでください(intl、shamsi_date、hijri を使用します)。週の開始日と週末はロケールによって異なるため、正規の日付で区分を計算し、ロケールに応じてラベルを付けます。examples/numeral_normalizer.dart と references/numerals-and-calendars.md を参照してください。
ロケールに依存しない座標空間を使う CustomPainter
ペインターが固定の座標グリッドを使用する場合、周囲の RTL によってジオメトリが反転されないよう、そのサブツリーのみを Directionality(textDirection: TextDirection.ltr, …) に固定します。周囲の UI は引き続き論理インセットによってミラーリングされます。ポインターは localPosition に対してペインター独自の変換を適用します。RTL の場合に dx を反転してはいけません。ペインターには、UI とキャンバス上の数字が一致するよう、同じ numberFormatFor(locale) を渡します。チャートの UI(軸、凡例、時間軸)はミラーリングしますが、プロットされたデータ自体は反転しません。詳しくは custom-canvas-and-gestures を参照してください。
アンチパターン
Text('Order details')— gen-l10n の経路がないハードコードされたリテラルです。l10n.orderDetailsを読み取ってください。 (ハードコードされた UI 文字列を正確に grep で検出するのは誤検知が起きやすいため、これはcheck_i18n_bans.shではなくレビューで検出します。このゲートではジオメトリ、アイコン、数値の連結、レガシーな双方向テキスト処理、フォントの取得を対象とします。)- 翻訳ファイルを先に作成する、またはキーが見つからない場合に null にフォールバックさせる — テンプレートは
enで、nullable-getter: falseによりキーが見つからないとコンパイルエラーになります。 count == 1 ? 'item' : 'items'— 2 通りの三項演算子ではアラビア語のfew/manyに対応できません。ICU のpluralを使用してください。Text('$count items')または'Total ' + n.toString()— 文字列の断片を連結し、生の ASCII 数字を使っています。RTL では語順と数字のブロックが誤ります。ICU メッセージ 1 つとnumberFormatForを使用してください。- ペルシャ語で
٤٥٦(アラビア・インド数字)、またはアラビア語で۴۵۶(拡張アラビア・インド数字)を表示する — これらは Unicode の異なるブロックです。fa/ckb にはfaフォーマッターを、ar にはarフォーマッターを使用してください。 EdgeInsets.only(left: 16)/Alignment.centerLeft/Positioned(left:)— 物理的な辺を使うとミラーリングが適用されません(grep で検出されます)。- アプリのルートを
Directionality(TextDirection.rtl)で囲み、「RTL を有効にする」 — RTL はロケールによって決まります。ルートにハードコードすると、物理的な辺に関する問題が隠れ、LTR の領域も壊れます。 [...AppLocalizations.localizationsDelegates, GlobalMaterialLocalizations.delegate, …]— このスプレッドには すでに 3 つのGlobal*デリゲートが含まれているため、再追加すると重複し、同梱したデリゲートより前に配置されます。Localizations._loadAllは、対象のロケールに対応する最初の型のデリゲートを使用するため、独自のデリゲートには到達しません。- 未対応ロケール用に
MaterialLocalizationsデリゲートだけを同梱する — クラッシュは解消されますが、代わりにアプリが逆向きに表示されます。WidgetsLocalizationsのデリゲートも同梱するか、どちらも同梱しないでください。 - Sorani、ウルドゥー語、パシュトー語のビルドで、フォントファウンドリのページにある「アラビア語をサポート」という説明を信頼する — 言語固有の文字が同梱フォントの
cmapに含まれていることを確認してください。 - 分離制御文字(
U+2066–U+2069)をストレージ/検索/エクスポートに渡す — 境界で除去してください。 Ahemを使って RTL/数字のゴールデンテストを描画する — ペルシャ語の数字やアラビア文字の字形形成が一切テストされません。実際に同梱されているフォントを読み込んでください(widget-golden-and-a11y-testingを参照)。- 生の数値フィールドで
int.parse(userText)を使う — 先に ASCII に正規化してください。
完了の定義
- テンプレート
app_en.arbに@descriptionと型付きプレースホルダーを含むキーがあること。他のすべてのロケールにも同じキーがあり、check_arb_parity.shが成功すること。 -
nullable-getter: falseであること。文字列はAppLocalizations経由でのみ読み取り、Text/tooltip/label/hint/semanticsLabelにリテラルがないこと。 - 数を含む文字列は ICU の
pluralを使用すること(アラビア語が提供される場合は、アラビア語の6つすべてのカテゴリを使用)。三項演算子や$countの連結を使用しないこと。 - 数字と日付は
numberFormatFor(locale)/intlのDateFormatで取得し、プレースホルダーとして注入すること。ASCII 形式で手動整形したり、誤った数字ブロックを使用したりしないこと。描画処理でも同じフォーマッターを共有すること。 - すべての数値入力で、解析/保存の前に
normalizeToAsciiを通すこと。往復変換をテストすること。 - すべてのジオメトリで方向性を考慮すること(
start/end、Icons.adaptive)。grep で物理方向を指定する API が残っていないこと。方向はハードコードされたルートのDirectionalityではなく、ロケールから取得すること。 - 方向が混在するすべての文字列範囲で、1つの FSI/PDI ヘルパーを使用すること(方向が既知の場合は FSI より優先)。生の文字列連結や旧式の
LRE/RLEを使用しないこと。分離制御文字がストレージに到達しないこと。 - 日付/数値は正規形式(UTC エポック+ASCII)で保存し、カレンダー/数字は描画時にのみ変換すること。リマインダーはエポックに基づいてスケジュールすること。
- 提供するすべてのスクリプトをフォントがフォールバックでカバーすること。実行時にフォントを取得しないこと。RTL と数字のゴールデンテストを実際のフォントで実行すること。カバレッジはバンドルされた各フォントの
cmapから検証すること。スクリプトだけでなく、その言語固有の文字を検証すること。 - 提供する各ロケールが
flutter_localizationsに含まれているか、またはGlobal*のものより前に3つすべてのデリゲートがベンダー提供されていること。各ロケールについてDirectionality.ofとMaterialLocalizationsの文字列を検証し、プラットフォームのマニフェスト(CFBundleLocalizations)にすべてのタグを記載すること。
関連スキル
accessibility-as-codeを参照してください。Semanticsのロール/ラベル、およびMediaQueryから読み取るアクセシビリティ状態について説明しています。widget-golden-and-a11y-testingを参照してください。RTL/数字のゴールデンテスト(Ahem ではなく実際のフォントを使用)と、textScaler のオーバーフローマトリクスについて説明しています。custom-canvas-and-gesturesを参照してください。LTR に固定された描画処理、共有変換、ポインターマッピングについて説明しています。value-objects-money-and-unitsを参照してください。ここで描画する正規形式の整数最小単位による保存について説明しています。local-notifications-schedulerを参照してください。カレンダー計算ではなく、正規形式の時点に基づいてスケジュールする方法について説明しています。widget-compositionを参照してください。これらの文字列を配置する方向対応のレイアウトプリミティブについて説明しています。scaffold-feature-moduleを参照してください。新しい機能の文字列をこの ARB ワークフローに接続する方法について説明しています。design-system-structure(references/typography-and-fonts.md)を参照してください。スクリプトごとのフォールバックカスケードの宣言場所と、バンドルされたグリフのカバレッジを証明する方法について説明しています。app-startup-and-bootstrapを参照してください。最初のフレームより前に永続化されたロケールを復元する方法について説明しています。OS で選択できないロケールにアクセスできるようにする唯一の方法です。
参考資料
- Flutter の国際化:https://docs.flutter.dev/ui/accessibility-and-internationalization/internationalization
- gen-l10n /
l10n.yamlのオプション:https://docs.flutter.dev/ui/accessibility-and-internationalization/internationalization#configuring-the-l10nyaml-file - ARB 形式:https://github.com/google/app-resource-bundle/wiki/ApplicationResourceBundleSpecification
- ICU の
MessageFormat(複数形/選択):https://unicode-org.github.io/icu/userguide/format_parse/messages/ - CLDR の複数形ルール:https://cldr.unicode.org/index/cldr-spec/plural-rules
intlパッケージ(NumberFormat、DateFormat、Bidi):https://pub.dev/packages/intl- Unicode 双方向アルゴリズム(UAX #9)、アイソレート制御文字:https://www.unicode.org/reports/tr9/
Directionality、EdgeInsetsDirectional:https://api.flutter.dev/flutter/widgets/Directionality-class.htmlLocalizationsDelegate(ベンダー提供のもの):https://api.flutter.dev/flutter/widgets/LocalizationsDelegate-class.html- サポートされているロケールの一覧と、未サポートのロケールの追加:https://docs.flutter.dev/ui/accessibility-and-internationalization/internationalization#advanced-locale-definition
- OpenType の
cmapテーブル(グリフのカバレッジ):https://learn.microsoft.com/en-us/typography/opentype/spec/cmap