iT邦幫忙

2025 iThome 鐵人賽

DAY 4
0
Mobile Development

我的 Flutter 進化論:30 天打造「Crew Up!」的架構之旅系列 第 4

Day 4 - 國際化與在地化:打造全球化的 App

  • 分享至 

  • xImage
  •  

大家好,歡迎來到第四天!在 Day 3,我們建立了精美的 UI 元件。今天,我們要來討論如何讓我們的 App 支援多國語言,為全球使用者提供在地化的體驗。

國際化(i18n)和在地化(l10n)是讓 App 支援多國語言的核心技術。本章將說明如何在 Flutter 專案中實作多語言支援,讓全球使用者都能享受在地化體驗。

認識 i18n 和 l10n:多語言支援的兩個面向

國際化(i18n)和在地化(l10n)是多語言 App 開發的兩個核心概念:

國際化(i18n):架構設計

設計和建立一個 App,讓它能夠適應各種語言和地區,而不需要對程式碼進行大幅修改

重點包括:

  • 建立彈性的架構,支援多種語言和字元集
  • 設計能適應不同文字長度的 UI 排版
  • 預先考慮日期、貨幣、數字格式的差異
  • 支援不同的文字方向(左至右/右至左)

在地化(l10n):細節調整

將 App 調整為適合特定地區或語言的過程,不只是文字翻譯而已。

涵蓋範圍:

  • 文字翻譯和內容在地化
  • 圖形、圖示的文化調整
  • 當地習俗、法規的配合
  • 日期、貨幣、數字的格式調整

💡 白話來說: i18n 是「讓程式架構能支援多語言」,l10n 是「針對特定地區進行細節調整」。

為什麼選擇 Flutter 官方 l10n 解決方案?

市面上有很多多語言解決方案,為什麼我們要選擇 Flutter 官方的 l10n 呢?

三種主要解決方案比較

方案 優點 缺點 適合情境
Flutter 官方 l10n 官方維護、型別安全、IDE 整合 學習門檻較高 長期專案、團隊合作
第三方套件 設定簡單、彈性高 維護風險、型別安全較弱 快速原型、小型專案
自行開發 完全掌控 開發維護成本高 特殊需求、資源充足

為什麼選擇官方 l10n?

對於 Crew Up 這樣的長期專案來說,官方 l10n 是最好的選擇:

🎯 核心優點

  • 穩定可靠:與 Flutter SDK 同步更新,沒有維護風險
  • 型別安全:編譯時檢查,避免執行時字串錯誤
  • 開發效率:IDE 自動完成、重構支援、自動檢查翻譯完整性
  • 標準化:ARB 格式方便與翻譯團隊協作

🌏 中文專案友好

  • 完美支援中文 zh 的在地化機制
  • 不需要複雜的複數規則處理
  • 支援中文特有的日期、數字格式

Crew Up 專案的 l10n 架構

專案設定

我們的專案採用官方的 Flutter l10n 解決方案,以中文為主要語言,同時支援國際化:

// l10n.yaml
arb-dir: lib/l10n                    # ARB 檔案存放資料夾
template-arb-file: app_en.arb        # 英文預設檔案(符合國際化標準)
output-localization-file: app_localizations.dart  # 產生統一的在地化類別檔案
output-class: S                      # 簡化類別名稱,使用 S.of(context)
nullable-getter: false               # 確保所有翻譯都有值,避免 null 錯誤
required-resource-attributes: true   # 強制要求每個翻譯項目都要有說明
untranslated-messages-file: l10n_errors.txt  # 自動追蹤未翻譯項目

App 設定

// lib/app.dart
MaterialApp(
  // 國際化設定 - 依系統語言自動切換
  localizationsDelegates: S.localizationsDelegates,
  supportedLocales: S.supportedLocales,
  // ...
)

提醒:預設不需要手動設定 locale,Flutter 會自動依據系統語言從 supportedLocales(例如:enzh)挑選最佳符合的語系。

如需在特定情境下「強制語言」(例如 Demo 或除錯),才建議暫時加入以下設定:

// 不建議做為預設,僅於特定情境使用
MaterialApp(
  locale: const Locale('zh'), // 強制顯示中文
  localizationsDelegates: S.localizationsDelegates,
  supportedLocales: S.supportedLocales,
)

pubspec.yaml 設定

// pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter
  intl: ^0.20.2

支援語言

目前專案支援以下語言:

  • 中文(zh) - 主要語言,包含完整翻譯
  • 英文(en) - 預設語言,符合國際化標準

💡 Flutter 語言機制:

Flutter 的 locale resolution 機制會按照優先順序尋找合適的語言資源:

zh → zh.arb → en.arb

