init: GitHub 中文化 (繁體) userscript v0.15

This commit is contained in:
2026-04-22 17:00:35 +08:00
commit 9ea401ac1d
2 changed files with 629 additions and 0 deletions

158
DEVELOPMENT.md Normal file
View File

@@ -0,0 +1,158 @@
# GitHub 中文化 (繁體) UserScript — 開發紀錄
一份給 ScriptCat / Tampermonkey 用的 GitHub 介面繁體中文化腳本,檔案在 `github-zh-tw.user.js`。本文件紀錄從 v0.6 一路迭代到 v0.15 的過程、遇到的坑與解法。
## 設計原則
1. **只翻 GitHub 介面本身,不動使用者內容。** 檔名、commit 訊息、issue 標題、使用者名稱、topic 標籤、repo 描述、README、程式碼一律保留原樣。
2. **不破壞功能。** 搜尋語法(`is:issue state:open`)必須維持英文,否則點擊篩選會失效。
3. **SPA 友善。** GitHub 用 Turbo/pushState切頁不會重載腳本需要自己監聽。
4. **效能優先。** 預編譯 regex、debounce、只處理必要節點。
## 架構概念
三層替換:
```
text
├─ TIME_AGO_REGEX "3 days ago" → "3 天前"
├─ WORD_BOUNDARY_REGEX 詞庫批次替換(長詞優先、大小寫不敏感)
└─ ABBREV_REGEX "62.8k" → "6.3 萬"
```
兩層跳過:
```
shouldSkipTextAncestor(el)
├─ SKIP_TAGS SCRIPT / STYLE / CODE / PRE / INPUT / TEXTAREA ...
├─ isContentEditable / role="textbox"
└─ SKIP_CONTEXT_SELECTOR closest() 快速判斷使用者內容脈絡
```
掃描策略:`TreeWalker` 走所有文字節點 + `querySelectorAll(ATTR_SELECTOR)` 補翻屬性。
## 版本演進
### v0.6(初版)
使用者提供,基本能翻導覽列。用 `setInterval(500ms)` 輪詢 + MutationObserver flag。
**問題:** 詞庫太少、每個詞跑一次 regex 效能差、沒處理 SPA。
### v0.7
- 擴充詞庫Settings、Pulls、Commits、Branches…
- 使用預編譯的 `WORD_BOUNDARY_REGEX` 一次替換
- 拿掉 setInterval 輪詢,改 MutationObserver + debounce
- Hook `pushState` / `replaceState`、監聽 `turbo:load`
### v0.8
**Bug** `stars` / `watching` / `forks` 小寫沒被翻。
**原因:** regex 只有 `g``i` 旗標。
**修:**`gi`,搭配 `LOWER_MAP` 做大小寫不敏感查表。
**Bug** `Security and quality``安全 and quality`
**原因:** 只有 `Security` 在詞庫,`and quality` 不在。
**修:** 加整句 `Security and quality``安全與品質`。長詞優先排序會先吃掉整句。
### v0.9(數字格式化)
使用者要求「62.8k」改成更好看的格式。
新增 `formatAbbrev`
- `≥ 1 萬``X.X 萬`≥100 萬去掉小數)
- `< 1 萬` → 千分號 `7,800`
- 純數字不動
**額外問題:** `<strong>62.8k</strong>` 沒被掃到。
**原因:** SCAN_SELECTORS 沒包 `<strong>`
**修:** 加入 `strong, b, em, h1-h4, li`
### v0.10
**Bug** 按鈕上的 `7.8k` / `62.8k`(在 `<span class="Counter">` 裡)沒格式化。
**原因:** `Counter` 在 skip 清單。
**修:** 移除 `Counter` skip。純數字本來就不會被誤翻`k/M` 後綴才會處理。
### v0.11(分支頁)
Branches 頁大量詞彙沒翻Default、Active、Stale、All、Updated、Check status、Behind、Ahead 等。
也加了相對時間 regex
```js
TIME_AGO_REGEX = /\b(\d+)\s+(second|minute|hour|day|week|month|year)s?\s+ago\b/gi
```
**修 `Active 分支清單` 的半翻:**`Branches``分支`(中文無複數差異),加整句 `Active branches``活躍分支`
### v0.12TreeWalker 重構)
**Bug** `<th>` / `<td>` / `<relative-time>` 的文字沒翻,`<input>` 的 placeholder 也沒翻。
**原因:** SCAN_SELECTORS 白名單太窄;`<input>` 整個被 SKIP_TAGS 跳過,連屬性都沒碰。
**大改:**
1. 拿掉 SCAN_SELECTORS改用 `TreeWalker` 走所有文字節點
2. 屬性翻譯獨立於文字節點跳過邏輯 — `<input>` 文字跳過但 placeholder 仍翻
3. `shouldSkipTextAncestor` 改成走 parent chain比單一元素判斷更穩
### v0.13
補空狀態整句:`No branches match the search``沒有分支符合搜尋條件` 等。
**教訓:** 差點加了 `of``/共`,但 `of` 會破壞「Member of X」「out of」等句型移除。同理 `Page``Showing` 也太通用不加。
### v0.14(使用者內容保護)
使用者抱怨檔名、commit 訊息、repo 描述被翻(`授權` / `自述檔.md` / `更新.sh` / `Initial 提交` / `全部 IN ONE Hacking Tool For Hackers`)。
**新增 `SKIP_CONTEXT_SELECTOR`**
- 檔名:`.react-directory-filename-column``a[href*="/blob/"]``a[href*="/tree/"]`
- Commit`a[href*="/commit/"]``[data-testid="latest-commit-details"]`
- Repo 描述:`.BorderGrid-cell > p``.f4.my-3`
- Topics`a.topic-tag`
- README`.markdown-body`
- 程式碼:`.blob-code-inner``.highlight``.react-code-lines`
- Issue / PR 標題:`.markdown-title``.js-issue-title`
`el.closest(SKIP_CONTEXT_SELECTOR)` 一次判斷祖先脈絡。
### v0.15Issues 頁)
多個新問題:
1. `is:issue state:開啟中` — 搜尋語法被翻,**破壞功能**
2. `karanraza608-星標` — 使用者名稱被翻
3. 下拉選單沒翻Sort by / Newest / Oldest…
4. `read the 貢獻 guidelines` — 半翻
5. Dismiss / Want to contribute 沒翻
**新增跳過脈絡:**
- 使用者:`a[data-hovercard-type="user"|"organization"]``a[data-hovercard-url*="/users/"]`
- 搜尋:`[role="searchbox"]``[role="combobox"]`、語法 token `.pl-c1`
- Labels`.IssueLabel``.Label`
**新增詞庫:** `Sort by` / `Order` / `Newest` / `Oldest` / `Created on` / `Last updated` / `Total comments` / `Best match` / `Reactions` / `Recently updated` / `Dismiss` / `Want to contribute to` / 整句 `If you have a bug or an idea, read the contributing guidelines before opening an issue`
## 關鍵技術點
### 大小寫不敏感 + 保留原大小寫?
做不到。因為 `Open` (tab) 和 `opened` (issue meta 動詞) 經過 `gi` + LOWER_MAP 會用同一個翻譯。只能二選一。目前選「保留 tab 翻譯」,動詞保留英文。
### 長詞優先
```js
const sorted = [...TRANSLATION_MAP.keys()]
.sort((a, b) => b.length - a.length)
```
搭配 regex 的回溯backtracking長詞失敗時會嘗試短詞。讓「Active branches」整句優先於「Branches」單字。
### 屬性裡的 `...` 結尾
`Search branches...` 放進詞庫但 `\b...\b` 的尾端 `\b` 不會成立(`.` 是非字元 + 後方結尾也非字元)。解法是詞庫只放 `Search branches`regex 會在 `\b` 前停下,保留後面的 `...`
### React 元件的 key 變動
GitHub 的 React class 常含 hash`SearchInput-module__searchInput_f5Xpk`)。不靠這些,改靠穩定的 `role``data-testid``data-hovercard-*``[href*="/blob/"]`
## 目前已知限制
- **大小寫衝突**:同詞不同語境無法分別翻譯。
- **使用者自定 label / project 名稱**:會看內容決定,但已用 `.IssueLabel` / `.Label` 跳過彩色 label。
- **Issue 標題**:刻意保留英文(屬使用者內容)。
- **新 UI 改版**:如果 GitHub 改變 class / data-testid跳過規則可能失效。
## 檔案
- `github-zh-tw.user.js` — 腳本本體
- `DEVELOPMENT.md` — 本文件
## 安裝
在 ScriptCat / Tampermonkey「新增腳本」貼上 `github-zh-tw.user.js` 全文。或用「從本地檔案匯入」。

