Files
github-zh-tw/github-zh-tw.user.js
Timmy f881124bdf fix: v0.18 — 搜尋語法 state:open 不再被翻(修功能性災難)+ 貢獻 banner 補第一段
搜尋框:新版 React SearchBar 用 <span data-type="filter-expression"> 包住
is:issue state:open,外層 form 只有 role="form" 不是 role="search",
既有 [role="searchbox"] / form[role="search"] 全部抓不到,導致 state:open
被翻成 state:開啟中,點擊會破壞搜尋功能。

新增 skip:
- [data-type="filter-expression"] / [data-type="filter"] / [data-type="filter-value"]
- .styled-input-container

貢獻 banner:整句詞庫條目永遠不匹配(contributing guidelines 在 <a> 裡,
跨不過節點邊界),實際只靠三個片段拼。原本缺前段「If you have a bug or an
idea, read the」對應,所以顯示半翻。補上該片段條目,移除無效的整句條目與
read the contributing guidelines 條目。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 18:03:11 +08:00

495 lines
17 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ==UserScript==
// @name GitHub 中文化 (繁體)
// @namespace https://github.com/scriptscat/scriptcat
// @version 0.18
// @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', '開新拉取請求'],
['New repository', '新增倉庫'],
['Import repository', '匯入倉庫'],
['New codespace', '新增 codespace'],
['New gist', '新增 gist'],
['New organization', '新增組織'],
['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', '想貢獻給'],
// 此橫幅實際是三段文字節點:前段文字 + <a>contributing guidelines</a> + 後段文字。
// 整句條目永遠不會匹配(跨不過 <a> 邊界),只靠三個片段湊。
['If you have a bug or an idea, read the', '若您發現錯誤或有新想法,請先閱讀'],
['contributing guidelines', '貢獻指引'],
['before opening an issue', '再開啟問題'],
['Never', '從未'],
['Everyone', '所有人'],
['Anyone', '任何人'],
// Copilot / 搜尋框
['Ask Copilot', '詢問 Copilot'],
['About this page', '關於此頁'],
['In a space', '在 Space 中'],
['Download for', '下載'],
['Type', '輸入'],
['to search', '搜尋'],
// 全站導覽 / 使用者選單
['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',
// 2026 新版 React UI描述 <p class="f4 tmp-my-3"> 包在 .hide-sm.hide-md 裡,原 `> p` 直接子代選擇器失效。
// 用 p.f4 靠語意f4 = 大型段落文字 = prose 描述),對未來 tmp-* 類別變動有抵抗力。
'p.f4',
// 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',
// 2026 新版 React SearchBarfilter 語法 token 用 data-type 標記is / state / open / issue 等),
// 外層 form 只有 role="form" 不是 role="search",原規則抓不到。
'[data-type="filter-expression"]',
'[data-type="filter"]',
'[data-type="filter-value"]',
'.styled-input-container',
// 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();
}
})();