fix: prevent broken sentence replacement, enforce active English locale, and patch registered DSH dictionaries

This commit is contained in:
katR 2026-10-11 17:41:42 +05:30
parent f441d0cb12
commit f27175d752
4 changed files with 245 additions and 97 deletions

View file

@ -244,7 +244,7 @@ var DYNAMIC_PATTERNS = [
// src/client/index.ts // src/client/index.ts
var name = "dsh-plugin-english-client"; var name = "dsh-plugin-english-client";
var inject = []; var inject = ["locale"];
var CHINESE_REGEX = /[\u4e00-\u9fa5]/; var CHINESE_REGEX = /[\u4e00-\u9fa5]/;
function translateText(text) { function translateText(text) {
if (!text || !CHINESE_REGEX.test(text)) return text; if (!text || !CHINESE_REGEX.test(text)) return text;
@ -252,17 +252,21 @@ function translateText(text) {
if (DICTIONARY[trimmed]) { if (DICTIONARY[trimmed]) {
return text.replace(trimmed, DICTIONARY[trimmed]); return text.replace(trimmed, DICTIONARY[trimmed]);
} }
let result = text;
for (const [pattern, replacement] of DYNAMIC_PATTERNS) { for (const [pattern, replacement] of DYNAMIC_PATTERNS) {
result = result.replace(pattern, replacement); if (pattern.test(trimmed)) {
return text.replace(trimmed, trimmed.replace(pattern, replacement));
}
} }
let result = text;
let changed = false;
for (const [cn, en] of SORTED_PAIRS) { for (const [cn, en] of SORTED_PAIRS) {
if (result.includes(cn)) { if (cn.length >= 2 && result.includes(cn)) {
result = result.replaceAll(cn, en); result = result.replaceAll(cn, en);
changed = true;
if (!CHINESE_REGEX.test(result)) break; if (!CHINESE_REGEX.test(result)) break;
} }
} }
return result; return changed ? result : text;
} }
var isTranslating = false; var isTranslating = false;
function translateElement(root) { function translateElement(root) {
@ -270,10 +274,8 @@ function translateElement(root) {
if (root.nodeType === Node.TEXT_NODE) { if (root.nodeType === Node.TEXT_NODE) {
const val = root.nodeValue; const val = root.nodeValue;
if (val && CHINESE_REGEX.test(val)) { if (val && CHINESE_REGEX.test(val)) {
const translated = translateText(val); const t = translateText(val);
if (translated !== val) { if (t !== val) root.nodeValue = t;
root.nodeValue = translated;
}
} }
return; return;
} }
@ -281,7 +283,7 @@ function translateElement(root) {
const el = root; const el = root;
const tag = el.tagName; const tag = el.tagName;
if (tag === "SCRIPT" || tag === "STYLE" || tag === "PRE" || tag === "CODE") return; if (tag === "SCRIPT" || tag === "STYLE" || tag === "PRE" || tag === "CODE") return;
for (const attr of ["placeholder", "title", "aria-label"]) { for (const attr of ["placeholder", "title", "aria-label", "alt"]) {
const val = el.getAttribute(attr); const val = el.getAttribute(attr);
if (val && CHINESE_REGEX.test(val)) { if (val && CHINESE_REGEX.test(val)) {
el.setAttribute(attr, translateText(val)); el.setAttribute(attr, translateText(val));
@ -292,33 +294,31 @@ function translateElement(root) {
while (textNode) { while (textNode) {
const val = textNode.nodeValue; const val = textNode.nodeValue;
if (val && CHINESE_REGEX.test(val)) { if (val && CHINESE_REGEX.test(val)) {
const parentTag = textNode.parentElement?.tagName; const parent = textNode.parentElement;
if (parentTag !== "SCRIPT" && parentTag !== "STYLE" && parentTag !== "PRE" && parentTag !== "CODE") { if (parent) {
const translated = translateText(val); const pTag = parent.tagName;
if (translated !== val) { if (pTag !== "SCRIPT" && pTag !== "STYLE" && pTag !== "PRE" && pTag !== "CODE") {
textNode.nodeValue = translated; const t = translateText(val);
if (t !== val) textNode.nodeValue = t;
} }
} }
} }
textNode = walker.nextNode(); textNode = walker.nextNode();
} }
} }
function initTranslator() { function startDOMTranslator() {
if (typeof document === "undefined" || window.__DSH_TRANSLATOR_ACTIVE__) return; if (typeof document === "undefined") return;
window.__DSH_TRANSLATOR_ACTIVE__ = true; document.documentElement.lang = "en";
console.log("[dsh-plugin-english] Universal English translator activated.");
const queue = /* @__PURE__ */ new Set(); const queue = /* @__PURE__ */ new Set();
let frameRequested = false; let frameRequested = false;
const processQueue = () => { const flush = () => {
frameRequested = false; frameRequested = false;
isTranslating = true; isTranslating = true;
try { try {
const nodes = Array.from(queue); const nodes = Array.from(queue);
queue.clear(); queue.clear();
for (const n of nodes) { for (const n of nodes) {
if (n.isConnected) { if (n.isConnected) translateElement(n);
translateElement(n);
}
} }
} finally { } finally {
isTranslating = false; isTranslating = false;
@ -329,10 +329,7 @@ function initTranslator() {
for (const m of mutations) { for (const m of mutations) {
if (m.type === "childList") { if (m.type === "childList") {
for (let i = 0; i < m.addedNodes.length; i++) { for (let i = 0; i < m.addedNodes.length; i++) {
const n = m.addedNodes[i]; queue.add(m.addedNodes[i]);
if (n.nodeType === Node.TEXT_NODE || n.nodeType === Node.ELEMENT_NODE) {
queue.add(n);
}
} }
} else if (m.type === "characterData") { } else if (m.type === "characterData") {
if (m.target.nodeValue && CHINESE_REGEX.test(m.target.nodeValue)) { if (m.target.nodeValue && CHINESE_REGEX.test(m.target.nodeValue)) {
@ -344,7 +341,7 @@ function initTranslator() {
} }
if (!frameRequested && queue.size > 0) { if (!frameRequested && queue.size > 0) {
frameRequested = true; frameRequested = true;
requestAnimationFrame(processQueue); requestAnimationFrame(flush);
} }
}); });
const root = document.documentElement || document.body; const root = document.documentElement || document.body;
@ -355,18 +352,7 @@ function initTranslator() {
subtree: true, subtree: true,
characterData: true, characterData: true,
attributes: true, attributes: true,
attributeFilter: ["placeholder", "title", "aria-label"] attributeFilter: ["placeholder", "title", "aria-label", "alt"]
});
} else {
document.addEventListener("DOMContentLoaded", () => {
translateElement(document.body);
observer.observe(document.body, {
childList: true,
subtree: true,
characterData: true,
attributes: true,
attributeFilter: ["placeholder", "title", "aria-label"]
});
}); });
} }
setInterval(() => { setInterval(() => {
@ -380,11 +366,87 @@ function initTranslator() {
} }
}, 1e3); }, 1e3);
} }
if (typeof window !== "undefined") { function enforceEnglishStorage() {
initTranslator(); if (typeof window === "undefined" || !window.localStorage) return;
try {
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && (key.includes("lang") || key.includes("locale") || key.includes("all-usage") || key.includes("dsh"))) {
const val = localStorage.getItem(key);
if (val === "zh" || val === "zh-CN") {
localStorage.setItem(key, "en");
}
}
}
} catch {
}
} }
function apply(ctx) { function apply(ctx) {
initTranslator(); console.log("[dsh-plugin-english] Activating English locale & translation engine...");
enforceEnglishStorage();
if (ctx.locale) {
try {
if (typeof ctx.locale.setLocale === "function") {
ctx.locale.setLocale("en");
}
if (typeof ctx.locale.resolveActive === "function") {
ctx.locale.resolveActive = () => "en";
}
if (typeof ctx.locale.getSnapshot === "function") {
const origSnap = ctx.locale.getSnapshot.bind(ctx.locale);
ctx.locale.getSnapshot = () => {
const s = origSnap();
return s ? { ...s, active: "en" } : s;
};
}
if (typeof ctx.locale.publish === "function") {
ctx.locale.publish("en", true);
}
if (typeof ctx.locale.translate === "function") {
const origTranslate = ctx.locale.translate.bind(ctx.locale);
ctx.locale.translate = function(ns, key, params) {
const val = origTranslate(ns, key, params);
if (typeof val === "string" && CHINESE_REGEX.test(val)) {
return translateText(val);
}
return val;
};
}
if (ctx.locale.dicts && typeof ctx.locale.dicts.entries === "function") {
for (const [ns, localesMap] of ctx.locale.dicts.entries()) {
const zh = localesMap?.get("zh");
let en = localesMap?.get("en");
if (!en) {
en = {};
localesMap?.set("en", en);
}
if (zh) {
for (const [k, v] of Object.entries(zh)) {
if ((!en[k] || en[k] === k || CHINESE_REGEX.test(en[k])) && typeof v === "string") {
en[k] = translateText(v) || v;
}
}
}
}
}
} catch (err) {
console.error("[dsh-plugin-english] Locale patch error:", err);
}
}
if (typeof document !== "undefined") {
document.documentElement.lang = "en";
const observer = new MutationObserver(() => {
if (document.documentElement.lang !== "en") {
document.documentElement.lang = "en";
}
});
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] });
}
startDOMTranslator();
}
if (typeof window !== "undefined") {
enforceEnglishStorage();
startDOMTranslator();
} }
return module.exports; } }); return module.exports; } });
//# sourceMappingURL=client.js.map //# sourceMappingURL=client.js.map