471
github-zh-tw.user.js Normal file
View File

@@ -0,0 +1,471 @@
// ==UserScript==
// @name GitHub 中文化 (繁體)
// @namespace https://github.com/scriptscat/scriptcat
// @version 0.15
// @description 幫 GitHub 導覽列、按鈕、屬性換成繁體中文,支援 SPA 動態更新,不亂翻、不卡頓。
// @match https://github.com/*
// @run-at document-idle
// @grant none
// ==/UserScript==
(() => {
'use strict';
const TRANSLATION_MAP = new Map([
// 倉庫導覽
['Code', '原始碼'],
['Issues', '問題'],
['Pull requests', '拉取請求'],
['Pull request', '拉取請求'],
['Pulls', '拉取請求'],
['Discussions', '討論'],
['Actions', '動作'],
['Projects', '專案'],
['Security', '安全'],
['Security and quality', '安全與品質'],
['Code scanning', '程式碼掃描'],
['Secret scanning', '密鑰掃描'],
['Dependabot', 'Dependabot'],
['Insights', '洞察'],
['Pulse', '脈動'],
['Settings', '設定'],
['Wiki', '維基'],
['Overview', '總覽'],
['Repositories', '倉庫'],
['Repository', '倉庫'],
['Followers', '追蹤者'],
// 關注 / 分支 / 星標
['Watch', '關注'],
['Watching', '關注中'],
['Unwatch', '取消關注'],
['Fork', '分支'],
['Forks', '分支數'],
['Forked', '已分支'],
['Star', '星標'],
['Starred', '已標星'],
['Stars', '星標數'],
['Unstar', '取消星標'],
['stars', '星標'],
['watching', '關注中'],
['forks', '分支'],
['Sponsor', '贊助'],
['Sponsors', '贊助者'],
['Sponsor this project', '贊助此專案'],
['Learn more about GitHub Sponsors', '進一步了解 GitHub Sponsors'],
// 倉庫頁面側欄
['About', '關於'],
['Readme', '自述檔'],
['License', '授權'],
['MIT license', 'MIT 授權'],
['Apache-2.0 license', 'Apache-2.0 授權'],
['GPL-3.0 license', 'GPL-3.0 授權'],
['BSD-3-Clause license', 'BSD-3-Clause 授權'],
['Activity', '活動'],
['Custom properties', '自訂屬性'],
['Releases', '版本'],
['No releases published', '尚未發佈版本'],
['Contributors', '貢獻者'],
['Languages', '語言'],
['Packages', '套件'],
['No packages published', '尚未發佈套件'],
['Deployments', '部署'],
['Environments', '環境'],
['Report repository', '檢舉此倉庫'],
['Resources', '資源'],
['Citation', '引用'],
['Cite this repository', '引用此倉庫'],
['Contributing', '貢獻'],
['Code of conduct', '行為準則'],
// 檔案列表操作
['Go to file', '前往檔案'],
['Add file', '新增檔案'],
['Create new file', '新增檔案'],
['Upload files', '上傳檔案'],
['Find file', '搜尋檔案'],
['Clone', '複製'],
['Download ZIP', '下載 ZIP'],
['Open with GitHub Desktop', '以 GitHub Desktop 開啟'],
['Latest commit', '最新提交'],
['Commits', '提交'],
['Commit', '提交'],
['Branches', '分支'],
['Branch', '分支'],
['Default', '預設'],
['Active', '活躍'],
['Stale', '過時'],
['All', '全部'],
['Yours', '我的'],
['Active branches', '活躍分支'],
['Stale branches', '過時分支'],
['All branches', '所有分支'],
['Your branches', '我的分支'],
['Search branches', '搜尋分支'],
['Search branches...', '搜尋分支…'],
['New branch', '新增分支'],
['Delete branch', '刪除分支'],
['Rename branch', '重新命名分支'],
['Restore branch', '還原分支'],
['View all branches', '檢視所有分支'],
['No branches match the search', '沒有分支符合搜尋條件'],
['No branches', '沒有分支'],
['No results matched your search', '沒有結果符合你的搜尋'],
['No results found', '找不到結果'],
['Nothing to show', '沒有可顯示的內容'],
['No commits', '尚無提交'],
['No issues', '沒有問題'],
['No pull requests', '沒有拉取請求'],
['No open issues', '沒有開啟中的問題'],
['No open pull requests', '沒有開啟中的拉取請求'],
['No tags', '沒有標籤'],
['No matches found', '找不到符合項目'],
['Previous', '上一頁'],
['Next', '下一頁'],
['Loading', '載入中'],
['Updated', '更新時間'],
['Check status', '檢查狀態'],
['Behind', '落後'],
['Ahead', '領先'],
['Pull request', '拉取請求'],
['Tags', '標籤'],
['Tag', '標籤'],
// PR / Issue 常見按鈕
['New issue', '開新問題'],
['New pull request', '開新拉取請求'],
['Compare', '比較'],
['Merge', '合併'],
['Merge pull request', '合併拉取請求'],
['Squash and merge', '壓縮後合併'],
['Rebase and merge', '重定基底後合併'],
['Create a merge commit', '建立合併提交'],
['Close issue', '關閉問題'],
['Close pull request', '關閉拉取請求'],
['Reopen issue', '重新開啟問題'],
['Comment', '留言'],
['Review changes', '審查變更'],
['Files changed', '變更檔案'],
['Conversation', '對話'],
['Checks', '檢查'],
// 列表篩選
['Open', '開啟中'],
['Closed', '已關閉'],
['Author', '作者'],
['Labels', '標籤'],
['Label', '標籤'],
['Milestones', '里程碑'],
['Milestone', '里程碑'],
['Assignee', '指派對象'],
['Assignees', '指派對象'],
['Reviewer', '審查者'],
['Reviewers', '審查者'],
['Sort', '排序'],
['Sort by', '排序依據'],
['Order', '順序'],
['Newest', '最新'],
['Oldest', '最舊'],
['Created on', '建立時間'],
['Last updated', '最後更新時間'],
['Most commented', '留言最多'],
['Total comments', '留言總數'],
['Best match', '最佳匹配'],
['Reactions', '反應'],
['Most reactions', '反應最多'],
['Recently updated', '最近更新'],
['Dismiss', '關閉提示'],
['Want to contribute to', '想貢獻給'],
['If you have a bug or an idea, read the contributing guidelines before opening an issue', '若您發現錯誤或有新想法,請先閱讀貢獻指引再開啟問題'],
['read the contributing guidelines', '請閱讀貢獻指引'],
['contributing guidelines', '貢獻指引'],
['before opening an issue', '再開啟問題'],
['Never', '從未'],
['Everyone', '所有人'],
['Anyone', '任何人'],
// 全站導覽 / 使用者選單
['Pull', '拉取'],
['Marketplace', '市集'],
['Explore', '探索'],
['Your profile', '個人頁面'],
['Your repositories', '我的倉庫'],
['Your organizations', '我的組織'],
['Your projects', '我的專案'],
['Your stars', '我的星標'],
['Your gists', '我的程式片段'],
['Your sponsors', '我的贊助者'],
['Upgrade', '升級'],
['Feature preview', '功能預覽'],
['Help', '說明'],
['Sign out', '登出'],
['Sign in', '登入'],
// 通用動作
['Save', '儲存'],
['Cancel', '取消'],
['Delete', '刪除'],
['Edit', '編輯'],
['Update', '更新'],
['Create', '建立'],
['Submit', '送出'],
['Search', '搜尋'],
['Filter', '篩選'],
['Filters', '篩選'],
['Follow', '追蹤'],
['Following', '追蹤中'],
['Unfollow', '取消追蹤'],
['Block or Report', '封鎖或檢舉'],
// 頁尾 / 無障礙
['Footer', '頁尾'],
['Footer navigation', '頁尾導覽'],
['GitHub Homepage', 'GitHub 首頁'],
['Terms', '服務條款'],
['Privacy', '隱私'],
['Status', '狀態'],
['Community', '社群'],
['Docs', '文件'],
['Contact', '聯絡我們'],
['Manage cookies', '管理 Cookie'],
['Do not share my personal information', '不要分享我的個人資訊'],
['Skip to content', '跳至內容'],
// 相對時間
['yesterday', '昨天'],
['today', '今天'],
['tomorrow', '明天'],
['now', '剛剛'],
['just now', '剛才'],
['last week', '上週'],
['last month', '上個月'],
['last year', '去年'],
['this week', '本週'],
['this month', '本月'],
['this year', '今年'],
]);
const TIME_UNITS = {
second: '秒', minute: '分鐘', hour: '小時',
day: '天', week: '週', month: '個月', year: '年',
};
const TIME_AGO_REGEX = /\b(\d+)\s+(second|minute|hour|day|week|month|year)s?\s+ago\b/gi;
const TIME_AGO_ARTICLE_REGEX = /\b(a|an)\s+(second|minute|hour|day|week|month|year)\s+ago\b/gi;
// 這些標籤的文字節點不翻(避免動到原始碼、編輯器);但它們的屬性(如 placeholder仍會翻。
const SKIP_TAGS = new Set(['SCRIPT', 'STYLE', 'CODE', 'PRE', 'TEXTAREA', 'INPUT', 'NOSCRIPT']);
const ATTRIBUTES_TO_TRANSLATE = ['data-content', 'aria-label', 'title', 'placeholder'];
const ATTR_SELECTOR = '[data-content], [aria-label], [title], [placeholder]';
// 這些區塊內的文字節點屬於使用者內容檔名、commit 訊息、倉庫描述、README、程式碼…一律跳過。
const SKIP_CONTEXT_SELECTOR = [
// 檔案列表(新版 React UI
'.react-directory-filename-column',
'.react-directory-commit-message',
'[aria-labelledby="folders-and-files"] [role="rowheader"]',
// 最新 commit 列
'[data-testid="latest-commit-details"]',
'[data-testid="latest-commit"]',
// 任何指向檔案資料夾commitraw 的連結(含裡頭包的 <strong> 等)
'a[href*="/blob/"]',
'a[href*="/tree/"]',
'a[href*="/commit/"]',
'a[href*="/raw/"]',
'a[href*="/releases/tag/"]',
// 倉庫描述About 區塊的敘述段落)
'[data-testid="repository-description"]',
'[itemprop="about"]',
'.BorderGrid-cell > p',
'.f4.my-3',
// Topics 標籤
'a.topic-tag',
'.topic-tag',
'.topic-tag-link',
// 使用者/組織連結(含 hovercard— 避免翻到使用者名稱
'a[data-hovercard-type="user"]',
'a[data-hovercard-type="organization"]',
'a[data-hovercard-url*="/users/"]',
'a[data-hovercard-url*="/orgs/"]',
'a[data-hovercard-url*="/hovercards"]',
// 搜尋框 / 過濾器(含語法高亮 token
'[role="searchbox"]',
'[role="combobox"]',
'form[role="search"]',
'.subnav-search-input',
'.subnav-search-context',
'input[name="q"] ~ *',
'.pl-c1', '.pl-s', '.pl-en', '.pl-k', '.pl-v', '.pl-smi', '.pl-s1',
// Labels使用者自定義 label 顏色塊)
'a.IssueLabel',
'.IssueLabel',
'.Label',
// 倉庫完整名 org/repo 連結
'a[data-octo-click="hovercard-link-click"]',
// README / 渲染後的 markdown
'.markdown-body',
'article.markdown-body',
// 檔案內容、程式碼檢視
'.blob-code-inner',
'.highlight',
'.react-code-lines',
'.react-blob-view-header-sticky',
// Issue / PR 標題與內文
'.markdown-title',
'.js-issue-title',
'.markdown-body .comment-body',
].join(',');
// 預編譯一次:長詞優先、忽略大小寫
const WORD_BOUNDARY_REGEX = (() => {
const sorted = [...TRANSLATION_MAP.keys()]
.sort((a, b) => b.length - a.length)
.map(s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
return new RegExp(`\\b(${sorted.join('|')})\\b`, 'gi');
})();
// 給 regex 匹配結果做大小寫不敏感查表
const LOWER_MAP = new Map(
[...TRANSLATION_MAP.entries()].map(([k, v]) => [k.toLowerCase(), v])
);
// 把 62.8k → 6.3 萬、1.2M → 120 萬、999 以下千分號化
function formatAbbrev(_m, num, unit) {
const mul = unit.toLowerCase() === 'm' ? 1_000_000 : 1_000;
const val = Math.round(parseFloat(num) * mul);
if (val >= 10000) {
const wan = val / 10000;
const s = wan >= 100 ? Math.round(wan).toString() : wan.toFixed(1).replace(/\.0$/, '');
return `${s}`;
}
return val.toLocaleString('en-US');
}
const ABBREV_REGEX = /\b(\d+(?:\.\d+)?)([kKmM])\b/g;
function replaceText(text) {
if (!text) return text;
let out = text;
if (/[A-Za-z]/.test(out)) {
// 先處理 "X units ago" 避免被詞庫部分吃掉
out = out.replace(TIME_AGO_REGEX, (_m, n, u) => `${n} ${TIME_UNITS[u.toLowerCase()]}`);
out = out.replace(TIME_AGO_ARTICLE_REGEX, (_m, _a, u) => `1 ${TIME_UNITS[u.toLowerCase()]}`);
out = out.replace(WORD_BOUNDARY_REGEX, (m) => LOWER_MAP.get(m.toLowerCase()) ?? m);
}
if (ABBREV_REGEX.test(out)) {
ABBREV_REGEX.lastIndex = 0;
out = out.replace(ABBREV_REGEX, formatAbbrev);
}
return out;
}
function shouldSkipTextAncestor(el) {
if (!el) return false;
if (el.closest?.(SKIP_CONTEXT_SELECTOR)) return true;
while (el && el.nodeType === Node.ELEMENT_NODE) {
if (SKIP_TAGS.has(el.nodeName)) return true;
if (el.isContentEditable) return true;
if (el.getAttribute?.('role') === 'textbox') return true;
el = el.parentElement;
}
return false;
}
function translateAttributes(el) {
if (!(el instanceof Element)) return;
for (const attr of ATTRIBUTES_TO_TRANSLATE) {
const val = el.getAttribute?.(attr);
if (!val) continue;
const replaced = replaceText(val);
if (replaced !== val) el.setAttribute(attr, replaced);
}
}
function translateTextNode(node) {
const original = node.nodeValue;
if (!original || !/\S/.test(original)) return;
if (shouldSkipTextAncestor(node.parentElement)) return;
const replaced = replaceText(original);
if (replaced !== original) node.nodeValue = replaced;
}
function translateTree(root) {
if (!root) return;
// 屬性root 自己 + 所有子孫
if (root instanceof Element && root.matches?.(ATTR_SELECTOR)) translateAttributes(root);
root.querySelectorAll?.(ATTR_SELECTOR).forEach(translateAttributes);
// 文字節點:用 TreeWalker 走全部
const scanRoot = root.nodeType === Node.DOCUMENT_NODE ? root.body : root;
if (!scanRoot) return;
const walker = document.createTreeWalker(scanRoot, NodeFilter.SHOW_TEXT, {
acceptNode: (n) => {
if (!n.nodeValue || !/\S/.test(n.nodeValue)) return NodeFilter.FILTER_REJECT;
if (shouldSkipTextAncestor(n.parentElement)) return NodeFilter.FILTER_REJECT;
return NodeFilter.FILTER_ACCEPT;
},
});
const nodes = [];
let n;
while ((n = walker.nextNode())) nodes.push(n);
for (const node of nodes) translateTextNode(node);
}
// debounce短時間內多次變更只翻一次
function debounce(fn, wait) {
let timer = 0;
return () => {
clearTimeout(timer);
timer = setTimeout(fn, wait);
};
}
function init() {
translateTree(document.body);
const scheduleTranslate = debounce(() => translateTree(document.body), 150);
const observer = new MutationObserver((mutations) => {
for (const m of mutations) {
if (m.type === 'attributes' && m.target instanceof Element) {
translateAttributes(m.target);
} else if (m.type === 'childList') {
m.addedNodes.forEach((n) => {
if (n.nodeType === Node.ELEMENT_NODE) translateTree(n);
else if (n.nodeType === Node.TEXT_NODE) translateTextNode(n);
});
}
}
// 保險起見再安排一次全域掃描SPA 會在微任務後才補資料)
scheduleTranslate();
});
observer.observe(document.body, {
subtree: true,
childList: true,
attributes: true,
attributeFilter: ATTRIBUTES_TO_TRANSLATE,
});
// SPA 路由切換後重新掃描
const hookHistory = (type) => {
const orig = history[type];
history[type] = function () {
const ret = orig.apply(this, arguments);
scheduleTranslate();
return ret;
};
};
hookHistory('pushState');
hookHistory('replaceState');
window.addEventListener('popstate', scheduleTranslate);
document.addEventListener('turbo:load', scheduleTranslate);
document.addEventListener('turbo:render', scheduleTranslate);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init, { once: true });
} else {
init();
}
})();