# 開發故事:從 v0.6 到 v0.15 這份腳本不是一次寫到位的。初版只翻了十幾個字,每看一頁 GitHub 就發現一個新問題。下面是這條路的順序紀錄。 ## 起點 v0.6:能翻幾個字的樣子 使用者給的初版就能跑,但: - 詞庫只有十幾條,多數頁面還是英文 - 用 `setInterval(500ms)` 輪詢檢查 DOM 變動 — 永遠在掃,浪費 CPU - 每個詞建一個獨立 regex,對同一段文字跑 N 次 - SPA 切頁常常翻不到(Turbo 不會重新觸發 `DOMContentLoaded`) ## v0.7:整理地基 先不加詞,把架構改對: - 把所有詞合成一個 `WORD_BOUNDARY_REGEX`,長詞排前面(這個設計之後救了很多次半翻) - 拿掉 `setInterval`,改 `MutationObserver` + `debounce(150ms)` - Hook `history.pushState` / `replaceState`、聽 `turbo:load` — SPA 切頁能補翻 ## v0.8:regex 的第一個坑 使用者貼了側邊欄的 HTML:`62.8k stars`。 重載後發現 `stars` / `watching` / `forks` 都沒翻。 原因一點都不 glamorous — regex 旗標是 `g` 不是 `gi`。詞庫裡寫 `Stars` 大寫,遇到小寫 `stars` 就不配對。 改成 `gi` + 建 `LOWER_MAP` 查表做大小寫不敏感匹配。 同時發現 `Security and quality` 被翻成 `安全 and quality` — 因為只有 `Security` 在詞庫。 解法:整句丟進詞庫。配合「長詞優先」的排序,`Security and quality` 會先被吃掉,不會退化到只匹配 `Security`。 **這個 pattern 之後反覆用**:看到半翻就加整句。 ## v0.9:把數字弄漂亮 使用者想把 `62.8k` 改成更好看的樣子。 標準做法是 `X 萬`(台灣習慣): ``` 62.8k → 6.3 萬 7.8k → 7,800 (< 1 萬就千分號) 1.2M → 120 萬 ``` 寫了 `formatAbbrev`,加一條 `ABBREV_REGEX = /\b(\d+(?:\.\d+)?)([kKmM])\b/g`。 實作發現沒被吃到 — 因為 `62.8k` 包在 `` 裡,而 `` 不在 SCAN_SELECTORS 裡。 補了 `strong, b, em, h1-h4, li`。**但這是治標**(之後 v0.12 會把整個 SCAN 白名單砍掉)。 ## v0.10:Counter 的抉擇 按鈕上的 `7.8k` / `62.8k` 還是沒格式化。 追到原因:這些數字在 `` 裡,而 `Counter` 在我的 skip 清單。 當初加進 skip 是怕翻到純數字的計數 badge(例如 `Issues 5`)。 但仔細想 — regex 只匹配有 k/M 後綴的數字,純 `5` 本來就不會被動。 所以 Counter 根本不需要 skip。拿掉。 > 教訓:skip 規則是雙刃,加之前多想一步「它真的會被誤翻嗎」。 ## v0.11:Branches 頁的一片荒蕪 使用者貼了 `/branches` 的截圖:Default、Active、Stale、All、Updated、Behind、Ahead 全沒翻,還看到「Active 分支清單」這種慘劇(`Branches` 被翻了但 `Active branches` 沒整句)。 - 補了一堆詞 - 把 `Branches` 的翻譯從 `分支清單` 改成 `分支` — 中文無複數差異,統一成單字更乾淨 - 加整句 `Active branches` → `活躍分支` 然後處理相對時間,寫了 `TIME_AGO_REGEX`: ```js /\b(\d+)\s+(second|minute|hour|day|week|month|year)s?\s+ago\b/gi ``` `3 days ago` → `3 天前`。順手加了 `yesterday` / `last month` 等短語。 ## v0.12:白名單策略徹底失敗 下一張截圖又是 Branches 頁:`Branch`(表頭)、`Updated`、`Check status`、`Pull request`、`last month`(在 `` 裡)、`Search branches...`(placeholder)全都沒翻。 檢視原因 — 這些文字在 ``、``、``、`` 等標籤裡,而我的 `SCAN_SELECTORS` 是寫死的白名單:`span, a, button, summary, strong, b, em, h1-h4, li`。 每次 GitHub 改用新標籤我就要補 — 這條路走不下去。 **大改**: - 拿掉 SCAN_SELECTORS,改 `TreeWalker(NodeFilter.SHOW_TEXT)` 掃所有文字節點 - `shouldSkipTextAncestor` 走 parent chain,祖先命中 SKIP_TAGS 就跳 - 把屬性翻譯從文字節點跳過邏輯解耦 — `` 文字不翻,但 `placeholder` 翻 這次改動是全文重構最大的一次。之後再也沒為了「某個標籤翻不到」而補白名單。 ## v0.13:`of` 的誘惑 補 Branches 空狀態(`No branches match the search`)整句翻譯。 差點加 `of` → `/共`,寫到一半停下來想:「Member of organization」「one of many」「out of date」這些都會爆。 刪掉。`Showing` / `Page` 也一樣,太通用的詞只會製造誤傷。 > 原則:只有在能保證不誤傷的情況下才加單字;有歧義的留給整句處理。 ## v0.14:使用者內容入侵 使用者翻進 repo 主頁,傻眼: ``` 授權 ← LICENSE 自述檔.md ← README.md 更新.sh ← update.sh Initial 提交 ← Initial commit 全部 IN ONE Hacking Tool For Hackers ← All IN ONE... ``` 檔名、commit 訊息、repo 描述**都是使用者自己產生的內容**,一個字都不該動。 但這些內容在 DOM 上沒有 `` 包起來(那是我 skip 的 tag),跟 UI 的 `` 長得一樣。 新增一條 `SKIP_CONTEXT_SELECTOR`,用 `el.closest()` 判斷祖先脈絡: - `.react-directory-filename-column`、`a[href*="/blob/"]`、`a[href*="/tree/"]` — 檔名 - `a[href*="/commit/"]`、`[data-testid="latest-commit-details"]` — commit 訊息 - `.BorderGrid-cell > p`、`.f4.my-3`、`[itemprop="about"]` — repo 描述 - `a.topic-tag` — topic 標籤 - `.markdown-body` — 渲染後的 README - `.blob-code-inner`、`.highlight`、`.react-code-lines` — 程式碼檢視 - `.markdown-title`、`.js-issue-title` — Issue / PR 標題 這是整份腳本**最重要的一次設計決策**:從「列出要翻什麼」轉向「列出不要翻什麼」。 GitHub UI 裡能翻的文字變動慢,使用者內容的容器類型變動也慢,反而中間那層 React 組件 class 變動最快 — 避開最穩的兩端。 ## v0.15:Issues 頁的三重災難 /issues 頁一次暴露三類問題: **1. 搜尋語法被翻,破壞功能** `is:issue state:開啟中` — `Open` tab 的翻譯污染了 `state:open` 這個過濾器語法。按下搜尋直接拋錯。 加 `[role="searchbox"]`、`[role="combobox"]`、`.pl-c1`(syntax highlight token class)等到 skip context。 **2. 使用者名稱被翻** `karanraza608-星標` — 使用者 username 本來是 `karanraza608-stars`,結尾 `stars` 被詞庫吃掉。 加 `a[data-hovercard-type="user"]`、`a[data-hovercard-url*="/users/"]`。 **這個 selector 的選擇很關鍵** — GitHub 對任何會顯示使用者浮動卡的連結都會放 `data-hovercard-*`,穩定度高。 **3. 下拉選單半翻** `排序 by` / `Last 更新時間` — 又是經典半翻。 `Sort by`、`Last updated` 整句丟進去。 另外在考慮要不要加 `opened` / `closed` 這些動詞(`#654 opened 2 days ago`)時踩到一個限制 — `Closed` 是 tab 名稱翻成「已關閉」,而 `closed` 當動詞應該是「關閉於」。由於我的 regex 大小寫不敏感,同字只能有一種翻譯。只好不翻動詞。 ## 累積下來的幾個心得 1. **白名單會吃屎**。規則導向的東西用黑名單(skip)比白名單(scan)更好維護。 2. **使用者內容 vs. UI 的邊界靠語意屬性**。`role` / `data-testid` / `data-hovercard-*` / `href` pattern 比 class 穩。 3. **regex 的 `\b` 碰到標點會失敗**。詞庫別放結尾帶 `...`、`?` 的 key。 4. **半翻用整句修**。別妄想加個連接詞解決 — 直接加整句,讓長詞優先吃掉。 5. **大小寫不敏感有代價**。同字不同語境只能二選一,選使用頻率高的那個。 6. **一次只解一類問題**。每次使用者丟截圖就只修該截圖上的問題,不提前設計沒確認的需求。 ## 接下來可能的方向 - Dark / light 主題感知的時間格式(目前沒差別) - 讓使用者在擴充設定介面覆寫詞庫,不用改腳本原始碼 - 把 `TRANSLATION_MAP` 拆成獨立 JSON,腳本動態載入 - GitHub Enterprise 的 `@match` 範本 但這些不急。現版已經在實際瀏覽可接受。