Flutter: 国際化とローカライゼーション
1つのアプリで世界中へ — 国際化によりコードがユーザーの言語を話し、ユーザーの通貨を使います。
📋 前提条件: 以下に既に慣れている必要があります
- レッスン16:テーマとスタイリングシステム
1. このレッスンで学ぶこと
- flutter_localizations + intlパッケージ:ARBファイルワークフローとコード生成
- localizationsDelegatesとsupportedLocales設定
- 動的言語切り替え:実行時ロケール切り替え(Riverpod管理)
- ICUフォーマット:数値(千単位区切り)、通貨(USD/EUR/CNY)、日付、複数形
- ShopApp:中国語/英語/日本語三カ国語 + USD/CNY/JPY多通貨切り替え
2. グローバル展開の混乱のリアルなストーリー
(1) 悩み:ハードコードされた値がグローバルユーザーを混乱させる
BobのShopAppは英語とUSDのみをサポート。Alice(中国語ユーザー)は「$9,999.00」を見て9999 USDだと思い込みますが、本来は9999 CNYであるべきです。Charlie(日本語ユーザー)は「1,500」を見て、千五百なのか十五百なのか判別できません。さらに悪いことに、商品説明はすべて英語で、非英語ユーザーの離脱率は70%に達しています。
(2) ARB + ICUフォーマットソリューション
Flutterのl10nシステムはARBファイルで翻訳を管理し、ICUフォーマットが数値/通貨/日付の地域差を自動的に処理します。
DART
import 'package:intl/intl.dart';
// ⚙️ Install dependencies: flutter pub add intl
// Locale-aware formatting
final price = NumberFormat.simpleCurrency(locale: 'zh_CN').format(9999);
// → ¥9,999.00 (CNY)
final price2 = NumberFormat.simpleCurrency(locale: 'en_US').format(1299.99);
// → $1,299.99 (USD)
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
(3) 成果:離脱率が70%から15%に低下
3言語 + 3通貨を実装後、非英語圏の離脱率は70%から15%に低下し、日本市場では注文が3倍に増加しました。
3. i18nデータフロー
graph TD
ARB[ARB Files] --> |l10n| GEN[Generated Dart]
GEN --> MAT[MaterialApp.localizationsDelegates]
MAT --> L10N[AppLocalizations]
L10N --> EN[English: $1,299.99]
L10N --> ZH[中文: ¥9,999.00]
L10N --> JA[日本語: ¥150,000]
subgraph Users
Alice2[Alice: EN / USD]
Bob2[Bob: ZH / CNY]
Charlie[Charlie: JA / JPY]
end
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
(1) 国際化の主要用語
| 用語 | 説明 | 例 |
|---|---|---|
| i18n | Internationalization(18文字省略) | フレームワークサポート |
| l10n | Localization(10文字省略) | 特定の翻訳 |
| Locale | 言語 + 地域識別子 | en_US, zh_CN, ja_JP |
| ARB | Application Resource Bundle | 翻訳ファイル形式 |
| ICU | International Components for Unicode | フォーマット標準 |
4. ARBファイルワークフロー
(1) プロジェクト設定
YAML
# l10n.yaml (project root)
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
output-class: S
nullable-getter: false
YAML
# pubspec.yaml
flutter:
generate: true
▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:ARB翻訳ファイル
JSON
// lib/l10n/app_en.arb (template)
{
"appTitle": "ShopApp",
"productCount": "{count, plural, =0{No products} =1{1 product} other{{count} products}}",
"priceWithCurrency": "{price, select, USD{${price}} CNY{¥{price}} JPY{¥{price}}}",
"welcomeMessage": "Welcome, {name}!",
"lastUpdated": "Last updated: {date}",
"@productCount": {
"placeholders": { "count": { "type": "int" } }
},
"@priceWithCurrency": {
"placeholders": { "price": { "type": "String" }, "currency": { "type": "String" } }
},
"@welcomeMessage": {
"placeholders": { "name": { "type": "String" } }
},
"@lastUpdated": {
"placeholders": { "date": { "type": "DateTime" } }
}
}
JSON
// lib/l10n/app_zh.arb
{
"appTitle": "ShopApp",
"productCount": "{count, plural, =0{没有商品} other{{count} 件商品}}",
"priceWithCurrency": "{price, select, USD{\\${price}} CNY{¥{price}} JPY{¥{price}}}",
"welcomeMessage": "欢迎,{name}!",
"lastUpdated": "最后更新:{date}"
}
JSON
// lib/l10n/app_ja.arb
{
"appTitle": "ShopApp",
"productCount": "{count, plural, other{{count}件の商品}}",
"priceWithCurrency": "{price, select, USD{\\${price}} CNY{¥{price}} JPY{¥{price}}}",
"welcomeMessage": "ようこそ、{name}さん!",
"lastUpdated": "最終更新: {date}"
}
5. MaterialApp設定
▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:l10n設定
DART
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// ⚙️ Install dependencies: flutter pub add flutter_riverpod
// ⚙️ Configure l10n: create l10n.yaml in project root, add flutter: generate: true to pubspec.yaml
// Custom class definition source:
// - localeProvider: see Section 7 LocaleNotifier in this lesson
class ShopApp extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final locale = ref.watch(localeProvider);
return MaterialApp(
locale: locale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
title: 'ShopApp',
home: const HomePage(),
);
}
}
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:翻訳文字列の使用
DART
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
// ⚙️ Configure l10n: create l10n.yaml in project root, run flutter gen-l10n to generate code
// In any widget
final s = AppLocalizations.of(context)!;
Text(s.appTitle) // ShopApp
Text(s.productCount(42)) // 42 products / 42 件商品
Text(s.welcomeMessage('Alice')) // Welcome, Alice! / 欢迎,Alice!
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
6. ICUフォーマット
▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:通貨フォーマット
DART
import 'package:intl/intl.dart';
import 'package:intl/number_symbols_data.dart';
// ⚙️ Install dependencies: flutter pub add intl
class CurrencyFormatter {
static String format(double amount, {String locale = 'en_US', String? currency}) {
final format = NumberFormat.simpleCurrency(locale: locale, name: currency);
return format.format(amount);
}
}
// Usage
CurrencyFormatter.format(1299.99, locale: 'en_US') // $1,299.99
CurrencyFormatter.format(9999.00, locale: 'zh_CN') // ¥9,999.00
CurrencyFormatter.format(150000, locale: 'ja_JP') // ¥150,000
CurrencyFormatter.format(99.99, locale: 'de_DE', currency: 'EUR') // 99,99 €
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
| Locale | 数値フォーマット | 通貨フォーマット | 日付フォーマット |
|---|---|---|---|
| en_US | 1,299.99 | $1,299.99 | 07/13/2026 |
| zh_CN | 1,299.99 | ¥9,999.00 | 2026/07/13 |
| ja_JP | 1,299.99 | ¥150,000 | 2026/07/13 |
| de_DE | 1.299,99 | 1.299,99 € | 13.07.2026 |
▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:日付フォーマット
DART
import 'package:intl/intl.dart';
// ⚙️ Install dependencies: flutter pub add intl
String formatDate(DateTime date, {String locale = 'en_US'}) {
return DateFormat.yMMMd(locale).format(date);
}
// en_US: Jul 13, 2026
// zh_CN: 2026年7月13日
// ja_JP: 2026年7月13日
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:複数形フォーマット
DART
// In ARB file
"cartItemCount": "{count, plural, =0{Your cart is empty} =1{1 item in cart} other{{count} items in cart}}"
// Usage
Text(s.cartItemCount(0)) // Your cart is empty
Text(s.cartItemCount(1)) // 1 item in cart
Text(s.cartItemCount(5)) // 5 items in cart
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
7. 動的言語切り替え
▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:Riverpod Locale管理
DART
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:intl/intl.dart';
// ⚙️ Install dependencies: flutter pub add flutter_riverpod riverpod_annotation shared_preferences intl
// ⚙️ Dev dependencies: flutter pub add --dev riverpod_generator build_runner
// Custom class definition source:
// - localeNotifierProvider/currencyProvider: see definitions below
@riverpod
class LocaleNotifier extends _$LocaleNotifier {
@override
Locale build() {
// Load saved preference
_loadSavedLocale();
return const Locale('en');
}
Future<void> _loadSavedLocale() async {
final prefs = await SharedPreferences.getInstance();
final saved = prefs.getString('locale');
if (saved != null) {
state = Locale(saved);
}
}
Future<void> setLocale(Locale locale) async {
state = locale;
final prefs = await SharedPreferences.getInstance();
await prefs.setString('locale', locale.languageCode);
}
}
// Settings page
class LanguageSettingsPage extends ConsumerWidget {
static const _locales = [
(Locale('en'), 'English', '🇺🇸'),
(Locale('zh'), '中文', '🇨🇳'),
(Locale('ja'), '日本語', '🇯🇵'),
];
@override
Widget build(BuildContext context, WidgetRef ref) {
final current = ref.watch(localeNotifierProvider);
return Scaffold(
appBar: AppBar(title: const Text('Language')),
body: ListView(children: _locales.map((item) {
final (locale, name, flag) = item;
return ListTile(
leading: Text(flag, style: const TextStyle(fontSize: 24)),
title: Text(name),
trailing: current == locale ? const Icon(Icons.check, color: Colors.green) : null,
onTap: () => ref.read(localeNotifierProvider.notifier).setLocale(locale),
);
}).toList()),
);
}
}
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
8. 完全な例:ShopAppローカライズ価格ウィジェット
DART
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:intl/intl.dart';
// ⚙️ Install dependencies: flutter pub add flutter_riverpod riverpod_annotation shared_preferences intl
// ⚙️ Dev dependencies: flutter pub add --dev riverpod_generator build_runner
// Custom class definition source:
// - localeNotifierProvider: see Section 7 LocaleNotifier in this lesson
// - currencyProvider: see CurrencyNotifier below
class LocalizedPrice extends ConsumerWidget {
final double amount;
final TextStyle? style;
const LocalizedPrice({super.key, required this.amount, this.style});
@override
Widget build(BuildContext context, WidgetRef ref) {
final locale = ref.watch(localeNotifierProvider);
final currency = ref.watch(currencyProvider);
final formatted = _formatPrice(amount, locale.languageCode, currency);
return Text(formatted, style: style ?? const TextStyle(fontSize: 18, fontWeight: FontWeight.bold));
}
String _formatPrice(double amount, String languageCode, String currencyCode) {
final locale = switch (languageCode) {
'zh' => 'zh_CN',
'ja' => 'ja_JP',
_ => 'en_US',
};
return NumberFormat.simpleCurrency(locale: locale, name: currencyCode).format(amount);
}
}
// Currency provider
@riverpod
class CurrencyNotifier extends _$CurrencyNotifier {
@override
String build() {
_loadSaved();
return 'USD';
}
Future<void> _loadSaved() async {
final prefs = await SharedPreferences.getInstance();
final saved = prefs.getString('currency');
if (saved != null) state = saved;
}
Future<void> setCurrency(String code) async {
state = code;
final prefs = await SharedPreferences.getInstance();
await prefs.setString('currency', code);
}
}
// Usage in product card
LocalizedPrice(amount: product.price) // Auto-formats based on user's locale & currency
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
❓ よくある質問
Q ARBファイルの変更はどう反映されますか?
A
flutter gen-l10nを実行するか、単にflutter runしてください(自動的に生成がトリガーされます)。Q ホットリロードは言語切り替えをサポートしていますか?
A 部分的に。ARB内容の変更にはホットリスタート(R)が必要ですが、Locale切り替えはホットリロード(r)に対応しています。
Q 通貨フォーマットにintlと手動文字列連結のどちらを使うべきですか?
A intlのNumberFormat.simpleCurrencyを使ってください。手動連結ではロケール間の千単位区切りと小数点の違いに対応できません。
Q 中国語の「万」はどうフォーマットしますか?
A ICU標準には「万」の単位がありません — 中国語の数値も千単位区切りのカンマを使用します(0.9999万ではなく9,999)。「万」表示にはカスタムフォーマットが必要です。
Q RTL言語(アラビア語)はどうサポートしますか?
A FlutterはRTLレイアウトを自動的に処理します(
locale: Locale('ar')を設定)。Directionalityとstart/endをleft/rightの代わりに使用してください。Q 翻訳ファイルは誰が管理しますか?
A 開発者が英語テンプレートARBを管理し、翻訳チームまたはAIが他言語ARBを翻訳します。Lokalise/Crowdinのような翻訳管理プラットフォームの使用を推奨します。
📖 まとめ
- ARBファイルが翻訳を管理、flutter gen-l10nが型安全なDartコードを自動生成
- MaterialAppがlocalizationsDelegates + supportedLocalesを設定
- ICUフォーマットが数値/通貨/日付/複数形のロケール差を自動処理
- Riverpod + SharedPreferencesが言語と通貨の設定を管理
- NumberFormat.simpleCurrencyで多通貨フォーマットが可能
📝 練習問題
- 基本 (⭐):l10n.yamlとARBファイルを設定し、10以上の翻訳文字列で中国語/英語二カ国語切り替えを実装してください。
- 中級 (⭐⭐):日本語サポートを追加し、ICU複数形フォーマット(カート内商品数)と千単位区切り数値の自動適応を実装してください。
- チャレンジ (⭐⭐⭐):完全なShopApp多言語システムを実装してください:中国語/英語/日本語三カ国語 + USD/CNY/JPY三通貨 + 日付フォーマットローカライズ + 設定ページ切り替え + SharedPreferences永続化。