跳至主要内容

國際化 (i18n)

AppFuse Web 提供輕量級的國際化解決方案,支援多語言切換、參數插值和語言回退。

架構概覽

核心特點

  • 輕量級(核心 ~3KB)
  • 按需載入語言包
  • 三層回退機制:zh-TWzhen → 原文
  • 大小寫不敏感的 key

快速開始

在元件中使用

import { useTranslation } from '@appfuse/appfuse-web'

function OrderForm() {
const { t } = useTranslation()

return (
<form>
<h1>{t('Create Order')}</h1>
<button type="submit">{t('Save')}</button>
</form>
)
}

帶參數的翻譯

const { t } = useTranslation()

// 命名參數
t('Order ${orderNumber} created successfully!', { orderNumber: '12345' })
// → "訂單 12345 創建成功!"

// 位置參數
t('${0} of ${1} items', [5, 10])
// → "5 / 10 項目"

目錄結構

src/nls/
├── types.ts # TypeScript 型別(自動完成)
├── term/
│ └── zh-TW.ts # 繁體中文術語
├── message/
│ └── zh-TW.ts # 繁體中文訊息
└── article/ # 長文內容(預留目錄,目前為空)
資訊

英文不需要語言檔案,因為翻譯的 key 本身就是英文原文。當找不到翻譯時,i18n 引擎會直接回傳 key 作為顯示文字。

語言檔案格式

Term(術語)

單字或短語,無參數:

// src/nls/term/zh-TW.ts
const translations = {
'Save': '儲存',
'Cancel': '取消',
'Delete': '刪除',
'Create Order': '創建訂單',
'Customer': '客戶',
'Product': '商品',
'Active': '啟用',
'Inactive': '停用',
} as const

export default translations
export type TermKeys = keyof typeof translations

Message(訊息)

完整句子,可帶參數:

// src/nls/message/zh-TW.ts
const translations = {
'Order ${orderNumber} created successfully!': '訂單 ${orderNumber} 創建成功!',
'${field} is required': '${field}為必填',
'${field} must be between ${min} and ${max}': '${field}必須介於${min}和${max}之間',
'Permission denied: requires ${requiredRole} or higher, your role is ${userRole}':
'權限不足:此操作需要 ${requiredRole} 或更高角色,您的角色為 ${userRole}',
} as const

export default translations
export type MessageKeys = keyof typeof translations

型別定義

types.ts 彙整所有語言檔案的 key,提供 IDE 自動補全:

// src/nls/types.ts
import type { TermKeys } from './term/zh-TW'
import type { MessageKeys } from './message/zh-TW'

// 已知鍵值有自動補全,未知鍵值也允許
export type TranslationKey = TermKeys | MessageKeys | (string & {})

API 參考

i18n 物件

import { i18n } from '@appfuse/appfuse-web'

// 設定語言
i18n.language = 'zh-TW'

// 設定回退語言
i18n.fallback = 'en'

// 新增翻譯
i18n.addTranslation('zh-TW', {
'Hello': '你好',
})

// 檢查是否有翻譯
i18n.hasTranslation('zh-TW')

// 配置(通常在 App.tsx 初始化時呼叫)
i18n.configure({
language: 'zh-TW',
fallback: 'en',
})

useTranslation Hook

import { useTranslation } from '@appfuse/appfuse-web'

function MyComponent() {
const { t, language } = useTranslation()

// t: 翻譯函數
// language: 當前語言

return <div>{t('Hello')}</div>
}

useI18nContext Hook

import { useI18nContext } from '@appfuse/appfuse-web'

function LanguageSwitcher() {
const { language, setLanguage } = useI18nContext()

return (
<select
value={language}
onChange={(e) => setLanguage(e.target.value)}
>
<option value="zh-TW">繁體中文</option>
<option value="en">English</option>
</select>
)
}

React 整合

I18nProvider

在應用程式根元件包裝:

// src/main.tsx
import { I18nProvider, i18n } from '@appfuse/appfuse-web'

createRoot(document.getElementById('root')!).render(
<StrictMode>
<I18nProvider>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<App />
</Provider>
</QueryClientProvider>
</I18nProvider>
</StrictMode>,
)

語言偏好持久化

I18nProvider 自動將語言偏好儲存到 localStorage:

// 自動儲存到 localStorage['selected-locale']
// 優先順序:
// 1. localStorage 儲存的值
// 2. defaultLanguage prop
// 3. i18n.language 配置值

語言切換

LanguageToggle 元件

// src/components/language-toggle.tsx
import { i18n, useI18nContext, useTranslation } from '@appfuse/appfuse-web'
import { nlsService } from '@/services/core'