File diff suppressed because one or more lines are too long

View file

@ -31,6 +31,9 @@
}, },
"client": { "client": {
"immediately": true, "immediately": true,
"inject": [
"@deepseek-ai/dsh-client-locale"
],
"platform": "web" "platform": "web"
} }
}, },

View file

@ -1,42 +1,50 @@
import { DICTIONARY, SORTED_PAIRS, DYNAMIC_PATTERNS } from "./dictionary.js"; import { DICTIONARY, SORTED_PAIRS, DYNAMIC_PATTERNS } from "./dictionary.js";
export const name = "dsh-plugin-english-client"; export const name = "dsh-plugin-english-client";
export const inject = []; export const inject = ["locale"];
const CHINESE_REGEX = /[\u4e00-\u9fa5]/; const CHINESE_REGEX = /[\u4e00-\u9fa5]/;
/** /**
* Translates a single text string using dictionary and pattern matching. * Translates a text string safely without corrupting sentences:
* 1. Checks exact match on the entire string first.
* 2. Matches full dynamic count/date patterns.
* 3. Matches multi-word phrases from longest to shortest (never single characters).
*/ */
export function translateText(text: string): string { export function translateText(text: string): string {
if (!text || !CHINESE_REGEX.test(text)) return text; if (!text || !CHINESE_REGEX.test(text)) return text;
const trimmed = text.trim(); const trimmed = text.trim();
// 1. Direct exact dictionary match (highest fidelity)
if (DICTIONARY[trimmed]) { if (DICTIONARY[trimmed]) {
return text.replace(trimmed, DICTIONARY[trimmed]); return text.replace(trimmed, DICTIONARY[trimmed]);
} }
let result = text; // 2. Dynamic patterns (e.g. "3 个已启用" -> "3 enabled")
// Dynamic regex patterns (counts, numbers, time)
for (const [pattern, replacement] of DYNAMIC_PATTERNS) { for (const [pattern, replacement] of DYNAMIC_PATTERNS) {
result = result.replace(pattern, replacement); if (pattern.test(trimmed)) {
return text.replace(trimmed, trimmed.replace(pattern, replacement));
}
} }
// Exact phrase search for substrings (longest phrases first) // 3. Multi-word phrase matches (2+ characters only, longest first)
let result = text;
let changed = false;
for (const [cn, en] of SORTED_PAIRS) { for (const [cn, en] of SORTED_PAIRS) {
if (result.includes(cn)) { if (cn.length >= 2 && result.includes(cn)) {
result = result.replaceAll(cn, en); result = result.replaceAll(cn, en);
changed = true;
if (!CHINESE_REGEX.test(result)) break; if (!CHINESE_REGEX.test(result)) break;
} }
} }
return result; return changed ? result : text;
} }
let isTranslating = false; let isTranslating = false;
/** /**
* Translates an element and all its children using native TreeWalker. * Traverses elements using TreeWalker and translates whole text nodes.
*/ */
export function translateElement(root: Node) { export function translateElement(root: Node) {
if (!root) return; if (!root) return;
@ -44,10 +52,8 @@ export function translateElement(root: Node) {
if (root.nodeType === Node.TEXT_NODE) { if (root.nodeType === Node.TEXT_NODE) {
const val = root.nodeValue; const val = root.nodeValue;
if (val && CHINESE_REGEX.test(val)) { if (val && CHINESE_REGEX.test(val)) {
const translated = translateText(val); const t = translateText(val);
if (translated !== val) { if (t !== val) root.nodeValue = t;
root.nodeValue = translated;
}
} }
return; return;
} }
@ -56,10 +62,11 @@ export function translateElement(root: Node) {
const el = root as HTMLElement; const el = root as HTMLElement;
const tag = el.tagName; const tag = el.tagName;
// Ignore code, pre, scripts, styles
if (tag === "SCRIPT" || tag === "STYLE" || tag === "PRE" || tag === "CODE") return; if (tag === "SCRIPT" || tag === "STYLE" || tag === "PRE" || tag === "CODE") return;
// Translate attributes // Attributes (placeholder, title, aria-label)
for (const attr of ["placeholder", "title", "aria-label"]) { for (const attr of ["placeholder", "title", "aria-label", "alt"]) {
const val = el.getAttribute(attr); const val = el.getAttribute(attr);
if (val && CHINESE_REGEX.test(val)) { if (val && CHINESE_REGEX.test(val)) {
el.setAttribute(attr, translateText(val)); el.setAttribute(attr, translateText(val));
@ -72,11 +79,12 @@ export function translateElement(root: Node) {
while (textNode) { while (textNode) {
const val = textNode.nodeValue; const val = textNode.nodeValue;
if (val && CHINESE_REGEX.test(val)) { if (val && CHINESE_REGEX.test(val)) {
const parentTag = textNode.parentElement?.tagName; const parent = textNode.parentElement;
if (parentTag !== "SCRIPT" && parentTag !== "STYLE" && parentTag !== "PRE" && parentTag !== "CODE") { if (parent) {
const translated = translateText(val); const pTag = parent.tagName;
if (translated !== val) { if (pTag !== "SCRIPT" && pTag !== "STYLE" && pTag !== "PRE" && pTag !== "CODE") {
textNode.nodeValue = translated; const t = translateText(val);
if (t !== val) textNode.nodeValue = t;
} }
} }
} }
@ -84,25 +92,23 @@ export function translateElement(root: Node) {
} }
} }
function initTranslator() { function startDOMTranslator() {
if (typeof document === "undefined" || (window as any).__DSH_TRANSLATOR_ACTIVE__) return; if (typeof document === "undefined") return;
(window as any).__DSH_TRANSLATOR_ACTIVE__ = true;
console.log("[dsh-plugin-english] Universal English translator activated."); // Force document language to English immediately
document.documentElement.lang = "en";
const queue = new Set<Node>(); const queue = new Set<Node>();
let frameRequested = false; let frameRequested = false;
const processQueue = () => { const flush = () => {
frameRequested = false; frameRequested = false;
isTranslating = true; isTranslating = true;
try { try {
const nodes = Array.from(queue); const nodes = Array.from(queue);
queue.clear(); queue.clear();
for (const n of nodes) { for (const n of nodes) {
if (n.isConnected) { if (n.isConnected) translateElement(n);
translateElement(n);
}
} }
} finally { } finally {
isTranslating = false; isTranslating = false;
@ -115,10 +121,7 @@ function initTranslator() {
for (const m of mutations) { for (const m of mutations) {
if (m.type === "childList") { if (m.type === "childList") {
for (let i = 0; i < m.addedNodes.length; i++) { for (let i = 0; i < m.addedNodes.length; i++) {
const n = m.addedNodes[i]; queue.add(m.addedNodes[i]);
if (n.nodeType === Node.TEXT_NODE || n.nodeType === Node.ELEMENT_NODE) {
queue.add(n);
}
} }
} else if (m.type === "characterData") { } else if (m.type === "characterData") {
if (m.target.nodeValue && CHINESE_REGEX.test(m.target.nodeValue)) { if (m.target.nodeValue && CHINESE_REGEX.test(m.target.nodeValue)) {
@ -128,14 +131,12 @@ function initTranslator() {
queue.add(m.target); queue.add(m.target);
} }
} }
if (!frameRequested && queue.size > 0) { if (!frameRequested && queue.size > 0) {
frameRequested = true; frameRequested = true;
requestAnimationFrame(processQueue); requestAnimationFrame(flush);
} }
}); });
// Observe root
const root = document.documentElement || document.body; const root = document.documentElement || document.body;
if (root) { if (root) {
translateElement(root); translateElement(root);
@ -144,22 +145,11 @@ function initTranslator() {
subtree: true, subtree: true,
characterData: true, characterData: true,
attributes: true, attributes: true,
attributeFilter: ["placeholder", "title", "aria-label"], attributeFilter: ["placeholder", "title", "aria-label", "alt"],
});
} else {
document.addEventListener("DOMContentLoaded", () => {
translateElement(document.body);
observer.observe(document.body, {
childList: true,
subtree: true,
characterData: true,
attributes: true,
attributeFilter: ["placeholder", "title", "aria-label"],
});
}); });
} }
// Periodic safety check every 1s for any dynamically inserted lazy elements // Periodic safety check every 1s to sweep any lazily mounted modal content
setInterval(() => { setInterval(() => {
if (document.body && !isTranslating) { if (document.body && !isTranslating) {
isTranslating = true; isTranslating = true;
@ -172,11 +162,104 @@ function initTranslator() {
}, 1000); }, 1000);
} }
// Start immediately when the bundle loads in browser // Force English locale in localStorage across known plugin keys
if (typeof window !== "undefined") { function enforceEnglishStorage() {
initTranslator(); if (typeof window === "undefined" || !window.localStorage) return;
try {
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && (key.includes("lang") || key.includes("locale") || key.includes("all-usage") || key.includes("dsh"))) {
const val = localStorage.getItem(key);
if (val === "zh" || val === "zh-CN") {
localStorage.setItem(key, "en");
}
}
}
} catch {}
} }
export function apply(ctx: any) { export function apply(ctx: any) {
initTranslator(); console.log("[dsh-plugin-english] Activating English locale & translation engine...");
enforceEnglishStorage();
// 1. Force DSH core locale service to English
if (ctx.locale) {
try {
// Force active locale to "en"
if (typeof ctx.locale.setLocale === "function") {
ctx.locale.setLocale("en");
}
if (typeof ctx.locale.resolveActive === "function") {
ctx.locale.resolveActive = () => "en";
}
// Intercept snapshot
if (typeof ctx.locale.getSnapshot === "function") {
const origSnap = ctx.locale.getSnapshot.bind(ctx.locale);
ctx.locale.getSnapshot = () => {
const s = origSnap();
return s ? { ...s, active: "en" } : s;
};
}
// Publish active: "en" to trigger all native bilingual plugin updates
if (typeof ctx.locale.publish === "function") {
ctx.locale.publish("en", true);
}
// Intercept translate function
if (typeof ctx.locale.translate === "function") {
const origTranslate = ctx.locale.translate.bind(ctx.locale);
ctx.locale.translate = function (ns: string, key: string, params: any) {
const val = origTranslate(ns, key, params);
if (typeof val === "string" && CHINESE_REGEX.test(val)) {
return translateText(val);
}
return val;
};
}
// Auto-patch all plugin registered dictionaries with English translations
if (ctx.locale.dicts && typeof ctx.locale.dicts.entries === "function") {
for (const [ns, localesMap] of ctx.locale.dicts.entries()) {
const zh = localesMap?.get("zh");
let en = localesMap?.get("en");
if (!en) {
en = {};
localesMap?.set("en", en);
}
if (zh) {
for (const [k, v] of Object.entries(zh)) {
if ((!en[k] || en[k] === k || CHINESE_REGEX.test(en[k])) && typeof v === "string") {
en[k] = translateText(v) || v;
}
}
}
}
}
} catch (err) {
console.error("[dsh-plugin-english] Locale patch error:", err);
}
}
// 2. Keep document.documentElement.lang set to "en"
if (typeof document !== "undefined") {
document.documentElement.lang = "en";
const observer = new MutationObserver(() => {
if (document.documentElement.lang !== "en") {
document.documentElement.lang = "en";
}
});
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] });
}
// 3. Start DOM translation engine
startDOMTranslator();
}
// Start immediately on script load as well
if (typeof window !== "undefined") {
enforceEnglishStorage();
startDOMTranslator();
} }