問題背景

目前 Radarr 的翻譯社群已提供 zh_TW.json 繁體中文翻譯檔,然而使用者在 UI 設定頁面的語言下拉選單中,只能看到「Chinese」一個選項,且對應到的翻譯是簡體中文 (zh_CN)。

Sonarr、Lidarr 也有同樣的問題。

UI 語言系統架構

要理解問題,必須先了解整個語言選單的資料流向:

Language.All (Language.cs)
    │
    ▼
LanguageController.GetAll()          ← GET /api/v3/language
    │  將 Language.All 序列化
    ▼
Redux Store: state.settings.languages
    │
    ▼
createLanguagesSelector.ts           ← 過濾掉 "Any"
    │
    ▼
UISettingsConnector.js               ← 再過濾掉 "Any", "Unknown", "Original"
    │
    ▼
UISettings.js                        ← 渲染下拉選單

UI 語言選單的內容完全由 Language.cs 的 Language.All 靜態列表決定。

翻譯檔載入流程

UILanguage (int, 存於資料庫)
    │
    ▼
IsoLanguages.Get((Language)UILanguage)
    │  透過 Language 物件找到對應的 IsoLanguage
    ▼
組合識別符:TwoLetterCode + "-" + CountryCode
    │  例如 "zh" + "-" + "CN" → "zh-CN"
    ▼
GetLanguageFileName()
    │  "zh-CN" → replace("-","_") → "zh_cn"
    ▼
GetResourceFilename()
    │  "zh_cn" → "zh_CN.json"
    ▼
載入翻譯文件

現行程式碼的問題點

  • Language.cs
    目前僅有單一 Chinese 常數,未區分繁簡體:

    public static Language Chinese => new Language(10, "Chinese");
  • IsoLanguages.cs
    目前僅定義簡體中文的對應國碼:

    new IsoLanguage("zh", "cn", "zho", "Chinese", Language.Chinese),
  • LanguageParser.cs

    // 正規表達式解析 (line 31)
    (?<chinese>\[(?:CH[ST]|BIG5|GB)\]|简|繁|字幕)
     
    ...
     
    // 標題字串解析 (line 101)
    if (lowerTitle.Contains("mandarin") || lowerTitle.Contains("cantonese") || lowerTitle.Contains("chinese"))
    {
        languages.Add(Language.Chinese);
    }

    CHTBIG5 等專屬繁體標記,與 CHSGB 等簡體標記,全部被歸類為同一個 Language.Chinese,繁簡體通通視為同一語言,無法區分。

  • UiConfigController.cs
    UILanguage 存儲的是整數 Language ID,且驗證邏輯明確要求此 ID 必須存在於 Language.All 中。這意味著在不修改 Language.cs 的情況下,UI 無法新增一個獨立的繁體中文語言選項。

    SharedValidator.RuleFor(c => c.UILanguage)
        .Must(value => Language.All.Any(o => o.Id == value))
        .WithMessage("Invalid UI Language ID");

若僅新增繁體中文的 IsoLanguage

new IsoLanguage("zh", "tw", "zho", "Transition Chinese", Language.Chinese),

若讓繁體中文與簡體中文同時指向 Language.Chinese (id=10)
IsoLanguages.Get() 的實作是:

public static IsoLanguage Get(Language language)
{
    return All.FirstOrDefault(l => l.Language == language);
}

由於底層是 HashSet,迭代順序不保證,Get(Language.Chinese) 只會回傳第一個找到的條目。換言之,選擇 Chinese 後,載入的翻譯究竟是 zh_CN.json 還是 zh_TW.json,結果是不確定的。

此外,IsoLanguages.Find() 的三字母碼查找邏輯亦不支援國碼細分:

else if (langCode.Length == 3)
{
    // 只比對 ThreeLetterCode,完全忽略 CountryCode
    return All.FirstOrDefault(l => l.ThreeLetterCode == langCode);
}

zh-CN 與 zh-TW 的 ThreeLetterCode 均為 zho。當外部來源(例如字幕檔名 movie.zho.srt)傳入三字母碼時,系統無法區分繁簡體,結果取決於 HashSet 的內部順序,屬於不確定行為。
雖然兩字母碼查找具備 CountryCode 對應邏輯,但由於兩個條目都映射至同一個 Language.Chinese,UI 選單中仍只會顯示單一 Chinese 選項。

解決方案評估

方案A: Language.cs 新增 ChineseTraditional

優點

不引入任何新架構概念。Language.cs 的 Lookup 字典以 ID 作為 Key,只要 ID 唯一,靜態初始化不會有任何問題,整個媒體功能鏈完整支援繁體中文。

缺點

  1. Language 枚舉身兼兩職造成語義混淆
    Language 同時被用於「UI 翻譯語言」和「媒體音軌/字幕語言標籤」兩個不同領域。新增 ChineseTraditional 後,它在兩個領域都生效。對大多數人這是好事,但若社群有「繁體字幕、普通話音軌」這類精確表達需求,現有架構本身即無法處理,這是既有設計限制,並非本方案所引入的新問題。
  2. IsoLanguages.cs 三字母碼衝突必須謹慎處理
    zh-CN 與 zh-TW 都屬於 ISO 639-2 的 zho,新增條目時必須妥善處理查找優先順序,避免不確定行為。

方案B:新增 UILocale 字串欄位

讓 UI 翻譯語言選擇完全脫離 Language 枚舉系統,改以字串形式(如 zh-TW)儲存。

優點

  1. UI 翻譯語言與媒體語言枚舉完全解耦
    翻譯語言的選擇不再依賴 Language.All,只要存在對應的 JSON 檔案,即可自動支援;新增翻譯語言無需修改任何 C# 程式碼。
  2. 未來擴展性更高
    任何語言變體(如 zh-HK 粵語、pt-BR 巴西葡萄牙語作為 UI 語言)都可以在不修改後端程式碼的情況下直接支援,只需要添加翻譯 JSON 檔案。

缺點

  1. 引入平行設定欄位,架構雙重複雜
    系統中同時存在兩個語言設定:

    UILanguage: int  → 控制媒體語言偏好 (Language.Chinese id=10)
    UILocale: string → 控制 UI 翻譯語言 ("zh-TW")
    

    兩者責任界線模糊,容易造成維護人員混淆。

  2. 媒體功能完全無法使用繁體中文
    由於沒有新增 Language 枚舉值,以下功能完全不受益:

    • Quality Profile 無法設定「繁體中文」
    • Custom Format 無法條件判斷繁體中文音軌
    • MediaInfo 音軌 zh-TW 仍然被辨識為 Language.Chinese
    • 片名翻譯查詢無法指定繁體中文
  3. 修改量是方案 A 的兩倍以上