跳至主要内容

國際化 (i18n)

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

架構概覽

核心特點

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

新增語言

步驟

  1. 建立語言檔案

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

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

    <option value="ja">日本語</option>

最佳實踐

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

效能考量

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

下一步