三份文件定位不重疊: - README.md:參考手冊(功能、架構、規則對照表、相容性) - QUICKSTART.md:操作指南(安裝、自訂翻譯、自訂跳過、FAQ) - SUMMARY.md:開發故事線(v0.6→v0.15 順序敘事、設計心得)
7.9 KiB
開發故事:從 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:<strong>62.8k</strong> 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 包在 <strong> 裡,而 <strong> 不在 SCAN_SELECTORS 裡。
補了 strong, b, em, h1-h4, li。但這是治標(之後 v0.12 會把整個 SCAN 白名單砍掉)。
v0.10:Counter 的抉擇
按鈕上的 7.8k / 62.8k 還是沒格式化。
追到原因:這些數字在 <span class="Counter"> 裡,而 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:
/\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(在 <relative-time> 裡)、Search branches...(placeholder)全都沒翻。
檢視原因 — 這些文字在 <th>、<td>、<relative-time>、<input> 等標籤裡,而我的 SCAN_SELECTORS 是寫死的白名單:span, a, button, summary, strong, b, em, h1-h4, li。
每次 GitHub 改用新標籤我就要補 — 這條路走不下去。
大改:
- 拿掉 SCAN_SELECTORS,改
TreeWalker(NodeFilter.SHOW_TEXT)掃所有文字節點 shouldSkipTextAncestor走 parent chain,祖先命中 SKIP_TAGS 就跳- 把屬性翻譯從文字節點跳過邏輯解耦 —
<input>文字不翻,但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 上沒有 <code> 包起來(那是我 skip 的 tag),跟 UI 的 <a> 長得一樣。
新增一條 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 大小寫不敏感,同字只能有一種翻譯。只好不翻動詞。
累積下來的幾個心得
- 白名單會吃屎。規則導向的東西用黑名單(skip)比白名單(scan)更好維護。
- 使用者內容 vs. UI 的邊界靠語意屬性。
role/data-testid/data-hovercard-*/hrefpattern 比 class 穩。 - regex 的
\b碰到標點會失敗。詞庫別放結尾帶...、?的 key。 - 半翻用整句修。別妄想加個連接詞解決 — 直接加整句,讓長詞優先吃掉。
- 大小寫不敏感有代價。同字不同語境只能二選一,選使用頻率高的那個。
- 一次只解一類問題。每次使用者丟截圖就只修該截圖上的問題,不提前設計沒確認的需求。
接下來可能的方向
- Dark / light 主題感知的時間格式(目前沒差別)
- 讓使用者在擴充設定介面覆寫詞庫,不用改腳本原始碼
- 把
TRANSLATION_MAP拆成獨立 JSON,腳本動態載入 - GitHub Enterprise 的
@match範本
但這些不急。現版已經在實際瀏覽可接受。