這樣就能確保中文使用者都能獲得完整的在地化體驗!

ARB 檔案架構與最佳實踐

ARB 檔案結構

1. 主要語言:中文(完整翻譯)

// lib/l10n/app_zh.arb
{
  "@@locale": "zh",
  "appTitle": "Crew Up",
  "@appTitle": {
    "description": "App 標題"
  },
  "login": "登入",
  "@login": {
    "description": "登入按鈕文字"
  },
  "googleLogin": "Google 登入",
  "@googleLogin": {
    "description": "Google 登入按鈕文字"
  },
  "createActivity": "建立活動",
  "@createActivity": {
    "description": "建立活動按鈕文字"
  }
  // ... 包含所有功能的完整翻譯
}

2. 預設語言:英文(國際化標準)

// lib/l10n/app_en.arb
{
  "@@locale": "en",
  "appTitle": "Crew Up",
  "@appTitle": {
    "description": "The title of the application"
  },
  "login": "Login",
  "@login": {
    "description": "Login button text"
  }
  // ... 包含所有功能的完整翻譯
}

進階功能實作

1. 預留位置(Placeholders)

// lib/l10n/app_en.arb
{
  "daysAgo": "{count} days ago",
  "@daysAgo": {
    "description": "Days ago time label",
    "placeholders": {
      "count": {
        "type": "int"
      }
    }
  }
}

2. 複數形式處理(可選進階功能)

// lib/l10n/app_en.arb
{
  "reviewCount": "{count, plural, =0 {no reviews} =1 {1 review} other {{count} reviews}}",
  "@reviewCount": {
    "description": "Number of reviews with plural support",
    "placeholders": {
      "count": {
        "type": "int",
        "description": "The number of reviews"
      }
    }
  }
}

💡 實務建議: 複數形式處理雖然功能強大,但對中文來說通常沒必要。我們更推薦使用 enum extension 的 displayName 方法來處理動態文字,這樣更簡潔也更有型別安全。

3. 格式化支援

// lib/l10n/app_en.arb
{
  "activityDays": "{days} days",
  "@activityDays": {
    "description": "Activity duration in days",
    "placeholders": {
      "days": {
        "type": "int",
        "description": "Number of days"
      }
    }
  }
}

實際應用案例

1. 基本文字在地化

// 在 Widget 中使用(S 是由 l10n.yaml 中 output-class: S 設定自動產生的在地化資源類別)
Text(S.of(context).appTitle)

// 有參數的文字
Text(S.of(context).daysAgo(5))

// 複數形式(可選進階功能,中文專案通常不需要)
Text(S.of(context).reviewCount(reviewCount))

// 推薦:使用 enum displayName 方法處理動態文字
Text(ActivityStatus.registering.displayName(context))

2. 動態語言內容

// 相對時間處理
String getRelativeTime(int hours) {
  if (hours < 1) {
    return S.of(context).justNow;
  } else if (hours < 24) {
    return S.of(context).hoursAgo(hours);
  } else {
    final days = hours ~/ 24;
    return S.of(context).daysAgo(days);
  }
}

3. 格式化數字和日期

// 使用 intl 套件進行格式化
String formatCurrency(double amount, BuildContext context) {
  final formatter = NumberFormat.currency(
    locale: Localizations.localeOf(context).toString(),
    symbol: '\$',
  );
  return formatter.format(amount);
}

String formatDate(DateTime date, BuildContext context) {
  final formatter = DateFormat.yMd(
    Localizations.localeOf(context).toString(),
  );
  return formatter.format(date);
}

錯誤處理與品質保證

l10n_errors.txt 監控

專案設定了未翻譯訊息追蹤:

// l10n.yaml
untranslated-messages-file: l10n_errors.txt

這個檔案會自動記錄:

  • 缺少的翻譯
  • 不一致的參數
  • 格式錯誤

必要屬性檢查

// l10n.yaml
required-resource-attributes: true

確保每個翻譯項目都有合適的說明和上下文資訊。

Enum 國際化的實作方式(推薦作法)

痛點:分散的國際化邏輯

想像一下,我們有一個活動狀態需要在多個地方顯示。如果我們在每個 Widget 的 build 方法裡都這樣寫:

// ❌ 問題:邏輯分散各處,難以維護
Widget build(BuildContext context) {
  String statusText;
  switch (activity.status) {
    case ActivityStatus.registering:
      statusText = S.of(context).statusRegistering;
      break;
    case ActivityStatus.inProgress:
      statusText = S.of(context).statusInProgress;
      break;
    case ActivityStatus.completed:
      statusText = S.of(context).statusCompleted;
      break;
  }
  return Text(statusText);
}

