iT邦幫忙

2025 iThome 鐵人賽

DAY 24
0
Mobile Development

Flutter :30天打造念佛App,跨平台應用從Mobile到VR,讓極樂世界在眼前實現!系列 第 24

[ Day 24 ] Flutter 多國語系 — App翻譯蒟蒻, 上架各國必備的好幫手!

  • 分享至 

  • xImage
  •  

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


前言

昨天我們已經完成「第三方登入 Google & Apple 」,
我們已經可以辨識使用者身份,並且妥善地將使用者資料保存本機與雲端。
今天我們要來實作「多國語系」,
讓來全球各地的使用者都能使用熟悉的語言操作App

Day24 文章目錄:
一、多國語系
二、前置設定
三、實作核心


一、多國語系

1. 簡介

多國語系通常是指為app添加語言轉換的功能,
其中會涉及兩個層面「國際化」、「本地化」。

2. 國際化與本地化

國際化( i18n ):指的是開發時將 App 設計成「 能夠適配 語言及文化轉換 」 。
例如:

  1. 字串不被截斷
  2. 兼顧方向性 (LTR/RTL)

本地化( l10n ):指的是將 App「 實際適配 」某個語言、地區與文化。
例如:

  1. 佛號累計108次,英文介面完整顯示:
    A total of 108 times of chanting Buddha's name
  2. 確保文字依照正確閱讀方向顯示:
    阿拉伯文的書寫閱讀由右至左
面向 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>

Day24 重點回顧

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

上一篇
[ Day 23 ] Flutter 第三方登入 實戰應用篇—穿越到Coding世界的勇者啊,你知道裝備可以放哪嗎(3) #Apple登入 #Google登入
系列文
Flutter :30天打造念佛App,跨平台應用從Mobile到VR,讓極樂世界在眼前實現!24
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言