function LanguageToggle() {
const { t } = useTranslation()
const [loadingLanguage, setLoadingLanguage] = useState<string | null>(null)
const { language: currentLanguage, setLanguage } = useI18nContext()

const handleLanguageChange = async (language: string) => {
// 檢查是否已載入,未載入則動態載入
if (!i18n.hasTranslation(language)) {
setLoadingLanguage(language)
try {
const translation = await nlsService.fetchTranslation(language)
i18n.addTranslation(language, translation)
} finally {
setLoadingLanguage(null)
}
}
setLanguage(language)
}

// ... 渲染語言選項
}

初始化流程

應用程式啟動

App.tsx 在 useEffect 中依序完成初始化:

// src/App.tsx
import { environ, i18n } from '@appfuse/appfuse-web'
import { nlsService } from './services/core'

// 在 useEffect 中執行初始化
const initialize = async () => {
// Step 1: 獲取並配置環境
const environOpts = await environService.fetch()
environ.configure(environOpts)
i18n.configure(environ.i18n)

// Step 2: 恢復用戶語言偏好
const savedLocale = localStorage.getItem('selected-locale')
if (savedLocale) {
i18n.language = savedLocale
}

// Step 3: 載入當前語言包
const userLanguage = i18n.language
const translation = await nlsService.fetchTranslation(userLanguage)
i18n.addTranslation(userLanguage, translation)

// Step 4: 載入回退語言包
if (userLanguage !== i18n.fallback) {
const fallbackTranslation = await nlsService.fetchTranslation(i18n.fallback)
i18n.addTranslation(i18n.fallback, fallbackTranslation)
}
}
提示

main.tsx 中還預載了 Splash Screen 所需的最小翻譯集(如「載入中」、「正在取得語言包 ...」),確保在完整語言包載入前就能顯示繁體中文。

環境配置

// src/conf/config.ts
import { type EnvironConfig } from '@appfuse/appfuse-web'

export const config: EnvironConfig<AppConfig> = {
app: {
name: 'App',
// ...
},
i18n: {
language: 'zh-TW', // 預設語言
fallback: 'en', // 回退語言
languages: ['en', 'zh-TW'], // 支援的語言
},
}

NLS Service

nlsService 使用 Vite 的 import.meta.glob 動態載入語言檔案:

// src/services/core/nls-service.ts
const translationModules = import.meta.glob('../../nls/{term,message,article}/*.ts')

class NlsService {
async fetchTranslation(language: string) {
let translation = {}
for (const pack of ['term', 'message', 'article']) {
const path = `../../nls/${pack}/${normalizedLanguage}.ts`
if (translationModules[path]) {
const module = await translationModules[path]()
translation = { ...translation, ...module.default }
}
}
return translation
}
}

元件中的用法

基本翻譯

function Button() {
const { t } = useTranslation()
return <button>{t('Save')}</button>
}

動態標籤

function OrderApplet() {
const { t } = useTranslation()

// useMemo 確保語言變更時重新計算
const actions = useMemo(() => [
{ icon: List, label: t('Orders') },
{ icon: Plus, label: t('Create Order') },
], [t]) // 依賴 t 確保重新計算

return <AppletShell actions={actions} />
}

表單驗證訊息

function ValidationError({ field, error }: Props) {
const { t } = useTranslation()

// 錯誤訊息自動翻譯並替換 ${field}
return (
<span className="text-error">
{t(error, { field: t(field) })}
</span>
)
}

語言回退機制

zh-TW (完整標籤)
↓ 找不到
zh (語言部分)
↓ 找不到
en (回退語言)
↓ 找不到
原始 key

範例

i18n.language = 'zh-TW'
i18n.fallback = 'en'

t('Save')
// 1. 查找 zh-TW['save'] → "儲存" ✅

t('Unknown Key')
// 1. 查找 zh-TW['unknown key'] → 無
// 2. 查找 zh['unknown key'] → 無
// 3. 查找 en['unknown key'] → 無
// 4. 回傳 "Unknown Key"

新增語言

步驟

  1. 建立語言檔案

    src/nls/term/ja.ts
    src/nls/message/ja.ts
  2. 更新配置

    // src/conf/config.ts
    i18n: {
    languages: ['en', 'zh-TW', 'ja'],
    }
  3. 更新 LanguageToggle

    const languages = [
    { value: 'en', label: 'English', icon: '🇺🇸' },
    { value: 'zh-TW', label: '繁體中文', icon: '🇹🇼' },
    { value: 'ja', label: '日本語', icon: '🇯🇵' },
    ]

最佳實踐

  1. 使用 useTranslation - 所有需要翻譯的元件都要使用
  2. 保持扁平結構 - 不要使用巢狀 key(如 customer.name
  3. 使用 ${param} 語法 - 不要用字串拼接
  4. 測試多語言 - 切換語言確認顯示正確
  5. 使用 BCP 47 標準 - 語言標籤使用 zh-TWen-US

效能考量

  • 按需載入 - 只載入當前語言 + 回退語言
  • 大小寫不敏感 - 初始化時轉換,執行時 O(1) 查詢
  • 最多 3 次查詢 - zh-TW → zh → en → key

下一步