國際化 (i18n)
AppFuse Web 提供輕量級的國際化解決方案,支援多語言切換、參數插值和語言回退。
架構概覽
核心特點:
- 輕量級(核心 ~3KB)
- 按需載入語言包
- 三層回退機制:
zh-TW→zh→en→ 原文 - 大小寫不敏感的 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"
新增語言
步驟
-
建立語言檔案
src/nls/term/ja.tssrc/nls/message/ja.ts -
更新配置
// src/conf/config.tsi18n: {languages: ['en', 'zh-TW', 'ja'],} -
更新 LanguageToggle
const languages = [{ value: 'en', label: 'English', icon: '🇺🇸' },{ value: 'zh-TW', label: '繁體中文', icon: '🇹🇼' },{ value: 'ja', label: '日本語', icon: '🇯🇵' },]
最佳實踐
- 使用 useTranslation - 所有需要翻譯的元件都要使用
- 保持扁平結構 - 不要使用巢狀 key(如
customer.name) - 使用
${param}語法 - 不要用字串拼接 - 測試多語言 - 切換語言確認顯示正確
- 使用 BCP 47 標準 - 語言標籤使用
zh-TW、en-US
效能考量
- 按需載入 - 只載入當前語言 + 回退語言
- 大小寫不敏感 - 初始化時轉換,執行時 O(1) 查詢
- 最多 3 次查詢 - zh-TW → zh → en → key