2025 iThome鐵人賽
「 Flutter :30天打造念佛App,跨平台從Mobile到VR,讓極樂世界在眼前實現 ! 」
Day 24
「 Flutter 多國語系 — App 翻譯蒟蒻, 上架各國必備的好幫手 !」

昨天我們已經完成「第三方登入 Google & Apple 」,
我們已經可以辨識使用者身份,並且妥善地將使用者資料保存本機與雲端。
今天我們要來實作「多國語系」,
讓全球各地的使用者都能使用熟悉的語言操作App。
Day24 文章目錄:
一、多國語系
二、前置設定
三、實作核心
1. 簡介
多國語系通常是指為app添加語言轉換的功能,
其中會涉及兩個層面「國際化」、「本地化」。
2. 國際化與本地化
國際化( i18n ):指的是開發時將 App 設計成「 能夠適配 語言及文化轉換 」 。
例如:
- 字串不被截斷
- 兼顧方向性 (LTR/RTL)
本地化( l10n ):指的是將 App「 實際適配 」某個語言、地區與文化。
例如:
- 佛號累計108次,英文介面完整顯示:
A total of 108 times of chanting Buddha's name- 確保文字依照正確閱讀方向顯示:
阿拉伯文的書寫閱讀由右至左
| 面向 | i18n(工程能力) | l10n(內容落地) |
|---|---|---|
| 文案/字串 | gen-l10n + ARB + 型別安全 AppLocalizations | 為每個語言新增 app_xx.arb 並翻譯 |
| 框架接線 | AppLocalizations.localizationsDelegates、supportedLocales | 在 MaterialApp.locale 或 Localizations.override 覆蓋語系 |
| 日期/數字/貨幣 | intl 的 DateFormat / NumberFormat | 設定正確的 locale 與符號(例如 NT$) |
| 方向(LTR/RTL) | TextAlign.start/end、EdgeInsetsDirectional、AlignmentDirectional、Image.matchTextDirection | 實機驗證 RTL、必要時局部 Directionality 覆蓋 |
3. 常見實作與擴充方式
| 類型 | 名稱 | 主要用途 | 優點 | 注意事項 |
|---|---|---|---|---|
| 官方流程 | gen-l10n + flutter_localizations | 以 ARB 產生 AppLocalizations,官方推薦做法 | 效能佳、與框架整合深 | pubspec.yaml 啟用 flutter: generate: true |
| 格式化 | intl(Dart 套件) | 日期/數字/貨幣/格式化 | API 完整、跨平台 | 正確設定 locale 或使用 defaultLocale。 |
| VS Code Extension | ARB Editor | ARB 語法高亮、驗證、片段 | 編輯體驗好、減少格式錯誤 | 僅編輯輔助 |
| 第三方封裝 | easy_localization | 以 JSON/鍵值快速上手的封裝 | 上手快、社群多示例 | 注意長期維護策略 |
1. 安裝套件 pubspec.yaml
environment:
sdk: ">=3.5.0 <4.0.0"
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
intl: ^0.20.2
flutter: # 專案 flutter 設定區
generate: true # 必填,啟用官方 gen-l10n 產生器
2. 新增 l10n 設定 amitabha/l10n.yaml(新建在專案根目錄)
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
# 輸出到專案中,方便 IDE 導覽與相對匯入
synthetic-package: false
output-dir: lib/l10n/generated
# 常用選項(依需求開啟)
untranslated-messages-file: lib/l10n/untranslated.txt
nullable-getter: false
preferred-supported-locales: #限定與排序 supportedLocales
- en
- zh_Hant_TW
use-deferred-loading: false #是否延遲載入
3. 建立ARB字串檔
zh_Hant_TW(中文.繁體.台灣)= 語言 language + 腳本 script + 地區 region ;
gen-l10n 規則:
帶腳本碼/國碼的語系,建議提供同檔名前綴的基底語言檔,作為回退階層的一環。
app_zh_Hant_TW.arb → app_zh_Hant.arb → app_zh.arb → template (app_en.arb)
(1) 建立基底語言檔 lib/l10n/app_zh.arb
{ "@@locale": "zh" }
(2) 建立基底語言檔 lib/l10n/app_zh_Hant.arb
{ "@@locale": "zh_Hant" }
(3) 建立字串檔 lib/l10n/app_zh_Hant_TW.arb
{
"amitabha": "阿彌陀佛",
"@amitabha": { "description": "佛號顯示文字" },
"start": "開始",
"@start": { "description": "開始按鈕" },
"pause": "暫停",
"@pause": { "description": "暫停按鈕" },
"save": "儲存",
"@save": { "description": "儲存按鈕" },
"logIn": "登入",
"@logIn": { "description": "登入按鈕" },
"logOut": "登出",
"@logOut": { "description": "登出按鈕" },
"total": "累計",
"@total": { "description": "統計標題" },
"totalCount": "累計次數:{count}",
"@totalCount": {
"description": "帶數值的累計次數",
"placeholders": { "count": { "type": "int", "example": "108" } }
}
}
(4) 建立字串檔 lib/l10n/app_en.arb
{
"amitabha": "Amitabha",
"@amitabha": { "description": "Buddha name label" },
"start": "Start",
"@start": { "description": "Start button" },
"pause": "Pause",
"@pause": { "description": "Pause button" },
"save": "Save",
"@save": { "description": "Save button" },
"logIn": "Log in",
"@logIn": { "description": "Sign-in button" },
"logOut": "Log Out",
"@logOut": { "description": "Sign-out button" },
"total": "Total",
"@total": { "description": "Summary label (text only)" },
"totalCount": "Total count: {count}",
"@totalCount": {
"description": "Total count with value",
"placeholders": { "count": { "type": "int", "example": "108" } }
}
}
2. 產生在地化類別 lib/l10n/generated/app_localizations.dart
終端機執行
flutter clean
flutter pub get
flutter gen-l10n #會在 build/run 時自動產生,也可手動
1. MaterialApp 接上本地化
//app.dart
// 匯入
import 'l10n/generated/app_localizations.dart';
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) {
final m = DownloadModel();
if (F.appFlavor == Flavor.dev) {
m.useAsr();
} else {
m.useKws();
}
return m;
}),
Provider<AuthFacade>(
create: (_) => AuthFacade(
auth: FirebaseAuthRepository(),
users: FirestoreUserRepository(),
),
),
],
child: MaterialApp(
// 動態標題:多語自動顯示「阿彌陀佛 / Amitabha」
onGenerateTitle: (ctx) => AppLocalizations.of(ctx)!.amitabha,
theme: ThemeData(primarySwatch: Colors.blue),
// 啟用框架內建字串與自訂翻譯
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: _flavorBanner(child: const MyHomePage(), show: true),
),
);
}
Widget _flavorBanner({required Widget child, bool show = true}) =>
show ? Banner(
location: BannerLocation.topStart,
message: F.name,
color: Colors.green.withAlpha(150),
textStyle: const TextStyle(fontWeight: FontWeight.w700, fontSize: 12, letterSpacing: 1),
textDirection: TextDirection.ltr,
child: child,
) : child;
}
// my_home_page.dart
import 'package:flutter/material.dart';
import 'package:amitabha/flavors.dart';
import 'package:amitabha/streaming_asr.dart';
import 'package:amitabha/streaming_kws.dart';
import 'package:amitabha/l10n/generated/app_localizations.dart';
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key});
@override
Widget build(BuildContext context) {
final t = AppLocalizations.of(context)!; // 取得翻譯器
final screen = (F.appFlavor == Flavor.dev)
? const StreamingAsrScreen()
: const StreamingKwsScreen();
return Scaffold(
appBar: AppBar(
title: Text(t.amitabha), //阿彌陀佛 / Amitabha
actions: [
PopupMenuButton<String>(
itemBuilder: (_) => [ //其他鍵值
PopupMenuItem(value: 'start', child: Text(t.start)),
PopupMenuItem(value: 'pause', child: Text(t.pause)),
PopupMenuItem(value: 'save', child: Text(t.save)),
PopupMenuItem(value: 'logIn', child: Text(t.logIn)),
PopupMenuItem(value: 'logOut', child: Text(t.logOut)),
],
),
],
),
body: screen,
);
}
}
2. 日期 / 數字 / 貨幣 :使用 intl
import 'package:intl/intl.dart';
//指定locale
final dateStr = DateFormat.yMMMMEEEEd('zh_Hant_TW').format(DateTime.now());
//使用系統預設
String formatFullDate(BuildContext context, DateTime dt) {
final locale = Localizations.localeOf(context).toString();
return DateFormat.yMMMMEEEEd(locale).format(dt);
}
3. 多國語言 App 名稱
(1) Xcode Info 添加 Localizations : English 、Chinese (Traditional)

