公開日 2026-07-27

Flutterアプリを日本語化する(l10n + arb 最小構成)

Flutter標準の l10n と arb を使い、日本語 UI、引数付き文言、複数形、バリデーションメッセージの最小構成を確認できるようにする。

目次

  1. 1. ゴールと非対象
  2. 対象読者
  3. この記事で到達する状態
  4. 非対象
  5. 2. 先に l10n の流れを掴む
  6. 3. 動かす環境を整える
  7. 3-1. Flutter プロジェクトを作成する
  8. 3-2. エミュレーターを起動する
  9. 3-3. pubspec.yaml を更新する
  10. コードのポイント
  11. 3-4. l10n.yaml を作成する
  12. 4. arb を作成する
  13. 4-1. lib/l10n/app_ja.arb を作成する
  14. コードのポイント
  15. 4-2. lib/l10n/app_en.arb を作成する
  16. コードのポイント
  17. 4-3. 生成コードを作成する
  18. 5. lib/main.dart に最小サンプルを作る
  19. コードのポイント
  20. 7. 起動して確認する
  21. AppLocalizations の import でエラーが出るとき
  22. 8. 最初に localize する対象を絞る
  23. 9. まとめ

Flutterで端末設定と利用者設定を保存する(SharedPreferencesとsecure storageの使い分け) の次に揃えておきたいのが、文言の置き場です。日本語業務アプリでは、画面タイトル、入力ラベル、件数表示、バリデーションメッセージを最初から日本語で出したくなります。そこで Text('固定文言') を増やし始めると、あとでフォームやログイン画面へ広げるときに置換が増えます。この記事では Flutter 標準の l10narb を使い、日本語化の最小構成を pubspec.yamll10n.yamllib/l10n/*.arblib/main.dart の 4 点で作ります。

1. ゴールと非対象

対象読者

  • Flutter の環境構築、基本 UI、REST API 通信、設定保存の入口までは進んでいる人
  • 日本語 UI を入れたいが、l10n / arb / 生成コードの役割がまだ曖昧な人
  • フォームやログイン画面へ進む前に、文言の管理方法を先に揃えたい人

この記事で到達する状態

  • flutter_localizationsintlgenerate: truel10n.yaml の役割を説明できる
  • lib/l10n/app_ja.arbapp_en.arb を作成できる
  • AppLocalizationsMaterialApp と各 Widget から参照できる
  • 引数付き文言、複数形、バリデーションメッセージの最小例を動かせる
  • 日本語切り替えをどこで行い、どの画面要素から localize すべきかを説明できる

非対象

  • intl パッケージを直接書く設計
  • l10n.yaml の詳細カスタマイズや多言語ファイル分割
  • easy_localization など外部ライブラリとの比較
  • 日付、数値、通貨フォーマットの細かいロケール差
  • Riverpod や go_router と組み合わせた本格的なアプリ全体設計

今回は「最初の 1 本」を動かすところに絞ります。日本語だけに閉じず、英語も 1 つ入れて切り替え確認まで含めるのは、生成コードと supportedLocales の動きを見やすくするためです。

2. 先に l10n の流れを掴む

今回の流れは次の通りです。

flowchart LR
  A["pubspec.yaml で l10n を有効化"] --> B["l10n.yaml と app_ja.arb / app_en.arb を用意"]
  B --> C["flutter gen-l10n"]
  C --> D["lib/l10n/app_localizations.dart を生成"]
  D --> E["MaterialApp に delegate と supportedLocales を設定"]
  E --> F["Widget から AppLocalizations.of(context) を参照"]
  F --> G["タイトル / ラベル / 件数 / バリデーション文言を表示"]

役割は 4 つに分けると追いやすくなります。

  • pubspec.yaml: l10n 関連の依存関係と生成スイッチ
  • l10n.yaml: どの ARB を基準にし、どこへ生成コードを出すかを決める設定
  • arb: 翻訳データの正本
  • MaterialApp 設定: どのロケールを使えるか、いま何語で描画するかを決める場所

ここで混ざりやすいのが supportedLocaleslocale の違いです。

  • supportedLocales: このアプリが対応している言語一覧
  • locale: いま強制的に使う言語。null なら端末設定に従う

先にこの役割を切り分けておくと、あとで「日本語文言はあるのに切り替わらない」状態を見たとき、確認箇所を絞りやすくなります。

3. 動かす環境を整える

3-1. Flutter プロジェクトを作成する

環境構築がまだの場合は Windows 11で始めるFlutter開発環境 を先に参照してください。

次のコマンドでプロジェクトを作成します。

flutter create my_l10n_app
cd my_l10n_app

3-2. エミュレーターを起動する

利用可能なエミュレーター一覧を確認します。

flutter emulators

表示された ID を指定して起動します。

flutter emulators --launch <emulator_id>

3-3. pubspec.yaml を更新する

アプリコードから intl を直接呼ぶ必要はありませんが、現行 Flutter の gen-l10n では依存関係として追加しておく必要があります。そのため、DartPad ではなく Flutter プロジェクト上で確認します。

ここで追加するのは 3 箇所です。flutter create 直後の pubspec.yaml を丸ごと書き換える必要はありません。

  • dependencies:flutter: 直下に flutter_localizations を追加する
  • dependencies:intl: any を追加する
  • flutter: セクションの uses-material-design: true と同じ階層に generate: true を追加する

手元で生成されたコメント行、descriptionenvironment.sdkflutter_lints の値はそのままで構いません。更新後に確認したい箇所は次のとおりです。

dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter
  intl: any
  cupertino_icons: ^1.0.8

flutter:
  uses-material-design: true
  generate: true

コードのポイント

flutter_localizationsintl を dependencies に入れる

dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter
  intl: any
  cupertino_icons: ^1.0.8

flutter_localizations は、日付ピッカーや一部の Material 文言まで切り替えるために必要です。intl は生成された l10n コードが内部で利用する依存関係で、plural やプレースホルダーを使う最小構成でも揃えておくほうが確実です。

generate: true が生成コードのスイッチになる

flutter:
  uses-material-design: true
  generate: true

generate: true を入れると、l10n.yamllib/l10n/*.arb をもとに lib/l10n/app_localizations.dart が生成されます。ARB を保存しても import 先が見つからないときは、まずこのスイッチを確認するのが最短です。

3-4. l10n.yaml を作成する

プロジェクトルートに l10n.yaml を作成します。

このファイルは、gen-l10n がどの ARB を入力として読み、どこへ生成コードを書き出すかを決める設定です。今回は lib/l10n/app_localizations.dart を生成する最小構成にします。

テンプレート ARB には英語版を使うのが Flutter 標準の流れなので、ここでは app_en.arb を指定します。

arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart

この時点では app_en.arb がまだなくても構いません。次の 4-2 で作成します。

pubspec.yamll10n.yaml を保存したら、次のコマンドで依存関係を解決します。

flutter pub get

4. arb を作成する

4-1. lib/l10n/app_ja.arb を作成する

lib/l10n/app_ja.arb は次の内容で作成します。

このファイルは、日本語で表示する文言とプレースホルダー定義をまとめる ARB です。注目点は、固定文字列だけでなく、引数付き文言と plural も最初から同じ形式で持っていることです。

{
  "@@locale": "ja",
  "appTitle": "倉庫アプリサンプル",
  "screenTitle": "出荷状況の確認",
  "description": "タイトル、件数表示、保存メッセージをローカライズします。",
  "languageModeLabel": "表示言語",
  "systemLocaleOption": "端末設定に従う",
  "japaneseOption": "日本語",
  "englishOption": "英語",
  "currentLocaleLabel": "現在のロケール: {locale}",
  "@currentLocaleLabel": {
    "placeholders": {
      "locale": {
        "type": "String"
      }
    }
  },
  "nameLabel": "作業者名",
  "nameHint": "例: 佐藤",
  "requiredName": "作業者名を入力してください。",
  "welcomeMessage": "{name} さん、ようこそ。",
  "@welcomeMessage": {
    "placeholders": {
      "name": {
        "type": "String"
      }
    }
  },
  "pendingCount": "{count, plural, =0{未処理の出荷はありません。} =1{未処理の出荷が1件あります。} other{未処理の出荷が{count}件あります。}}",
  "@pendingCount": {
    "placeholders": {
      "count": {
        "type": "int"
      }
    }
  },
  "saveButton": "下書きを保存",
  "savedMessage": "{language} で保存しました。",
  "@savedMessage": {
    "placeholders": {
      "language": {
        "type": "String"
      }
    }
  },
  "countSelectorLabel": "未処理件数"
}

コードのポイント

① プレースホルダー付き文言は本体とメタ情報をセットで持つ

"welcomeMessage": "{name} さん、ようこそ。",
"@welcomeMessage": {
  "placeholders": {
    "name": {
      "type": "String"
    }
  }
}

welcomeMessage の本文だけでなく、@welcomeMessage 側に引数型まで書いておくことで生成コードから安全に呼べます。ログインユーザー名や受注番号のように、後から動的値を差し込みたい場面でも同じ形式を使えます。

② 件数文言は plural で定義しておく

"pendingCount": "{count, plural, =0{未処理の出荷はありません。} =1{未処理の出荷が1件あります。} other{未処理の出荷が{count}件あります。}}",
"@pendingCount": {
  "placeholders": {
    "count": {
      "type": "int"
    }
  }
}

plural は、件数に応じて文言を出し分ける仕組みです。この例では =0 が 0 件、=1 が 1 件、other がそれ以外を表します。

日本語だけを見ると文字列連結でも書けそうですが、ここで plural へ寄せておくと英語版を追加したときに構造を変えずに済みます。件数表示を最初からローカライズ対象として扱うための土台です。

4-2. lib/l10n/app_en.arb を作成する

lib/l10n/app_en.arb は次の内容で作成します。

このファイルは、日本語版と同じキー構成で英語文言を定義する ARB です。注目点は、キー名とプレースホルダー構造を揃えたまま、英語側だけ plural ルールや語順を自然な形へ差し替えていることです。

{
  "@@locale": "en",
  "appTitle": "Warehouse Sample",
  "screenTitle": "Shipment status",
  "description": "Localize the title, count summary, and save message.",
  "languageModeLabel": "Language",
  "systemLocaleOption": "Use system setting",
  "japaneseOption": "Japanese",
  "englishOption": "English",
  "currentLocaleLabel": "Current locale: {locale}",
  "@currentLocaleLabel": {
    "placeholders": {
      "locale": {
        "type": "String"
      }
    }
  },
  "nameLabel": "Operator name",
  "nameHint": "Example: Sato",
  "requiredName": "Please enter an operator name.",
  "welcomeMessage": "Hello, {name}.",
  "@welcomeMessage": {
    "placeholders": {
      "name": {
        "type": "String"
      }
    }
  },
  "pendingCount": "{count, plural, =0{No shipments are waiting.} =1{1 shipment is waiting.} other{{count} shipments are waiting.}}",
  "@pendingCount": {
    "placeholders": {
      "count": {
        "type": "int"
      }
    }
  },
  "saveButton": "Save draft",
  "savedMessage": "Saved in {language}.",
  "@savedMessage": {
    "placeholders": {
      "language": {
        "type": "String"
      }
    }
  },
  "countSelectorLabel": "Pending count"
}

コードのポイント

① 日本語版と同じキー構成を保っている

{
  "@@locale": "en",
  "appTitle": "Warehouse Sample",
  "screenTitle": "Shipment status",
  "description": "Localize the title, count summary, and save message.",
  "languageModeLabel": "Language",

キー名を日本語版と揃えているため、Widget 側は AppLocalizations の呼び出し方を変えずに済みます。言語ごとの差は ARB に閉じ込め、Dart 側は同じ API で使える形にしているのがポイントです。

② plural と引数文言は言語ごとの自然な語順へ任せる

"pendingCount": "{count, plural, =0{No shipments are waiting.} =1{1 shipment is waiting.} other{{count} shipments are waiting.}}",
"savedMessage": "Saved in {language}.",

英語では 1 shipment3 shipments のように件数で形が変わるため、plural を通す価値が日本語以上に大きくなります。固定文言、引数付き文言、plural の 3 種類を先に揃えておくと、あとから String の手書き連結へ戻りにくくなります。

4-3. 生成コードを作成する

ARB を 2 つ保存したら、次のコマンドで生成コードを作成します。

flutter gen-l10n

実行後に lib/l10n/app_localizations.dart が生成されていれば準備完了です。Target of URI doesn't exist が出る場合は、まずこのコマンドが通っているかと、lib/main.dart 側が package:flutter_gen/... ではなく l10n/app_localizations.dart を import しているかを確認してください。

5. lib/main.dart に最小サンプルを作る

flutter gen-l10n を実行し、lib/l10n/app_localizations.dart が生成されたら、lib/main.dart は次の内容で作成します。

このファイルは、生成された AppLocalizations を画面で実際に使う最小サンプルです。注目点は、MaterialApp 側で利用可能な言語を宣言し、画面側では引数付き文言、plural、バリデーションまで同じ l10n API から読んでいることです。

import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'l10n/app_localizations.dart';

void main() {
  runApp(const MyApp());
}

enum LanguageMode {
  system,
  japanese,
  english,
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  LanguageMode _languageMode = LanguageMode.system;

  Locale? get _locale {
    switch (_languageMode) {
      case LanguageMode.system:
        return null;
      case LanguageMode.japanese:
        return const Locale('ja');
      case LanguageMode.english:
        return const Locale('en');
    }
  }

  void _handleLanguageChanged(LanguageMode? mode) {
    if (mode == null) {
      return;
    }

    setState(() {
      _languageMode = mode;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      onGenerateTitle: (BuildContext context) {
        return AppLocalizations.of(context)!.appTitle;
      },
      locale: _locale,
      supportedLocales: AppLocalizations.supportedLocales,
      localizationsDelegates: const <LocalizationsDelegate<dynamic>>[
        AppLocalizations.delegate,
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
      ],
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
      ),
      home: LocalizationDemoPage(
        languageMode: _languageMode,
        onLanguageChanged: _handleLanguageChanged,
      ),
    );
  }
}

class LocalizationDemoPage extends StatefulWidget {
  const LocalizationDemoPage({
    super.key,
    required this.languageMode,
    required this.onLanguageChanged,
  });

  final LanguageMode languageMode;
  final ValueChanged<LanguageMode?> onLanguageChanged;

  @override
  State<LocalizationDemoPage> createState() => _LocalizationDemoPageState();
}

class _LocalizationDemoPageState extends State<LocalizationDemoPage> {
  final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
  final TextEditingController _nameController =
      TextEditingController(text: '佐藤');

  int _pendingCount = 3;
  String _lastSavedMessage = '-';

  @override
  void dispose() {
    _nameController.dispose();
    super.dispose();
  }

  void _save(AppLocalizations l10n) {
    final FormState? form = _formKey.currentState;
    if (form == null || !form.validate()) {
      return;
    }

    final Locale activeLocale = Localizations.localeOf(context);
    final String languageLabel = _languageLabel(l10n, activeLocale);
    final String message = l10n.savedMessage(languageLabel);

    setState(() {
      _lastSavedMessage = message;
    });

    ScaffoldMessenger.of(context)
      ..hideCurrentSnackBar()
      ..showSnackBar(SnackBar(content: Text(message)));
  }

  String _languageLabel(AppLocalizations l10n, Locale locale) {
    switch (locale.languageCode) {
      case 'ja':
        return l10n.japaneseOption;
      case 'en':
        return l10n.englishOption;
      default:
        return locale.languageCode;
    }
  }

  @override
  Widget build(BuildContext context) {
    final AppLocalizations l10n = AppLocalizations.of(context)!;
    final Locale activeLocale = Localizations.localeOf(context);

    return Scaffold(
      appBar: AppBar(
        title: Text(l10n.screenTitle),
      ),
      body: SafeArea(
        child: ListView(
          padding: const EdgeInsets.all(16),
          children: <Widget>[
            _SectionCard(
              title: l10n.languageModeLabel,
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  SegmentedButton<LanguageMode>(
                    segments: <ButtonSegment<LanguageMode>>[
                      ButtonSegment<LanguageMode>(
                        value: LanguageMode.system,
                        label: Text(l10n.systemLocaleOption),
                      ),
                      ButtonSegment<LanguageMode>(
                        value: LanguageMode.japanese,
                        label: Text(l10n.japaneseOption),
                      ),
                      ButtonSegment<LanguageMode>(
                        value: LanguageMode.english,
                        label: Text(l10n.englishOption),
                      ),
                    ],
                    selected: <LanguageMode>{widget.languageMode},
                    onSelectionChanged: (Set<LanguageMode> selection) {
                      widget.onLanguageChanged(selection.firstOrNull);
                    },
                  ),
                  const SizedBox(height: 12),
                  Text(l10n.currentLocaleLabel(activeLocale.languageCode)),
                ],
              ),
            ),
            const SizedBox(height: 16),
            _SectionCard(
              title: l10n.screenTitle,
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  Text(l10n.description),
                  const SizedBox(height: 12),
                  Form(
                    key: _formKey,
                    child: TextFormField(
                      controller: _nameController,
                      onChanged: (String _) {
                        setState(() {});
                      },
                      decoration: InputDecoration(
                        labelText: l10n.nameLabel,
                        hintText: l10n.nameHint,
                        border: const OutlineInputBorder(),
                      ),
                      validator: (String? value) {
                        if (value == null || value.trim().isEmpty) {
                          return l10n.requiredName;
                        }

                        return null;
                      },
                    ),
                  ),
                  const SizedBox(height: 12),
                  Text(l10n.welcomeMessage(_nameController.text.trim())),
                ],
              ),
            ),
            const SizedBox(height: 16),
            _SectionCard(
              title: l10n.countSelectorLabel,
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  DropdownButton<int>(
                    value: _pendingCount,
                    items: const <DropdownMenuItem<int>>[
                      DropdownMenuItem<int>(
                        value: 0,
                        child: Text('0'),
                      ),
                      DropdownMenuItem<int>(
                        value: 1,
                        child: Text('1'),
                      ),
                      DropdownMenuItem<int>(
                        value: 3,
                        child: Text('3'),
                      ),
                      DropdownMenuItem<int>(
                        value: 10,
                        child: Text('10'),
                      ),
                    ],
                    onChanged: (int? value) {
                      if (value == null) {
                        return;
                      }

                      setState(() {
                        _pendingCount = value;
                      });
                    },
                  ),
                  const SizedBox(height: 8),
                  Text(l10n.pendingCount(_pendingCount)),
                ],
              ),
            ),
            const SizedBox(height: 16),
            FilledButton(
              onPressed: () => _save(l10n),
              child: Text(l10n.saveButton),
            ),
            const SizedBox(height: 12),
            Text(_lastSavedMessage),
          ],
        ),
      ),
    );
  }
}

class _SectionCard extends StatelessWidget {
  const _SectionCard({
    required this.title,
    required this.child,
  });

  final String title;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Text(
              title,
              style: Theme.of(context).textTheme.titleMedium,
            ),
            const SizedBox(height: 12),
            child,
          ],
        ),
      ),
    );
  }
}

extension<T> on Set<T> {
  T? get firstOrNull {
    if (isEmpty) {
      return null;
    }

    return first;
  }
}

コードのポイント

MaterialApp 側で対応言語と delegate を宣言する

    return MaterialApp(
      onGenerateTitle: (BuildContext context) {
        return AppLocalizations.of(context)!.appTitle;
      },
      locale: _locale,
      supportedLocales: AppLocalizations.supportedLocales,
      localizationsDelegates: const <LocalizationsDelegate<dynamic>>[
        AppLocalizations.delegate,
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
      ],

MaterialApp 側の責務は、どの言語を使えるかと、どの delegate を有効にするかを宣言することです。ここが抜けると、自分で書いた文字列だけでなく Material Components 側の文言も期待どおりに切り替わりません。

② 保存メッセージは引数付き文言から組み立てる

  void _save(AppLocalizations l10n) {
    final FormState? form = _formKey.currentState;
    if (form == null || !form.validate()) {
      return;
    }

    final Locale activeLocale = Localizations.localeOf(context);
    final String languageLabel = _languageLabel(l10n, activeLocale);
    final String message = l10n.savedMessage(languageLabel);

    setState(() {
      _lastSavedMessage = message;
    });

保存完了メッセージを l10n.savedMessage(languageLabel) から組み立てているため、文言の語順を言語ごとに ARB 側へ任せられます。固定文言だけでなく引数付き文言も同じ API へ寄せると、後続の記事でログインユーザー名や受注番号を差し込む場面へそのまま広げられます。

③ バリデーションも同じ l10n API から返す

                  Form(
                    key: _formKey,
                    child: TextFormField(
                      controller: _nameController,
                      onChanged: (String _) {
                        setState(() {});
                      },
                      decoration: InputDecoration(
                        labelText: l10n.nameLabel,
                        hintText: l10n.nameHint,
                        border: const OutlineInputBorder(),
                      ),
                      validator: (String? value) {
                        if (value == null || value.trim().isEmpty) {
                          return l10n.requiredName;
                        }

入力ラベル、ヒント、未入力エラーまで AppLocalizations から返すため、表示言語の切り替え範囲がフォーム全体で揃います。件数表示の plural と同じく、画面側は l10n API を呼ぶだけにしておくと、文言の置き場を作り直さずに済みます。

7. 起動して確認する

ここまでできたら、次のコマンドで起動します。

flutter run

起動後は次の順で確認すると追いやすくなります。

  1. System のまま起動し、端末設定に応じた言語になるか確認する
  2. 日本語 に切り替え、タイトル、入力ラベル、保存ボタンが日本語になるか確認する
  3. 英語 に切り替え、plural と Snackbar が英語に変わるか確認する
  4. 名前を空にして保存し、バリデーションメッセージだけも切り替わるか確認する

AppLocalizations の import でエラーが出るとき

生成コードがまだ無い状態で flutter analyze を実行すると、次のように出ます。

  error - Target of URI doesn't exist: 'l10n/app_localizations.dart' - lib\main.dart:3:8 - uri_does_not_exist
  error - Undefined name 'AppLocalizations' - lib\main.dart:13:31 - undefined_identifier
  error - Undefined name 'AppLocalizations' - lib\main.dart:14:25 - undefined_identifier

**Target of URI doesn't exist が本体で、Undefined name 'AppLocalizations' はその巻き添えです。**import が解決できないので、そこから来る名前が全部未定義になります。下の Undefined name を1件ずつ追いかけないでください。

見直す順序は次のとおりです。

  1. pubspec.yamlgenerate: trueflutter: セクション側。dependencies: ではない)
  2. l10n.yaml がプロジェクトルートにあるか
  3. lib/l10n/*.arb を保存したか
  4. flutter pub get を実行したか

generate: true が入っていれば、flutter pub get の時点で lib/l10n/app_localizations.dart が生成されます。flutter gen-l10n を明示的に叩かなくても構いません。逆に言うと、generate: true を書き忘れていると、ARB をいくら保存してもファイルは生成されません。最初に疑うのはここです。

また、現行 Flutter では package:flutter_gen/... ではなく l10n/app_localizations.dart を import します。古い記事のとおりに package:flutter_gen/gen_l10n/app_localizations.dart と書くと、同じ Target of URI doesn't exist になります。

以下は、英語を既定にした Android Emulator での確認例です。切り替え直後の状態を載せているので、Snackbar とバリデーションは保存操作と未入力時の確認で補ってください。

System を選び、端末設定に従って英語表示になった状態です。

System を選び、英語表示になっている画面

日本語 を選び、タイトル、説明文、件数表示が日本語へ切り替わった状態です。

日本語を選び、日本語表示になっている画面

英語 を選び、アプリ側の言語設定で英語へ切り替えた状態です。

英語を選び、英語表示になっている画面

8. 最初に localize する対象を絞る

最初の段階では、画面内のすべての文字列を一度に移す必要はありません。優先度を付けるなら次の順が目安になります。

  • AppBar タイトルや画面見出し
  • ボタン文言
  • 入力ラベルとバリデーションメッセージ
  • 件数や状態の要約文

この順に寄せておくと、次に Flutterでgo_routerの認証ガードを実装する(redirect最小構成) のログイン画面や、FlutterからREST APIを呼ぶ最小構成(JSON通信 + エラー処理) のエラー表示へ広げるとき、文言の置き場を作り直さずに済みます。

9. まとめ

Flutter の日本語化は、arb に文言を置き、AppLocalizations を経由して Widget から呼ぶ流れを最初に固めると進めやすくなります。supportedLocales は対応言語の一覧、locale は現在使う言語、validator や plural も含めて AppLocalizations へ寄せる。この形を先に作っておけば、設定画面、ログイン画面、フォーム、CRUD へ進んでも文言の置き場がぶれません。

次に見返しやすい既存記事は次の 3 本です。

シリーズ 25/38

このシリーズ

Flutter導入と基礎

  1. 1. Windows 11で始めるFlutter開発環境:Android Emulatorで動かすまで
  2. 2. Flutter + FVM で開発環境のバージョンを固定する
  3. 3. Flutterで画像・SVG・アイコンを管理する(flutter_gen最小構成)
  4. 4. Flutterで最初に詰まりやすいDartの書き方:final・const・null safety・async/await を最初に整理する
  5. 5. DartのStream入門(非同期データの流れをつかむ)
  6. 6. FlutterのWidgetライフサイクル入門(initState / dispose で詰まらないために)
  7. 7. FlutterでBuildContextとKeyを理解する
  8. 8. Flutterのレイアウト入門(Column / Row / Stack の使い分け)
  9. 9. Flutterのテーマ設計入門(ThemeData + Theme Extension)
  10. 10. FlutterでMediaQueryとLayoutBuilderを使って画面サイズに対応する(スマホ・タブレット両対応)
  11. 11. FlutterのContainerとSizedBoxを使いこなす(余白・サイズ・装飾の基本)
  12. 12. FlutterのListViewとGridViewで一覧画面を作る(基本パターン)
  13. 13. Flutterのダイアログ・スナックバー・ボトムシートを使う(確認・通知UIの基本)
  14. 14. FlutterのTabBarとBottomNavigationBarで複数画面を切り替える
  15. 15. Flutterでカスタムウィジェットを作る入門(StatelessWidget の分割と再利用)
  16. 16. Flutterのルーティング入門(Navigator と go_router の使い分け)
  17. 17. FlutterからREST APIを呼ぶ最小構成(JSON通信 + エラー処理)
  18. 18. Flutterでjson_serializable + build_runnerを使ってJSONモデルを型安全に扱う
  19. 19. Flutterの状態管理入門(Riverpod最小構成)
  20. 20. Flutterでローディング・空状態・エラー表示を整える
  21. 21. Dart 3のsealed classとパターンマッチングで分岐を安全に書く
  22. 22. Flutterでgo_routerの認証ガードを実装する(redirect最小構成)
  23. 23. Flutterで端末設定と利用者設定を保存する(SharedPreferencesとsecure storageの使い分け)
  24. 24. Flutterでログイン状態を保持する(JWT + secure storage 最小構成)
  25. 25. Flutterアプリを日本語化する(l10n + arb 最小構成) 現在の記事
  26. 26. Flutterで業務用バーコード読み取りアプリを作る(最小構成)
  27. 27. Flutterでスキャン入力を受けて処理する
  28. 28. FlutterでGS1-128バーコードを解析する
  29. 29. Flutterで単一画面の入力フローを作る
  30. 30. Flutterで複数画像の添付UIを作る
  31. 31. Flutterでデータをファイルに書き出す
  32. 32. permission_handler でAndroid権限を実践的に扱う(カメラ・ストレージ・Bluetooth)
  33. 33. Flutterアプリのネイティブ設定を整える(アプリ名 / アイコン / スプラッシュ / 署名)
  34. 34. Flutterの環境切替と配布前チェック(flavor / release build / 権限確認)
  35. 35. FlutterのWidgetテスト入門(画面ロジックを壊さない最小構成)
  36. 36. FlutterのIntegration Test入門(ログインから一覧表示まで確認する)
  37. 37. Flutterアプリを社内配布する(Android APK サイドロード + MDM 概要)
  38. 38. Sentryでクラッシュとエラーを検知する(Flutter最小構成)