> For the complete documentation index, see [llms.txt](https://docs.ipcheck.ing/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.ipcheck.ing/developer/zh-tw/development/i18n.md).

# i18n

翻譯如何運作：單一中央註冊表、按需載入的語言包，以及從第一天就隨附的部分翻譯。

UI 提供的每種語言都在中央註冊表中佔一行。語言選擇器， `<html lang>`、延遲載入對應表、瀏覽器語言匹配與回退鏈都由它推導而來——沒有任何語言資訊會在別處硬編碼。

{% code title="common/locale-registry.js" %}

```js
export const LOCALES = [
  { code: 'en', nativeName: '英文', flag: 'us', apiTag: 'en', htmlLang: 'en', status: '完整' },
  // … 每個已註冊語言一行——檔案本身就是目前清單 …
  { code: 'es-MX', nativeName: '西班牙語（墨西哥）', flag: 'mx', apiTag: 'es', htmlLang: 'es-MX', status: '測試版' },
];
```

{% endcode %}

| 欄位           | 說明                                                                             |
| ------------ | ------------------------------------------------------------------------------ |
| `code`       | UI 代碼 **以及** 套件檔名（`frontend/locales/<code>.json`)                              |
| `nativeName` | 選擇器顯示的名稱，以該語言使用者自己的寫法呈現                                                        |
| `flag`       | 選擇器圖示用的雙字母圓形旗幟代碼                                                               |
| `apiTag`     | 傳給上游地名資料來源的標籤——他們公布的最接近標籤，不一定是 UI 代碼                                           |
| `htmlLang`   | 寫入的 BCP-47 標籤 `<html lang>` — `zh` 宣告 `zh-CN` 因此在日文系統上漢字字形會保持簡體，而 `zh-TW` 宣告自己 |
| `狀態`         | `完整` 或 `測試版` ——請見 [完整與測試版](#full-and-beta)                                     |

註冊表順序也是選擇器渲染的順序。檔案位於 `common/`；前端透過這個薄橋接匯入它 `frontend/utils/locale-registry.js`.

## 設定

此應用使用 **vue-i18n** 的 Composition API 模式。實例建立於 `frontend/locales/i18n.js` ，並以 `legacy: false`，接著註冊於 `main.js`.

在元件中，你會從 `t()` 取出 `useI18n()`:

```vue
<script setup>
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
</script>

<template>
  <p>{{ t('macchecker.Note') }}</p>
</template>
```

沒有任何使用者可見內容是硬編碼的。每個字串都會經過 `t()`.

## 語系檔案

每個語系檔都是一個 JSON 物件，依功能命名空間化。頂層命名空間包括 `nav`, `advancedtools`, `page`, `changelog`，以及每個工具一個—— `macchecker`, `whois`, `dnsresolver`, `censorshipcheck`，等等。

工具的命名空間通常會與其註冊表 slug 相符：

{% code title="frontend/locales/en.json" %}

```json
"macchecker": {
  "Title": "MAC 查詢",
  "Note": "查詢實體位址（MAC 位址）的製造商…",
  "Note2": "請輸入實體位址以開始查詢：",
  "Placeholder": "F0:2F:4B:01:0A:AA",
  "invalidMAC": "無效的實體位址",
  "fetchError": "無法取得查詢結果"
}
```

{% endcode %}

首頁上的卡片說明是分開存放的，位於共用的 `advancedtools` 命名空間中，因為工具註冊表指向的就是 `noteKey` ：

```json
"advancedtools": {
  "MacChecker": "查詢實體位址資訊"
}
```

**`en.json` 是參考版本。** 其他所有套件都帶有完全相同的鍵路徑——不多不少。只有值不同。

### 尚未翻譯的值是 `""`，不是缺少的鍵

一個套件是 `en.json`的完整骨架，而尚未被翻譯的字串會留在檔案中作為 `""`。這正是讓翻譯差異可讀的原因：每一行都是一個 `""` 變成句子，而在「尚未翻譯」與「忘了」之間沒有任何東西能藏起來。

`""` 只是原始碼層級的標記。Vite 外掛（`localeStripPlugin` 於 `vite.config.js`，並由 `stripUntranslated()` 於 `common/locale-pack.js`）會在進入 bundle 的路上把每個套件都跑過一次剝除，因此在執行期，未翻譯的值與不存在的鍵是同一回事——都是留給回退鏈填補的洞。陣列則是全有或全無：只要有一個空項目，整個陣列就會回退，因為刪掉一個元素會讓其餘元素位移。

## 載入是按需進行的

套件從不會一起載入。當時只有四種語言時，把它們全都預先打包會多出約 44 KB gzipped 的死重——每次頁面載入都會帶上除啟用語系以外的所有語系，而之後新增的每一種語言只會讓情況更糟。

相反地 `frontend/locales/i18n.js` 會透過 glob 發現套件，並讓註冊表決定 UI 提供哪些：

{% code title="frontend/locales/i18n.js" %}

```js
const localePacks = import.meta.glob('./*.json');
const localeLoaders = Object.fromEntries(
  LOCALE_CODES
    .filter((code) => localePacks[`./${code}.json`])
    .map((code) => [code, localePacks[`./${code}.json`]]),
);
```

{% endcode %}

因此，只加一個套件檔案並不足以讓語言出現；而註冊表裡有一行、卻沒有套件，測試就會失敗。兩者必須一致。

i18n 實例起始時帶有 **空的** 訊息。 `loadActiveLocaleMessages()` 會注入目前語系的整條回退鏈（並行、已記憶化），並且 `main.js` 在掛載前先等待它完成——所以第一次渲染時就已經是翻譯過的。切換語言會持久化選擇並重新啟動應用，這表示每次頁面載入時，永遠只有一個語系處於啟用狀態。

### 回退鏈

`fallbackChain()` 在註冊表中，是缺失字串會沿著走訪路徑的唯一定義： **該語系 → 其基礎語言（如果該基礎語言已註冊）→ `en`.**

* `fr` → `en`
* `zh-TW` → `zh` → `en`

vue-i18n 只能回退到實際存在於實例中的訊息，這就是為什麼 `loadActiveLocaleMessages()` 會載入整條鏈，而不只是啟用中的語系。隱私文案與安全檢查清單會走同一條鏈，只是一次一整個檔案。

刻意關閉了缺少鍵與回退警告：一個測試版套件沿著鏈回退是受支援的狀態，不是錯誤。

### 語言如何被選定

`setLanguage()` 於 `frontend/locales/i18n.js` 依序解析：

1. **儲存的偏好設定** 於 `localStorage`，前提是它所指明的語系有隨附套件。
2. **`?hl=` 查詢參數。**
3. **瀏覽器語言** (`navigator.language`).
4. **`en`.**

後兩者會經過 `matchLocale()`，它會以三個步驟解析 BCP-47 標籤—— **先精確比對，再比對基礎語言，最後比對同一語系中任何已註冊的語系**，由註冊表順序決定兄弟之間的先後。 `?hl=zh-TW` 會精確匹配， `?hl=zh-CN` 沒有精確匹配，會落到基礎語言 `zh`，而一個 `pt-PT` 瀏覽器會落到 `pt-BR` 套件，依語系家族而定。儲存的偏好設定會勝過 `?hl=`，所以查詢參數只會幫助尚未做出選擇的訪客。

建置時的 Vite 外掛（`localePreloadPlugin` 於 `vite.config.js`）會複製完全相同的順序——包括三步驟比對與回退鏈——在一小段內嵌 `<head>` script，並在 HTML 還在串流時，為鏈上的每個套件發出一個 `<link rel="modulepreload">` ，讓語系下載與主 bundle 並行，而不是等它完成後才載入。猜錯最多只會浪費一次預載；真正的匯入仍然會決定。

訊息載入後， `updateMeta()` 會設定 `document.documentElement.lang` 來自註冊表的 `htmlLang` 並重新整理 `標題`, `關鍵字`，以及 `描述` meta 標籤，來源為 `page.*` 鍵。

## 子套件

有兩個資料集大到足以不放進主語系套件，而只針對啟用中的語系按需載入：

<table><thead><tr><th width="300">子套件</th><th>載入者</th></tr></thead><tbody><tr><td><code>frontend/locales/security-checklist/&#x3C;code>.json</code></td><td><code>SecurityChecklist.vue</code> ——它有自己的 glob，以語系代碼為鍵；這個資料集每種語言約 30 KB gzipped，而且只有那一個工具會讀取它。</td></tr><tr><td><code>frontend/locales/privacy/&#x3C;code>.json</code></td><td><code>PrivacyPolicy.vue</code> ——透過 <code>mergeLocaleMessage()</code> 合併進 i18n，因此 <code>t()</code> 以及 <code>tm()</code> 會正常解析文案。</td></tr></tbody></table>

兩者都採用同一種模式：透過 glob 發現載入器，而沒有自己檔案的語系則會取回回退鏈上第一個有檔案的語系。

{% hint style="warning" %}
**這兩者要嘛整包出貨，要嘛完全不出貨。** 與主套件不同，它們各自以單一檔案載入，裡面沒有逐鍵回退，所以只完成一半的檔案會渲染出半英語的頁面。測試會拒絕一個 `""` 出現在其中任何一個。
{% endhint %}

{% hint style="info" %}
**何時新增子套件：** 資料集很大、只屬於某一個延遲載入的畫面，而且否則就會待在每個訪客的首次繪製 bundle 中。一般 UI 字串永遠放在主套件裡。
{% endhint %}

## 完整與測試版

`狀態` 在註冊表中決定一種語言要被要求達到多少完整度。

| 項目            | `完整`                    | `測試版`             |
| ------------- | ----------------------- | ----------------- |
| 主套件           | 每個值都已翻譯——一個 `""` 會使建置失敗 | 任意數量的 `""`        |
| 隱私文案 · 安全檢查清單 | 兩者皆必須，且都要完整             | 可選——但若存在就必須完整     |
| 變更記錄歷史        | 每個項目都必須                 | 免除                |
| 在選擇器中         | 一般                      | 帶有一個小型 **測試版** 徽章 |

每個已註冊的語系都屬於其中之一，而 `common/locale-registry.js` 是目前哪個語系屬於哪種狀態的清單。新的語言一開始是 `測試版`；要把它升級是維護者的決定，會在語言真正完成而且文案變更持續落入其中時進行。

{% hint style="danger" %}
**任何會曝光文案的變更都會落入每個 `完整` 語系的同一次變更中。** 不是後續 PR，也不是 TODO。這表示一個新鍵會進入所有語系，被改寫的字串會在所有語系中都改寫，被刪除的鍵會從所有語系中刪除，而隨附的 `changelog.json` 條目會帶上它們全部的翻譯。
{% endhint %}

回退鏈意味著缺口會退化成英文，而不是顯示原始鍵路徑——這正是為什麼在 `完整` 語系中的部分覆蓋很容易在審查時被漏看。不要依賴它；下面的測試才是真正會抓到它的東西。

## 工具

三個指令，全都是放在 `scripts/`:

| 指令                     | 作用                                                                                                             |
| ---------------------- | -------------------------------------------------------------------------------------------------------------- |
| `pnpm i18n-new <code>` | 為語言建立骨架：一個全`""` 的骨架 `en.json`，以及一個 `status: '測試版'` 註冊表行。 `--privacy` / `--checklist` 建立這兩個可選檔案的骨架（兩者都需要先註冊語言）。 |
| `pnpm i18n-status`     | 每種語言、每個資料集的進度報告——翻譯百分比、下一批要處理的鍵，以及與英文字節完全相同的值。永遠不會失敗； `--locale` 以及 `--limit` 縮小範圍。                            |
| `pnpm i18n-sync`       | 讓每個套件與 `en.json` 在英文變更後重新對齊：把新鍵加入為 `""`，刪除英文已不存在的鍵，並恢復英文的鍵順序。它永遠不會覆蓋翻譯，而對於兩個可選檔案，它只會 *回報* 哪些內容移動了。             |

## 新增語言

這是前端變更，而 **部分** 翻譯是受歡迎的 PR——任何保留為 `""` 都會回退到英文，所以一個裡面有一百個字串的套件在第一天就有用，之後也可以透過幾個 PR 慢慢擴充。

```bash
pnpm i18n-new es-MX
```

這會寫出語言所需的兩個檔案—— `frontend/locales/es-MX.json` 以及一行在 `common/locale-registry.js`。除此之外沒有其他變更：沒有後端變更、沒有建置設定、沒有元件修改。

在你選擇代碼之前，有兩個命名規則值得先知道：語言的 **預設變體使用裸代碼** (`zh` （簡體中文就是如此），而 **後來加入的變體則使用地區代碼** (`zh-TW`, `pt-BR`），之後會把基礎語言當作回退。

{% content-ref url="/pages/26f3a530bd14e146465028890e72f4e4f34c2636" %}
[MyIP 翻譯](/developer/zh-tw/contributing/translating.md)
{% endcontent-ref %}

完整的貢獻者導覽——挑選 `apiTag`、哪些是 `pnpm test` 逐行強制檢查、無論你的套件多完整都仍會看起來像未翻譯的東西——都在那裡。

## 變更記錄

發行說明位於 `frontend/data/changelog.json`，不在語系檔案中。這個檔案是一個版本區塊陣列， **最舊在前** ——About 面板會反向渲染，所以新項目會附加到最後一個區塊。

{% code title="frontend/data/changelog.json" %}

```json
{
  "version": "v7.2.0",
  "date": "測試版",
  "content": [
    {
      "type": "add",
      "change": {
        "en": "全面重構封鎖測試：查看一個網站在全球哪些地方被封鎖",
        "zh": "全面重構封鎖測試，可以查詢一個網站在全球的封鎖情況",
        "zh-TW": "全面重構封鎖測試，可以查詢一個網站在全球的封鎖情況",
        "fr": "Refonte complète du test de censure : découvrez où un site est bloqué dans le monde",
        "ru": "Полностью переработана проверка цензуры: видно, где в мире сайт заблокирован"
      }
    }
  ]
}
```

{% endcode %}

形狀規則：

| 欄位        | 規則                                 |
| --------- | ---------------------------------- |
| `version` | 字串比對 `vX.Y…`                       |
| `date`    | ISO `YYYY-MM-DD`，或是預留字串 `"測試版"`    |
| `content` | 非空的變更項目陣列                          |
| `type`    | 以下其中之一 `新增`, `改善`, `修正`            |
| `變更`      | 一個非空字串，用於 **每個 `完整` 語系**；Beta 語系除外 |

將一百多筆歷史條目倒譯回來，是第一次翻譯 PR 最大的阻力，因此 Beta 語言才被豁免——這也是為什麼補齊回填是升級為 `完整`.

## 由測試強制執行

六個規格涵蓋上述部分，而且它們會在每個 `pnpm test`.

| 規格                              | 涵蓋                                                                                                                                    |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `tests/locale-packs.test.js`    | 嚴格門檻。登錄表與檔案一致；每個套件都恰好有 `en`的鍵；預留位置（`{count}`, `{name}`）是英文鍵的子集； `slug` 以及 `優先順序` 在檢查清單中保持未翻譯；沒有 `""` 在這兩個可選檔案中；沒有 `""` 在任何……中 `完整` 語系 |
| `tests/locale-registry.test.js` | 條目結構，以及共用對應—— `apiTag`, `htmlLang`、回退鏈、標籤比對                                                                                           |
| `tests/locale-pack.test.js`     | 這個 `""` 慣例的執行期部分：扁平化，以及建置時移除對物件與陣列的作用                                                                                                 |
| `tests/changelog.test.js`       | 變更記錄的結構，以及每個 `完整` 語系都有翻譯。此外，語系檔不再包含 `changelog.versions`，但仍保留 `changelog.Title` / `新增` / `改善` / `修正` UI 標籤                            |
| `tests/persona-i18n.test.js`    | 深入 Persona 檢查的詞彙——在 `frontend/utils/persona/check-ids.js` 中宣告的檢查 ID、判定、等級、維度、不適用原因與詳細鍵——在每個 `完整` 套件中都有文案，且沒有任何遺漏                      |
| `tests/index-html-i18n.test.js` | `index.html`的三處手動維護內嵌副本（啟動畫面妙語、JSON-LD、語言選擇器）與登錄表一致                                                                                   |

在新增語言時，最後那一項值得留意：Vue 啟動前的載入畫面有自己一小組字串，因為它是在應用程式和其 bundle 存在之前執行。Beta 語言在那裡會依設計回退到英文。

變更記錄規格也會守住一個值得記住的拆分： **發行說明文字位於 `changelog.json`；其周圍的徽章標籤與面板標題則留在語系檔中** 作為一般 UI 裝飾。

## 後端不參與

哪些語言的 **UI** 隨產品一同出貨，是前端的事。上游哪些語言的 **資料** 進來是另一組，由來源端負責——而且沒有任何處理器會驗證 `?lang` 是否與任何項目相符。

* `/api/maxmind` 將原始標籤傳遞給 `lookupMaxMind()`，它會將其正規化到已隨產品出貨的 City 資料庫實際提供的語言上（`SUPPORTED_LANGS` 於 `common/maxmind-service.js`: `de`, `en`, `es`, `fr`, `ja`, `pt-BR`, `ru`, `zh-CN`）並使用相同的「精確 → 基底 → 語系族」比對，最後回退到 `en`.
* 私有 IPCheck.ing API 的代理會原封不動轉送呼叫端的標籤；其上游自行負責解析。

前端送出的標籤是登錄表中的 `apiTag`，不是 UI 程式碼——這正是該欄位存在的全部原因。 `zh-TW` 送出 `zh-CN`，因為沒有任何上游提供繁體中文地名，所以繁體中文 UI 會顯示簡體中文地名。即使連相近的標籤都不存在時，地名也會以英文顯示在已完整翻譯的 UI 旁，這是預期行為，不是錯誤。國家名稱、日期與數字不受影響：它們來自瀏覽器自身的 `Intl` 資料，並可為任何語言在地化。

## 下一步

* [翻譯 MyIP](/developer/zh-tw/contributing/translating.md) ——給新增或改進語言的貢獻者導覽。
* [新增工具](/developer/zh-tw/development/adding-a-new-tool.md) ——i18n 步驟在完整功能中的位置。
* [測試](/developer/zh-tw/development/testing.md) ——以及其他 `pnpm test` 強制規範的內容。
* [如何貢獻](/developer/zh-tw/contributing/how-to-contribute.md) ——PR 預期。


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.ipcheck.ing/developer/zh-tw/development/i18n.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