很快就會發現:

  • 🔄 重複程式碼:相同的 switch 邏輯分散在多個檔案中
  • 🐛 容易出錯:新增狀態時容易忘記更新某些地方
  • 🔧 難以維護:修改邏輯需要找到所有使用的地方
  • 📝 缺乏型別安全:字串處理容易出現拼字錯誤

解決方案:使用 Extension 統一管理

為了解決這個問題,我們在 Crew Up 專案中使用 enum extension 搭配 displayName 方法,將國際化邏輯集中管理:

// lib/features/activity/domain/entities/activity_status.dart
/// 活動狀態
enum ActivityStatus {
  registering,
  inProgress,
  completed,
}

/// 使用 extension 統一管理國際化文字
extension ActivityStatusExtension on ActivityStatus {
  String displayName(BuildContext context) {
    switch (this) {
      case ActivityStatus.registering:
        return S.of(context).statusRegistering;
      case ActivityStatus.inProgress:
        return S.of(context).statusInProgress;
      case ActivityStatus.completed:
        return S.of(context).statusCompleted;
    }
  }
}

效果:簡潔優雅的使用方式

現在,原本複雜的國際化邏輯變得如此簡潔:

// ✅ 解決方案:一行搞定!
Widget build(BuildContext context) {
  return Text(activity.status.displayName(context));
}

// ✅ 條件顯示 - 型別安全
if (activity.status == ActivityStatus.registering) {
  return Text(activity.status.displayName(context));
}

// ✅ 批次處理 - 自動完成支援
...ActivityStatus.values.map(
  (status) => DropdownMenuItem(
    value: status,
    child: Text(status.displayName(context)),
  )
).toList()

// ✅ 在任何地方都能一致使用
AppBar(title: Text(currentStep.displayName(context)))

為什麼這個作法這麼實用?

🏆 這個作法的優勢

  • 型別安全:編譯時就能發現錯誤,不會在執行時才發現拼字錯誤
  • IDE 支援:自動完成、重構、查找引用,開發效率翻倍
  • 統一 API:所有 enum 都用相同模式,團隊合作更順暢
  • 集中管理:國際化邏輯集中在一處,維護變得輕鬆
  • 測試友善:可以輕鬆 mock context 進行單元測試
  • 中文友善:完美適配中文專案,不需要複雜的複數規則

對比:傳統方法的痛點

  • 分散邏輯:switch 語句重複出現在各處
  • 容易漏掉:新增 enum 值時容易忘記更新顯示邏輯
  • 複數形式:對中文專案是過度設計,增加不必要的複雜度
  • 執行時錯誤:字串拼字錯誤只能在執行時發現

💡 實際效果
在 Crew Up 專案中,採用這個作法後:

  • 🚀 減少了大量重複程式碼
  • 🐛 降低國際化相關錯誤
  • 提升開發效率
  • 🧪 測試比較容易寫

自動化與工具鏈

產生與建置

# 產生在地化檔案
flutter gen-l10n

# 檢查翻譯完整性
cat l10n_errors.txt

# 建置時包含在地化
flutter build apk --target-platform android-arm64

持續整合支援

// .github/workflows/ci.yml
# GitHub Actions 範例
- name: Generate localizations
  run: flutter gen-l10n

- name: Check translation completeness
  run: |
    if [ -s l10n_errors.txt ]; then
      echo "Missing translations found:"
      cat l10n_errors.txt
      exit 1
    fi

總結

透過 Flutter 官方 l10n 方案,我們為 Crew Up 建立了完整的多語言架構:

🎯 核心成果

  • 型別安全的國際化:使用 enum extension 統一管理動態文字
  • 完整的語言支援:zh(主要) → en(預設)
  • 開發效率提升:IDE 整合、自動檢查、重構支援

📋 實作重點

  • 採用官方 flutter_localizations 和 ARB 格式
  • 推薦使用 enum.displayName(context) 取代分散的 switch 邏輯
  • 建立完整的測試和品質保證流程

下一步

明天,我們將深入探討導航架構設計,學習如何透過 go_router 建立路由系統,並結合本專案的實際需求進行應用。

期待與您在 Day 5 相見!


📋 相關資源

📝 專案資訊

  • 專案名稱: Crew Up!
  • 開發日誌: Day 4 - 國際化與在地化:打造全球化的 App
  • 文章日期: 2025-09-18
  • 技術棧: Flutter 3.8+, Dart 3.8+, flutter_localizations, intl

上一篇
Day 3 - 元件的藝術:打造類型安全、高效能的 Flutter UI 分子
下一篇
Day 5 - 導航不再迷路:go_router 實戰心得與架構演進
系列文
我的 Flutter 進化論:30 天打造「Crew Up!」的架構之旅11
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言