MapleCheng

在浩瀚的網路世界中無限潛水欸少年郎!

0%

Upstream Merge 的隱藏地雷:為什麼你的翻譯會憑空消失

事情是這樣的。

我們有個 fork 自開源專案的 codebase,長期在上面開發客製化功能。每隔一段時間就要把 upstream 的新版本 merge 進來,保持跟主線的同步。流程做了很多次,已經算是熟門熟路。

然後有一次,merge 完、build 過、也部署了。結果測試的時候,某個新功能的介面文字全是英文。

「奇怪,我明明有加翻譯的⋯⋯」

你知道那種感覺。你確定你做了,但結果就是不對。

什麼是 i18n Drift?

Open source 專案通常有個 locales/i18n/ 資料夾,裡面放各語言的翻譯 JSON(或 yaml)。每次 upstream 新增功能,他們會先在英文(en.json)加 key,其他語言可能:

  1. 同步跟上了(理想情況)
  2. 留著等社群貢獻(常態)
  3. 根本沒人管(現實)

如果你的 fork 有自己維護的語言包(比如我們有維護繁體中文),upstream merge 之後,那些 upstream 新增的 key 就會在你的翻譯裡憑空消失——因為 upstream 的 zh-TW.json 根本沒有那些 key,而你 merge 進來的,是 upstream 的版本,不是你們自己維護的版本。

更精確地說:你的翻譯被 upstream 的不完整版本覆蓋了。

那次的事故還原

Merge upstream 的流程大概是:

1
2
3
git fetch upstream
git checkout -b feature/merge-upstream-vX.Y.Z
git merge upstream/main

解完衝突、跑 build、CI 過了就往下走。

問題是,衝突通常發生在業務邏輯的地方——Controller、Service、Migration 等等。翻譯檔因為格式單純(一個大 JSON),merge 工具幾乎都能自動解完,不會跳出來要你處理。

所以我完全沒注意到翻譯被動過。

直到上線才發現,某幾個新 UI 元件的文字回到英文。查了一下,發現 upstream 在這個版本新增了 30 幾個新的 translation key,而他們的 zh-CN.json 裡這些 key 根本不存在——git merge 就直接拿 upstream 的空殼覆蓋掉了。

翻譯同步的正確方式

這件事讓我確立了一個原則:merge upstream 時,翻譯檔要手動比對,不能只靠 git 自動 merge。

實際做法分兩步:

Step 1:找出新增的 keys

1
2
# 看 upstream 這個版本新增了哪些 en.json 的 key
git diff upstream/vX.Y.Z-prev upstream/vX.Y.Z -- locales/en.json

或者更直接,merge 完之後看 en.json 的 diff:

1
git diff HEAD~1 HEAD -- locales/en.json

找出所有新增的 key 之後,去你們維護的語言包裡一個一個補上翻譯。

Step 2:驗證沒有遺漏

可以寫個簡單的腳本比對 en.json 和目標語言 json 之間的 key 差異:

1
2
3
4
5
6
7
8
9
10
11
12
13
import json

with open("locales/en.json") as f:
en = json.load(f)

with open("locales/zh-TW.json") as f:
zh = json.load(f)

missing = set(en.keys()) - set(zh.keys())
if missing:
print(f"Missing {len(missing)} keys:")
for k in sorted(missing):
print(f" {k}: {en[k]!r}")

這個腳本跑一下,缺什麼一目瞭然。

(如果你的翻譯是巢狀結構,腳本要稍微複雜一點,但概念一樣。)

完整的 Upstream Merge Checklist

踩完那次坑之後,我把 upstream merge 的流程整理成一個 checklist。現在每次 merge 都照著跑,再也沒有翻譯突然消失的問題。

            
            flowchart TD
    A[git fetch upstream] --> B[建立 merge 分支]
    B --> C[git merge upstream/main]
    C --> D[解衝突]
    D --> E[同步翻譯檔
補缺少的 keys] E --> F[更新 .env 設定
有新的環境變數嗎?] F --> G[dotnet build / npm build] G --> H{Build OK?} H -- No --> D H -- Yes --> I[跑 swagger codegen / orval] I --> J[前端 pnpm build] J --> K{OK?} K -- No --> D K -- Yes --> L[commit & PR]

步驟解釋:

解衝突 — git 能自動 merge 的讓它跑,有衝突的手動解。重點是不要盲目選「ours」或「theirs」,要理解每個衝突的意義。

同步翻譯 — 這一步最容易被跳過,也是最容易踩坑的地方。用比對腳本驗證,不要靠感覺。

更新 .env — Upstream 新功能有時會帶新的環境變數。看一下 .env.example 的 diff,確認沒有漏掉必要設定。

Build — 這步很多人會做,但重點是要在所有程式碼修改完之後跑,包括翻譯和設定。中途 build 通不代表最後會通。

Swagger / Orval — 如果你的專案前後端是分離的,backend API spec 更新後要重新 generate 前端的 client code,否則前端拿到的型別定義會不對。

前端 Build — 前後端都跑完才是真的確認沒問題。

為什麼這步這麼容易被忽略?

我覺得問題在於「翻譯」這件事在技術人眼裡通常是低優先度的。它不是功能、不是邏輯、不是效能,它只是文字。

加上 git merge 通常能自動處理翻譯檔(因為它是純文字),不會跳出衝突提醒你,所以很自然就被跳過了。

但對使用者來說,介面文字回到英文這件事,跟「功能壞掉」沒什麼兩樣。

這件事教會我:checklist 的存在不是為了聰明人,而是為了人在疲倦或趕時間的時候,還能有最後一道防線。

最後

如果你也在維護一個 fork 並且需要定期 sync upstream,建議把「翻譯同步」這一步明確寫進你的 merge 流程裡。不用複雜,一個簡單的比對腳本 + 加進 merge checklist,就夠避掉大部分的問題。

如果這篇能幫你避開這個踩坑,那我的血淚就沒白流了 😂