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: any
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字串檔
lib/l10n/app_zh_Hant_TW.arb
{
"amitabha": "阿彌陀佛",
"@amitabha": { "description": "佛號顯示文字" },
"start": "開始",
"@start": { "description": "開始按鈕" },
"pause": "暫停",
"@pause": { "description": "暫停按鈕" },
"save": "儲存",
"@save": { "description": "儲存按鈕" },
"logIn": "登入",
"@logIn": { "description": "登入按鈕" },
"total": "累計",
"@total": { "description": "統計標題" },
"totalCount": "累計次數:{count}",
"@totalCount": {
"description": "帶數值的累計次數",
"placeholders": { "count": { "type": "int", "example": 108 } }
}
}
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" },
"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)),
],
),
],
),
body: screen,
);
}
}
2. 日期 / 數字 / 貨幣 :使用 intl
import 'package:intl/intl.dart';
// 指定 locale 或 使用系統預設
final dateStr = DateFormat.yMMMMEEEEd('zh_Hant_TW').format(DateTime.now());
final money = NumberFormat.currency(locale: 'zh_Hant_TW', symbol: 'NT$').format(1234567.89);
3. 多國語言 App 名稱
iOS(InfoPlist.strings)
// ios/Runner/InfoPlist.strings (English)
"CFBundleDisplayName" = "Amitabha";
// ios/Runner/InfoPlist.strings (Chinese (Traditional))
"CFBundleDisplayName" = "念佛";
Android(多組 res)
<!-- android/app/src/main/res/values/strings.xml -->
<resources><string name="app_name">Amitabha</string></resources>
<!-- android/app/src/main/res/values-zh-rTW/strings.xml -->
<resources><string name="app_name">念佛</string></resources>
重點 | 內容 |
---|---|
多國語系 | App 落實多語言文化 |
前置設定 | 安裝套件 ⭢ l10n 設定 ⭢ ARB字串 ⭢ 產生在地化類別 |
實作核心 | MaterialApp 接上本地化 |