-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcontent.js
More file actions
233 lines (209 loc) · 9.2 KB
/
Copy pathcontent.js
File metadata and controls
233 lines (209 loc) · 9.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
// content.js — 整页双语翻译脚本,由 background.js 通过 chrome.scripting.executeScript 注入
// (侧边栏的「翻译整页」按钮触发 RUN_PAGE_TRANSLATE 消息后注入)
// 每次触发都会重新执行本文件,因此用 window 上的标志位防止重复运行/重复插入
(function () {
if (window.__en2zhRunning) return;
window.__en2zhRunning = true;
const SKIP_TAGS = new Set(['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'CODE', 'PRE', 'SELECT']);
const BATCH_MAX_LINES = 8;
const BATCH_MAX_CHARS = 1200;
const DELAY_BETWEEN_BATCHES_MS = 1000; // 批次间隔,适度限速避免触发火山方舟 RPM 限制
const RATE_LIMIT_MAX_RETRIES = 2; // 撞上频控(429)时的重试次数
const RATE_LIMIT_WAIT_MS = 1500; // 每次重试前的等待时间
// 可见性判断:优先用 Chrome 105+ 的原生 checkVisibility(一次调用即可识别
// display:none 祖先链,且远快于 getComputedStyle),旧内核回退原实现;
// 结果按元素缓存,同一父元素下的多个文本节点只判一次
function makeVisibilityChecker() {
const cache = new Map();
const canNative = typeof Element.prototype.checkVisibility === 'function';
return (el) => {
let v = cache.get(el);
if (v === undefined) {
if (canNative) {
v = el.checkVisibility({ checkVisibilityCSS: true });
} else {
const style = window.getComputedStyle(el);
v = style.display !== 'none' && style.visibility !== 'hidden';
}
cache.set(el, v);
}
return v;
};
}
function collectTextNodes(root) {
const nodes = [];
const isVisible = makeVisibilityChecker();
// 单次递归同时收集文本节点与 open 状态的 shadow root,替代原先
// 「TreeWalker 走一遍 + 全页 querySelectorAll('*') 再走一遍」的双重遍历;
// SKIP_TAGS / contenteditable / 插件自身 UI 在元素层整棵剪枝,
// 也免去了每个文本节点一次 closest() 向上查找
function walk(parent) {
for (let node = parent.firstChild; node; node = node.nextSibling) {
if (node.nodeType === Node.TEXT_NODE) {
const text = normalizeText(node.nodeValue);
if (text.length < 2) continue;
// 中英互译:含英文字母或中文字符都值得翻译,纯数字/符号跳过
if (!hasTranslatableChar(text)) continue;
// 已经翻译过(紧跟着一个我们插入的译文 span)则跳过
const next = node.nextSibling;
if (next && next.nodeType === 1 && next.classList &&
next.classList.contains('__en2zh-cn')) continue;
// 直接挂在 shadow root 下的裸文本无父元素,与原实现一致跳过
if (!(parent instanceof Element) || !isVisible(parent)) continue;
nodes.push(node);
} else if (node.nodeType === Node.ELEMENT_NODE) {
if (SKIP_TAGS.has(node.tagName)) continue;
// 插件自身 UI(进度角标、划词气泡宿主)与已插入的译文整棵跳过
if (node.id === '__en2zh-badge' || node.id === '__en2zh-selection') continue;
if (node.classList.contains('__en2zh-cn')) continue;
if (node.getAttribute('contenteditable') === 'true') continue;
// 很多现代网站用自定义组件承载正文,顺路补全 open shadow root
if (node.shadowRoot) walk(node.shadowRoot);
walk(node);
}
}
}
walk(root);
return nodes;
}
function makeBatches(nodes) {
const batches = [];
let current = [];
let charCount = 0;
for (const node of nodes) {
const line = normalizeText(node.nodeValue);
if (current.length >= BATCH_MAX_LINES || charCount + line.length > BATCH_MAX_CHARS) {
if (current.length) batches.push(current);
current = [];
charCount = 0;
}
current.push(node);
charCount += line.length;
}
if (current.length) batches.push(current);
return batches;
}
function insertTranslation(node, zhText) {
// 分批异步翻译期间页面可能重渲染(SPA),节点已脱离 DOM 则跳过,避免中断整个流程
if (!node.isConnected || !node.parentNode) return;
const span = document.createElement('span');
span.className = '__en2zh-cn';
span.textContent = zhText;
// 关键属性加 !important,避免被网站自身 CSS 覆盖导致译文不可见
span.style.cssText =
'display:block !important;visibility:visible !important;opacity:1 !important;' +
'max-height:none !important;overflow:visible !important;text-indent:0 !important;' +
'color:#3a6fd8 !important;font-size:0.92em !important;line-height:1.5 !important;margin:2px 0 6px;';
node.parentNode.insertBefore(span, node.nextSibling);
}
function showBadge() {
let badge = document.getElementById('__en2zh-badge');
if (!badge) {
badge = document.createElement('div');
badge.id = '__en2zh-badge';
badge.style.cssText =
'position:fixed;top:16px;right:16px;z-index:2147483647;background:#1a1e27;color:#e8e6e1;' +
'padding:8px 14px;border-radius:8px;font:13px -apple-system,"PingFang SC",sans-serif;' +
'box-shadow:0 4px 16px rgba(0,0,0,.3);';
document.body.appendChild(badge);
}
return badge;
}
function removeBadge() {
const badge = document.getElementById('__en2zh-badge');
if (badge) badge.remove();
}
function sleep(ms) {
return new Promise((r) => setTimeout(r, ms));
}
// —— 内联工具函数(不再依赖外部 content-utils.js)——
function isRateLimitError(msg) {
return /429|RateLimit|TooManyRequests|频/i.test(msg || '');
}
function normalizeText(text) {
return text.replace(/\s+/g, ' ').trim();
}
function hasTranslatableChar(text) {
return /[a-zA-Z一-鿿]/.test(text);
}
// 发送一个批次;遇到频控(429)等待后重试,其他错误直接返回;
// 返回行数与请求行数不一致时标记 mismatch,由调用方跳过该批次避免译文错位
async function translateBatchWithRetry(lines, badge, progressText) {
for (let attempt = 0; attempt <= RATE_LIMIT_MAX_RETRIES; attempt++) {
const res = await chrome.runtime.sendMessage({ type: 'TRANSLATE_BATCH', lines });
// MV3 下 service worker 被回收/未调用 sendResponse 时会 resolve undefined
if (!res) return { error: '后台服务无响应,请重新加载扩展后重试' };
if (!res.error) {
if (!Array.isArray(res.translated) || res.translated.length !== lines.length) {
return { mismatch: true };
}
return res;
}
if (!isRateLimitError(res.error) || attempt === RATE_LIMIT_MAX_RETRIES) return res;
badge.textContent = `触发频率限制,等待重试... ${progressText}`;
await sleep(RATE_LIMIT_WAIT_MS * (attempt + 1));
}
}
async function run() {
let hadError = false;
let badge = null;
try {
// 取消信号:「还原原文」脚本会把该标志置为 true(同一隔离世界),
// 每批前后检查,避免还原后剩余批次继续插入新译文
window.__en2zhCancelled = false;
const nodes = collectTextNodes(document.body);
if (!nodes.length) return;
const batches = makeBatches(nodes);
badge = showBadge();
for (let i = 0; i < batches.length; i++) {
if (window.__en2zhCancelled) { removeBadge(); return; }
const progressText = `${i + 1}/${batches.length}`;
badge.textContent = `翻译中... ${progressText}`;
const batch = batches[i];
const lines = batch.map((n) => normalizeText(n.nodeValue));
try {
const res = await translateBatchWithRetry(lines, badge, progressText);
// 等待期间可能被「还原原文」取消,不再插入本批结果
if (window.__en2zhCancelled) { removeBadge(); return; }
if (res.error) {
hadError = true;
badge.textContent = '出错: ' + res.error;
break;
}
if (res.mismatch) {
// 返回行数对不上,跳过该批次继续后面的,避免译文插错位置
badge.textContent = `警告:第 ${i + 1} 批返回行数异常,已跳过`;
await sleep(1500);
continue;
}
res.translated.forEach((zh, idx) => insertTranslation(batch[idx], zh));
} catch (e) {
hadError = true;
badge.textContent = '出错: ' + e.message;
break;
}
if (i < batches.length - 1) {
await sleep(DELAY_BETWEEN_BATCHES_MS);
}
}
if (!hadError) {
badge.textContent = '翻译完成 ✓';
setTimeout(removeBadge, 1500);
} else {
// 出错时保留错误提示更长时间,再移除角标
setTimeout(removeBadge, 4000);
}
} catch (e) {
// 意外异常(如 DOM 遍历失败)也要给出提示,避免静默失败
if (badge) {
badge.textContent = '出错: ' + e.message;
setTimeout(removeBadge, 4000);
}
} finally {
window.__en2zhRunning = false;
}
}
window.__en2zhShowBadge = showBadge;
window.__en2zhRemoveBadge = removeBadge;
run();
})();