Node.js 多語系檔案

在 Node.js 專管語系檔案時,通常會使用物件格式來儲存字串。要在字串中嵌入變數,最常見的做法是使用大括號作為佔位符,並配合取代函數或是 i18next 這類套件來處理。

基礎實作方式

如果不使用外部套件,可以定義一個簡單的取代函式。在 translations.ts 中,字串會長這樣:

TypeScript

export const translations = {
  welcome: "你好,{name}!歡迎回來。",
  items: "你有 {count} 個待辦事項。"
};

接著撰寫一個處理邏輯,將大括號內的標記替換為實際數值:

TypeScript

function t(key: keyof typeof translations, variables: Record<string, string | number>) {
  let message = translations[key];
  
  Object.keys(variables).forEach(placeholder => {
    const value = variables[placeholder];
    message = message.replace(`{${placeholder}}`, String(value));
  });
  
  return message;
}

const greeting = t("welcome", { name: "小明" });
console.log(greeting); // 輸出:你好,小明!歡迎回來。

使用 i18next 套件

如果你使用的是業界標準的 i18next,語法會稍微不同,它預設支援雙大括號或是特定格式。在語系檔中:

TypeScript

{
  "welcome": "你好,{{name}}!"
}

在程式碼中調用的方式:

TypeScript

import i18next from 'i18next';

i18next.t('welcome', { name: '小明' });

這種方式的好處是套件會自動幫你處理複雜的格式,例如數字千分位或日期格式化。


注意事項

確保變數名稱在字串與程式碼中完全一致。如果翻譯檔是從外部 JSON 匯入的,建議建立一個介面來約束變數名稱,避免因為打錯字導致變數沒有成功被取代,畫面直接顯示出 {name} 本身。


支援多層級物件的翻譯解析函式

這是一個支援多層級物件(例如:common.welcome)且能自動取代變數的進階實作。這種結構在大型 Node.js 專案中非常實用。

translations.ts 語系檔結構

建議將語系內容定義為巢狀物件,並使用 TypeScript 的物件結構來確保型別安全:

TypeScript

export const translations = {
  common: {
    welcome: "你好,{name}!歡迎來到我們的平台。",
    error: "錯誤碼:{code}"
  },
  shop: {
    cart: "你的購物車中有 {count} 件商品。"
  }
};

解析函式實作

這個函式會根據點分隔符號(例如 “common.welcome”)找到正確的字串,再執行變數替換:

TypeScript

type TranslationData = typeof translations;

function getDeepValue(obj: any, path: string) {
  return path.split('.').reduce((prev, curr) => prev && prev[curr], obj);
}

function translate(key: string, params: Record<string, string | number> = {}) {
  let template = getDeepValue(translations, key);

  if (!template || typeof template !== 'string') {
    return key; // 如果找不到對應字串,回傳路徑名稱作為提示
  }

  // 遍歷所有傳入的變數並進行全域替換
  Object.keys(params).forEach(param => {
    const value = params[param];
    const regex = new RegExp(`{${param}}`, 'g');
    template = template.replace(regex, String(value));
  });

  return template;
}

如何使用

你可以在專案中的任何地方呼叫這個函式。這段邏輯模擬了 i18n 工具的基本行為:

TypeScript

const userName = "王小明";
const loginMessage = translate("common.welcome", { name: userName });
console.log(loginMessage); // 輸出:你好,王小明!歡迎來到我們的平台。

const cartMessage = translate("shop.cart", { count: 5 });
console.log(cartMessage); // 輸出:你的購物車中有 5 件商品。

進階建議

如果你的翻譯需求變得更複雜,例如需要處理複數形式(1 item vs 2 items)或性別差異,建議轉向使用 i18next 或 formatjs 這些成熟的函式庫。但對於大多數中小型專案,上述的字串取代邏輯已經足夠應付 90% 的情境。


要在 React 元件中處理這類動態變數,最直覺且不需要改動太多現有程式碼的方法是將 t.hi 視為範本字串,並在渲染時使用 replace 方法。

直接在 JSX 中處理

你可以直接在 <h2> 標籤內對字串進行取代。這種做法最快,適合專案規模較小的情況:

TypeScript

<h2 className="text-2xl font-semibold text-slate-900 mb-1">
  {t.hi.replace('{username}', username)}
</h2>

建立一個簡單的翻譯工具函式

如果你的專案中有很多類似的變數需求,建議寫一個輔助函式處理取代邏輯,避免在 UI 程式碼中到處寫 replace

TypeScript

// 輔助函式定義
const interpolate = (str: string, params: Record<string, string>) => {
  let result = str;
  for (const [key, value] of Object.entries(params)) {
    result = result.replace(`{${key}}`, value);
  }
  return result;
};

// 在元件中使用
<h2 className="text-2xl font-semibold text-slate-900 mb-1">
  {interpolate(t.hi, { username })}
</h2>

將翻譯內容改為函式(推薦做法)

為了確保型別安全並減少字串處理的效能損耗,另一種常見的做法是讓語系檔中的特定項目支援函式:

TypeScript

// 修改語系檔定義
export const translations = {
  zh: {
    // ... 其他內容
    hi: (username: string) => `Hi ${username}!`,
  }
};

// 在元件中使用
<h2 className="text-2xl font-semibold text-slate-900 mb-1">
  {typeof t.hi === 'function' ? t.hi(username) : t.hi}
</h2>

總結建議

  1. 如果只有一兩個地方要用,直接用 replace 即可。
  2. 如果你的校務建言系統規模會持續擴大,建議建立一個 interpolate 工具函式,這樣以後遇到 {date}{count} 等變數也能統一處理。
  3. 檢查你的 t.welcome 是否也包含變數,處理邏輯是一樣的。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *