Files
github-zh-tw/SUMMARY.md
Timmy 5d48265bb2 docs: 新增 README / QUICKSTART / SUMMARY 三份文件
三份文件定位不重疊:
- README.md:參考手冊(功能、架構、規則對照表、相容性)
- QUICKSTART.md:操作指南(安裝、自訂翻譯、自訂跳過、FAQ)
- SUMMARY.md:開發故事線(v0.6→v0.15 順序敘事、設計心得)
2026-04-22 17:12:33 +08:00

7.9 KiB
Raw Permalink Blame History

開發故事:從 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.8regex 的第一個坑

使用者貼了側邊欄的 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.10Counter 的抉擇

按鈕上的 7.8k / 62.8k 還是沒格式化。 追到原因:這些數字在 <span class="Counter"> 裡,而 Counter 在我的 skip 清單。 當初加進 skip 是怕翻到純數字的計數 badge例如 Issues 5)。

但仔細想 — regex 只匹配有 k/M 後綴的數字,純 5 本來就不會被動。 所以 Counter 根本不需要 skip。拿掉。

教訓skip 規則是雙刃,加之前多想一步「它真的會被誤翻嗎」。

v0.11Branches 頁的一片荒蕪

使用者貼了 /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 ago3 天前。順手加了 yesterday / last month 等短語。

v0.12:白名單策略徹底失敗

下一張截圖又是 Branches 頁:Branch(表頭)、UpdatedCheck statusPull requestlast 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_SELECTORSTreeWalker(NodeFilter.SHOW_TEXT) 掃所有文字節點
  • shouldSkipTextAncestor 走 parent chain祖先命中 SKIP_TAGS 就跳
  • 把屬性翻譯從文字節點跳過邏輯解耦 — <input> 文字不翻,但 placeholder

這次改動是全文重構最大的一次。之後再也沒為了「某個標籤翻不到」而補白名單。

v0.13of 的誘惑

補 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-columna[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.15Issues 頁的三重災難

/issues 頁一次暴露三類問題:

1. 搜尋語法被翻,破壞功能 is:issue state:開啟中Open tab 的翻譯污染了 state:open 這個過濾器語法。按下搜尋直接拋錯。 加 [role="searchbox"][role="combobox"].pl-c1syntax 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 byLast 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 範本

但這些不急。現版已經在實際瀏覽可接受。