(2) 建立 InfoPlist.strings
//終端機執行
//移動到 iOS Runner 目錄
cd ios/Runner
//建立英文與繁體語系資料夾
mkdir -p en.lproj zh-Hant.lproj
//建立英文版 InfoPlist.strings
cat > en.lproj/InfoPlist.strings <<'EOF'
"CFBundleDisplayName" = "Amitabha"; //App名稱
EOF
//建立繁體版 InfoPlist.strings
cat > zh-Hant.lproj/InfoPlist.strings <<'EOF'
"CFBundleDisplayName" = "念佛"; //App名稱
EOF
(3) 添加至專案





(1) 確認 App 名稱不是硬寫常值 android/app/src/main/AndroidManifest.xml
//android/app/src/main/AndroidManifest.xml
<application
android:label="@string/app_name" //字串資源才可以被本地化覆寫
... />
(2) 建立 strings.xml
//終端機執行
//移動到 android 目錄
cd android
//建立英文與繁體語系資料夾
mkdir -p app/src/main/res/values app/src/main/res/values-zh-rTW
//建立英文版strings.xml
cat > app/src/main/res/values/strings.xml <<'EOF'
<resources>
<string name="app_name">Amitabha</string> //App名稱
</resources>
EOF
//建立中文版strings.xml
cat > app/src/main/res/values-zh-rTW/strings.xml <<'EOF'
<resources>
<string name="app_name">念佛</string> //App名稱
</resources>
EOF

| 重點 | 內容 |
|---|---|
| 多國語系 | App 落實多語言文化 |
| 前置設定 | 安裝套件 ⭢ l10n 設定 ⭢ ARB字串 ⭢ 產生在地化類別 |
| 實作核心 | MaterialApp 接上本地化 |