國際化 (i18n)
AppFuse Web 提供輕量級的國際化解決方案,支援多語言切換、參數插值和語言回退。
架構概覽
核心特點:
- 輕量級(核心 ~3KB)
- 按需載入語言包
- 三層回退機制:
zh-TW→zh→en→ 原文 - 大小寫不敏感的 key
快速開始
在元件中使用
import { useTranslation } from '@appfuse/appfuse-web/utils';
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!', { orderNumber: '12345' });
// → "訂單 12345 創建成功!"
// 位置參數
t('${0} of ${1} items', 5, 10);
// → "5 / 10 項目"
目錄結構
src/nls/
├── types.ts # TypeScript 型別(自動完成)
├── term/
│ ├── zh-TW.ts # 繁體中文術語
│ └── en.ts # 英文術語
├── message/
│ ├── zh-TW.ts # 繁體中文訊息
│ └── en.ts # 英文訊息
└── article/
└── zh-TW.ts # 長文內容(未來使用)
語言檔案格式
Term(術語)
單字或短語,無參數:
// src/nls/term/zh-TW.ts
export default {
'Customer': '客戶',
'Order': '訂單',
'Product': '商品',
'Save': '儲存',
'Delete': '刪除',
'Active': '啟用',
'Inactive': '停用',
};
Message(訊息)
完整句子,可帶參數:
// src/nls/message/zh-TW.ts
export default {
'Order ${orderNumber} created successfully!': '訂單 ${orderNumber} 創建成功!',
'${field} is required': '${field}為必填',
'${field} must be between ${min} and ${max}': '${field}必須介於${min}和${max}之間',
};
API 參考
i18n 物件
import { i18n } from '@appfuse/appfuse-web/utils';
// 設定語言
i18n.language = 'zh-TW';
// 設定回退語言
i18n.fallback = 'en';
// 新增翻譯
i18n.addTranslation('zh-TW', {
'Hello': '你好',
});
// 檢查是否有翻譯
i18n.hasTranslation('zh-TW');
// 配置
i18n.configure({
language: 'zh-TW',
fallback: 'en',
translations: { ... },
});
useTranslation Hook
import { useTranslation } from '@appfuse/appfuse-web/utils';
function MyComponent() {
const { t, language } = useTranslation();
// t: 翻譯函數
// language: 當前語言
return <div>{t('Hello')}</div>;
}
useI18nContext Hook
import { useI18nContext } from '@appfuse/appfuse-web/utils';
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 } from '@appfuse/appfuse-web/utils';
ReactDOM.createRoot(document.getElementById('root')!).render(
<I18nProvider>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<App />
</Provider>
</QueryClientProvider>
</I18nProvider>
);
語言偏好持久化
I18nProvider 自動將語言偏好儲存到 localStorage:
// 自動儲存到 localStorage['selected-locale']
// 優先順序:
// 1. localStorage 儲存的值
// 2. defaultLanguage prop
// 3. i18n.language 配置值
語言切換
LanguageToggle 元件
// src/components/language-toggle.tsx
function LanguageToggle() {
const { language, setLanguage } = useI18nContext();
const [loading, setLoading] = useState<string | null>(null);
const handleChange = async (newLang: string) => {
// 檢查是否已載入
if (!i18n.hasTranslation(newLang)) {
setLoading(newLang);
try {
const translation = await nlsService.fetchTranslation(newLang);
i18n.addTranslation(newLang, translation);
} finally {
setLoading(null);
}
}
setLanguage(newLang);
};
return (
<select value={language} onChange={(e) => handleChange(e.target.value)}>
<option value="zh-TW">繁體中文</option>
<option value="en">English</option>
</select>
);
}
初始化流程
應用程式啟動
// src/App.tsx
async function initializeApp() {
// Step 1: 配置 i18n
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 fallback = await nlsService.fetchTranslation(i18n.fallback);
i18n.addTranslation(i18n.fallback, fallback);
}
}
環境配置
// src/conf/config.ts
export const config = {
i18n: {
language: 'zh-TW', // 預設語言
fallback: 'en', // 回退語言
languages: ['en', 'zh-TW'], // 支援的語言
},
};
元件中的用法
基本翻譯
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.ts
src/nls/message/ja.ts -
更新配置
// src/conf/config.ts
i18n: {
languages: ['en', 'zh-TW', 'ja'],
} -
更新 LanguageToggle
<option value="ja">日本語</option>
最佳實踐
- 使用 useTranslation - 所有需要翻譯的元件都要使用
- 保持扁平結構 - 不要使用巢狀 key(如
customer.name) - 使用
${param}語法 - 不要用字串拼接 - 測試多語言 - 切換語言確認顯示正確
- 使用 BCP 47 標準 - 語言標籤使用
zh-TW、en-US
效能考量
- 按需載入 - 只載入當前語言 + 回退語言
- 大小寫不敏感 - 初始化時轉換,執行時 O(1) 查詢
- 最多 3 次查詢 - zh-TW → zh → en → key