Flutter 多言語化対応についてまとめてみた
多言語化対応とは?
国際化の仕組みを構築する i18n (internationalization) と、各言語へ落とし込む l10n (localization) を行うことを多言語化対応と呼びます。
本記事では、Flutter公式ドキュメントに記載の方法をベースにした多言語化対応の構築手順や躓きポイント、Tipsについてまとめていきます。
参考:
- Internationalizing Flutter apps
- Localized messages are generated into source, not a synthetic package.
構築手順
1) pubspec.yaml:依存関係と generate を有効化
flutter_localizations と intl を追加し、flutter: generate: true を有効にします。
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
intl: any
flutter:
generate: true
2) l10n.yaml:生成設定を置く
プロジェクト直下に l10n.yaml を配置します。
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
3) arb ファイルを作る
arb-dir に合わせて lib/l10n/ を作り、言語ごとに app_XX.arb(英語ならapp_en.arb) を用意します。
{
"@@locale": "en",
"hoge": "hoge",
"@hoge": {
"type": "text"
}
}
ICU MessageFormat(select / plural)
バリデーション文言など「条件で分岐したい文字列」は ICU 記法を使えます。
参考: Placeholders, plurals, and selects
{
"helloHoge": "hello, {hoge}!",
"@helloHoge": {
"type": "text",
"placeholders": {
"hoge": {}
}
},
"hogeValidator": "{validateResult,select, valid{}empty{入力してください}other{}}",
"@hogeValidator": {
"type": "text",
"placeholders": {
"validateResult": {}
}
}
}
4) 生成する
flutter pub get または flutter runを実行すると AppLocalizations 一式が生成されます。
flutter gen-l10n の実行でも生成可能です。
5) MaterialApp に localizationsDelegates / supportedLocales を設定
生成された AppLocalizations を MaterialApp に設定します。
return MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
);
文字列の使い方(Widget 側)
Widget では AppLocalizations.of(context)! で参照します。
final l10n = AppLocalizations.of(context)!;
Text(l10n.hoge);
Text(l10n.helloHoge('Taro'));
バリデーション実装
バリデーション文言は以下のように実装すると綺麗に書けるのでオススメです。
enum ValidateResult {
valid,
empty,
}
class FormValidator {
FormValidator();
bool isEmpty(String? value) {
return value == null || value.trim().isEmpty;
}
}
class HogeFormValidator {
HogeFormValidator(this.appLocalizations);
AppLocalizations appLocalizations;
FormValidator formValidator = FormValidator();
String? hoge(String? hoge) {
final result = _validateHoge(hoge);
return result != ValidateResult.valid
? appLocalizations.hogeValidator(result.name)
: null;
}
ValidateResult _validatehoge(String? hoge) {
if (formValidator.isEmpty(hoge)) {
return ValidateResult.empty;
} else {
return ValidateResult.valid;
}
}
}
class HogeTextFormField extends StatelessWidget {
const HogeTextFormField({super.key});
Widget build(BuildContext context) {
final appLocalizations = AppLocalizations.of(context)!;
final validator = HogeFormValidator(appLocalizations);
return TextFormField(
...
validator: validator.hoge,
);
}
}
設計・運用のポイント(構築後)
端末言語設定との同期
localeListResolutionCallback でbasicLocaleListResolutionを使用し、 locale(端末言語設定)とsupportedLocales(アプリサポート言語)から使用する言語の解決ができます。
return MaterialApp(
locale: ref.watch(localeStateProvider),
localeListResolutionCallback: (locales, supportedLocales) {
// 例)
// * 端末の言語設定: 中国語(繁体字) > 英語 > 日本語
// アプリ対応言語: 日本語 > 英語
// → 端末言語設定の優先順位で最初にマッチする「英語」が選択される
// * 端末の言語設定: 中国語(繁体字)
// アプリ対応言語: 英語 > 日本語
// → マッチする言語がないため、アプリサポート言語の中で最も優先度が高い「英語」が選択される
final locale = basicLocaleListResolution(locales, supportedLocales);
Intl.defaultLocale = locale.toString(); // アプリのi18nのデフォルトロケール設定
// 描画時にステートを更新するとエラーになるためaddPostFrameCallbackを使用
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(localeStateProvider.notifier).set(locale);
}
return locale;
);
非同期処理が必要な場合
localeListResolutionCallback は async にできないため、非同期処理を行う場合は工夫が必要です。
ポイントは「callback の中で Future(() async { ... }) を起動し、保存処理は post-frame で流す」ことです。
return MaterialApp(
locale: ref.watch(localeStateProvider).whenOrNull(data: (locale) => locale);
localeListResolutionCallback: (locales, supportedLocales) {
Future(() async {
// なんらかの非同期処理
final locale = basicLocaleListResolution(locales, supportedLocales);
Intl.defaultLocale = locale.toString();
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(localeStateProvider.notifier).set(locale);
}
return locale;
});
// callback 自体は async にできないため、ここでは null を返して後段の rebuild に任せる
return null;
},
);
zh-XXのように地域コードが存在する場合
basicLocaleListResolution はzh(国コード)でlocaleが返されるため、zh-CH,zh-TWが区別できずに予期せぬ動作になってしまいます。
そのためLocale解決用のメソッドを自作する必要があります。
例としてscriptCodeで解決するメソッドが以下になります。
Locale resolveLocale(
List<Locale>? locales,
Iterable<Locale> supportedLocales,
) {
final supported = supportedLocales.toList(growable: false);
final fallback = supported.first;
Locale useIfSupported(Locale candidate) =>
supported.contains(candidate) ? candidate : fallback;
final deviceLocales = locales ?? const <Locale>[];
for (final deviceLocale in deviceLocales) {
switch (deviceLocale.languageCode) {
case 'en':
return Locale('en');
case 'ja':
return Locale('ja');
case 'zh':
final scriptCode = deviceLocale.scriptCode;
if (scriptCode != null && scriptCode.isNotEmpty) {
switch (scriptCode.toLowerCase()) {
case 'hans': // 簡体
return Locale('zh', 'CN');
case 'hant': // 繁体
return Locale('zh', 'TW');
}
}
}
}
return fallback;
}
arbファイルの管理
チーム開発では、arb を手作業で管理すると次の課題が起きやすいです。
- 命名コスト: 文言ごとにキー設計・命名が必要
- コミット衝突: 変更が同じファイルに集中し、特に開発初期はコンフリクトが増えがち
- ヒューマンエラー: JSON 編集のため、小さなミスが起きやすい(タイポなど)
対策としては、翻訳シート(スプレッドシート等)を正とし、arb はそこから生成する運用に寄せるのが現実的です。
翻訳シートを元に AI や GAS で arb を自動生成するなど、チームで再現できる生成フローを決めておくと安心です。
iOSの権限ダイアログ文言
Flutter 側の文言(arb)とは別に、iOS の権限ダイアログ文言(NSCameraUsageDescription など)は Info.plist / InfoPlist.strings で設定します。
デバイス標準ではない文言で多言語対応したい場合は、各言語の InfoPlist.strings を用意し、同じキーに対して翻訳文言を置く必要があります。
InfoPlist.strings の例(ios/en.lproj/InfoPlist.strings):
※StringCatalogsを使ったほうがより良いのかもしれない。
NSPhotoLibraryUsageDescription = "Access the photo library to select and save images.";
その他の注意点
翻訳文言
翻訳文言の長さを考慮しないと、レイアウト崩れが起きてしまう可能性があります。
フォント
使用しているフォントが言語に対応しておらず、豆腐文字になってしまう可能性があります。
OS毎の考慮
flutter_localizations で対応していても、iOSとAndroidで対応していないケースがあります。
iOSでは対応していても、Androidで対応してないケースもあったり。
アプリ名
必要な場合はアプリ名の多言語化も実施。
まとめ
Flutterの多言語化対応は、思うほど難しいことが少ないように思います。
ですが、要件によって考慮すべきことは多くなり、重大なインシデントにもつながるため(自戒)、全体像を把握できるように本記事を書いてみました。
国際化(i18n)、多言語化(l10n)することでアプリをより多くの人に届けられます。
gen-l10n(arb)を中心に仕組みを整えつつ、arb の運用(命名・更新・生成)もチームで回せる形にして、安心してアプリをスケールさせていきたい。







