@yadsh/dsh-l10n-overrides 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +83 -0
- package/ROADMAP.md +35 -0
- package/cordis.patch.yml +4 -0
- package/lib/client.js +1207 -0
- package/lib/index.js +4 -0
- package/lib/types/adapters/dsh-locale-runtime.d.ts +26 -0
- package/lib/types/client/index.d.ts +15 -0
- package/lib/types/index.d.ts +1 -0
- package/lib/types/packs/example.d.ts +25 -0
- package/lib/types/packs/index.d.ts +24 -0
- package/lib/types/registry/diagnostics.d.ts +15 -0
- package/lib/types/registry/translation-registry.d.ts +21 -0
- package/lib/types/runtime/dom-translator.d.ts +10 -0
- package/lib/types/runtime/interpolate.d.ts +1 -0
- package/lib/types/runtime/locale-hook.d.ts +3 -0
- package/lib/types/types.d.ts +28 -0
- package/package.json +95 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,1207 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "dsh-l10n-overrides",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
//#region src/adapters/dsh-locale-runtime.ts
|
|
8
|
+
function failure() {
|
|
9
|
+
return { ok: false };
|
|
10
|
+
}
|
|
11
|
+
function adaptDshLocaleRuntime(value) {
|
|
12
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
13
|
+
let translate;
|
|
14
|
+
let getSnapshot;
|
|
15
|
+
let subscribe;
|
|
16
|
+
let originalTranslateDescriptor;
|
|
17
|
+
try {
|
|
18
|
+
translate = Reflect.get(value, "translate");
|
|
19
|
+
getSnapshot = Reflect.get(value, "getSnapshot");
|
|
20
|
+
subscribe = Reflect.get(value, "subscribe");
|
|
21
|
+
originalTranslateDescriptor = Reflect.getOwnPropertyDescriptor(value, "translate");
|
|
22
|
+
} catch {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (typeof translate !== "function" || typeof getSnapshot !== "function" || typeof subscribe !== "function") return;
|
|
26
|
+
const runtime = value;
|
|
27
|
+
const originalTranslate = translate;
|
|
28
|
+
const capturedGetSnapshot = getSnapshot;
|
|
29
|
+
function inspectRestoration(wrapper) {
|
|
30
|
+
try {
|
|
31
|
+
const current = Reflect.get(runtime, "translate");
|
|
32
|
+
if (current === originalTranslate) return "restored";
|
|
33
|
+
if (current === wrapper) return "failed";
|
|
34
|
+
return "replaced";
|
|
35
|
+
} catch {
|
|
36
|
+
return "failed";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function restoreOriginalTranslate(wrapper) {
|
|
40
|
+
try {
|
|
41
|
+
if (originalTranslateDescriptor === void 0) {
|
|
42
|
+
if (Reflect.getOwnPropertyDescriptor(runtime, "translate") === void 0) {
|
|
43
|
+
if (!Reflect.set(runtime, "translate", originalTranslate)) return inspectRestoration(wrapper);
|
|
44
|
+
} else if (!Reflect.deleteProperty(runtime, "translate")) return inspectRestoration(wrapper);
|
|
45
|
+
} else if ("value" in originalTranslateDescriptor) {
|
|
46
|
+
if (!Reflect.defineProperty(runtime, "translate", originalTranslateDescriptor)) return inspectRestoration(wrapper);
|
|
47
|
+
} else {
|
|
48
|
+
if (!Reflect.set(runtime, "translate", originalTranslate)) return inspectRestoration(wrapper);
|
|
49
|
+
if (!Reflect.defineProperty(runtime, "translate", originalTranslateDescriptor)) return inspectRestoration(wrapper);
|
|
50
|
+
}
|
|
51
|
+
return inspectRestoration(wrapper);
|
|
52
|
+
} catch {
|
|
53
|
+
return inspectRestoration(wrapper);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
runtime,
|
|
58
|
+
getActiveLocale() {
|
|
59
|
+
try {
|
|
60
|
+
const snapshot = Reflect.apply(capturedGetSnapshot, runtime, []);
|
|
61
|
+
if (typeof snapshot !== "object" || snapshot === null) return failure();
|
|
62
|
+
const active = Reflect.get(snapshot, "active");
|
|
63
|
+
return typeof active === "string" ? {
|
|
64
|
+
ok: true,
|
|
65
|
+
value: active
|
|
66
|
+
} : failure();
|
|
67
|
+
} catch {
|
|
68
|
+
return failure();
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
callOriginal(args) {
|
|
72
|
+
return Reflect.apply(originalTranslate, runtime, args);
|
|
73
|
+
},
|
|
74
|
+
install(wrapper) {
|
|
75
|
+
const inspectCurrent = () => {
|
|
76
|
+
try {
|
|
77
|
+
const current = Reflect.get(runtime, "translate");
|
|
78
|
+
if (current === wrapper) return "wrapper";
|
|
79
|
+
if (current === originalTranslate) return "original";
|
|
80
|
+
return "foreign";
|
|
81
|
+
} catch {
|
|
82
|
+
return "unknown";
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
const failedInstall = () => {
|
|
86
|
+
const beforeRollback = inspectCurrent();
|
|
87
|
+
if (beforeRollback === "wrapper") return {
|
|
88
|
+
ok: false,
|
|
89
|
+
runtimeMayBePatched: restoreOriginalTranslate(wrapper) === "failed"
|
|
90
|
+
};
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
runtimeMayBePatched: beforeRollback === "unknown"
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
try {
|
|
97
|
+
if (!Reflect.set(runtime, "translate", wrapper)) return failedInstall();
|
|
98
|
+
if (Reflect.get(runtime, "translate") !== wrapper) return failedInstall();
|
|
99
|
+
return { ok: true };
|
|
100
|
+
} catch {
|
|
101
|
+
return failedInstall();
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
restoreOriginalIfCurrent(wrapper) {
|
|
105
|
+
try {
|
|
106
|
+
if (Reflect.get(runtime, "translate") !== wrapper) return {
|
|
107
|
+
ok: true,
|
|
108
|
+
value: "replaced"
|
|
109
|
+
};
|
|
110
|
+
const restoration = restoreOriginalTranslate(wrapper);
|
|
111
|
+
return restoration === "failed" ? failure() : {
|
|
112
|
+
ok: true,
|
|
113
|
+
value: restoration
|
|
114
|
+
};
|
|
115
|
+
} catch {
|
|
116
|
+
return failure();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
//#endregion
|
|
122
|
+
//#region src/packs/example.ts
|
|
123
|
+
const example = Object.freeze({
|
|
124
|
+
id: "example-plugin-en",
|
|
125
|
+
target: Object.freeze({
|
|
126
|
+
package: "dsh-example-plugin",
|
|
127
|
+
versions: ">=0.4.0 <1.0.0"
|
|
128
|
+
}),
|
|
129
|
+
en: Object.freeze({ "example.settings": Object.freeze({
|
|
130
|
+
title: "Settings",
|
|
131
|
+
enabled: "Enabled",
|
|
132
|
+
server: "Server address",
|
|
133
|
+
save: "Save"
|
|
134
|
+
}) }),
|
|
135
|
+
dom: Object.freeze([Object.freeze({
|
|
136
|
+
scope: "[data-plugin=\"example-plugin\"]",
|
|
137
|
+
source: "设置",
|
|
138
|
+
target: "Settings"
|
|
139
|
+
})]),
|
|
140
|
+
metadata: Object.freeze({
|
|
141
|
+
sourceLanguage: "zh",
|
|
142
|
+
description: "Example English translation pack"
|
|
143
|
+
})
|
|
144
|
+
});
|
|
145
|
+
//#endregion
|
|
146
|
+
//#region src/packs/index.ts
|
|
147
|
+
const translationPacks = Object.freeze([example]);
|
|
148
|
+
//#endregion
|
|
149
|
+
//#region src/registry/diagnostics.ts
|
|
150
|
+
const PREFIX = "[dsh-l10n-overrides]";
|
|
151
|
+
var Diagnostics = class {
|
|
152
|
+
logger;
|
|
153
|
+
#debugEnabled;
|
|
154
|
+
#entries = [];
|
|
155
|
+
constructor(logger = console, options = {}) {
|
|
156
|
+
this.logger = logger;
|
|
157
|
+
this.#debugEnabled = options.debug === true;
|
|
158
|
+
}
|
|
159
|
+
info(code, message) {
|
|
160
|
+
this.#record("info", "info", code, message);
|
|
161
|
+
}
|
|
162
|
+
warning(code, message) {
|
|
163
|
+
this.#record("warning", "warn", code, message);
|
|
164
|
+
}
|
|
165
|
+
error(code, message) {
|
|
166
|
+
this.#record("error", "error", code, message);
|
|
167
|
+
}
|
|
168
|
+
debug(code, message) {
|
|
169
|
+
if (!this.#debugEnabled) return;
|
|
170
|
+
this.#record("debug", "debug", code, message);
|
|
171
|
+
}
|
|
172
|
+
snapshot() {
|
|
173
|
+
return Object.freeze(this.#entries.slice());
|
|
174
|
+
}
|
|
175
|
+
#record(level, loggerMethod, code, message) {
|
|
176
|
+
this.#entries.push(Object.freeze({
|
|
177
|
+
level,
|
|
178
|
+
code,
|
|
179
|
+
message
|
|
180
|
+
}));
|
|
181
|
+
try {
|
|
182
|
+
this.logger[loggerMethod](`${PREFIX} ${message}`);
|
|
183
|
+
} catch {}
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
//#endregion
|
|
187
|
+
//#region src/registry/translation-registry.ts
|
|
188
|
+
const DOM_TRANSLATION_ATTRIBUTES$1 = /* @__PURE__ */ new Set([
|
|
189
|
+
"placeholder",
|
|
190
|
+
"title",
|
|
191
|
+
"aria-label",
|
|
192
|
+
"alt"
|
|
193
|
+
]);
|
|
194
|
+
const EMPTY_DOM_RULES = Object.freeze([]);
|
|
195
|
+
function isNonBlankString(value) {
|
|
196
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
197
|
+
}
|
|
198
|
+
function isDomTranslationAttribute(value) {
|
|
199
|
+
return typeof value === "string" && DOM_TRANSLATION_ATTRIBUTES$1.has(value);
|
|
200
|
+
}
|
|
201
|
+
function isDictionary(value) {
|
|
202
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
203
|
+
}
|
|
204
|
+
function normalizeDomRule(value) {
|
|
205
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
206
|
+
const rule = value;
|
|
207
|
+
const source = rule.source;
|
|
208
|
+
const target = rule.target;
|
|
209
|
+
const scope = rule.scope;
|
|
210
|
+
const mode = rule.mode;
|
|
211
|
+
const rawAttributes = rule.attributes;
|
|
212
|
+
if (!isNonBlankString(source) || !isNonBlankString(target) || !isNonBlankString(scope) || mode !== void 0 && mode !== "exact" || rawAttributes !== void 0 && !Array.isArray(rawAttributes)) return;
|
|
213
|
+
let attributes;
|
|
214
|
+
if (rawAttributes !== void 0) {
|
|
215
|
+
const capturedAttributes = [];
|
|
216
|
+
const attributeCount = rawAttributes.length;
|
|
217
|
+
for (let index = 0; index < attributeCount; index += 1) {
|
|
218
|
+
const attribute = rawAttributes[index];
|
|
219
|
+
if (!isDomTranslationAttribute(attribute)) return void 0;
|
|
220
|
+
capturedAttributes.push(attribute);
|
|
221
|
+
}
|
|
222
|
+
attributes = Object.freeze(capturedAttributes);
|
|
223
|
+
}
|
|
224
|
+
return Object.freeze({
|
|
225
|
+
source,
|
|
226
|
+
target,
|
|
227
|
+
scope,
|
|
228
|
+
...mode === void 0 ? {} : { mode },
|
|
229
|
+
...attributes === void 0 ? {} : { attributes }
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
function normalizeTranslations(value) {
|
|
233
|
+
if (!isDictionary(value)) return void 0;
|
|
234
|
+
const translations = [];
|
|
235
|
+
for (const [namespace, dictionary] of Object.entries(value)) {
|
|
236
|
+
if (!isDictionary(dictionary)) return void 0;
|
|
237
|
+
for (const [key, translation] of Object.entries(dictionary)) {
|
|
238
|
+
if (typeof translation !== "string") return void 0;
|
|
239
|
+
translations.push(Object.freeze({
|
|
240
|
+
namespace,
|
|
241
|
+
key,
|
|
242
|
+
value: translation
|
|
243
|
+
}));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return Object.freeze(translations);
|
|
247
|
+
}
|
|
248
|
+
function normalizeDomRules(value) {
|
|
249
|
+
if (value === void 0) return Object.freeze({
|
|
250
|
+
rules: EMPTY_DOM_RULES,
|
|
251
|
+
invalidRuleIndexes: Object.freeze([]),
|
|
252
|
+
invalidDeclaration: false
|
|
253
|
+
});
|
|
254
|
+
if (!Array.isArray(value)) return Object.freeze({
|
|
255
|
+
rules: EMPTY_DOM_RULES,
|
|
256
|
+
invalidRuleIndexes: Object.freeze([]),
|
|
257
|
+
invalidDeclaration: true
|
|
258
|
+
});
|
|
259
|
+
const rules = [];
|
|
260
|
+
const invalidRuleIndexes = [];
|
|
261
|
+
const ruleCount = value.length;
|
|
262
|
+
for (let index = 0; index < ruleCount; index += 1) try {
|
|
263
|
+
const rule = normalizeDomRule(value[index]);
|
|
264
|
+
if (rule === void 0) invalidRuleIndexes.push(index);
|
|
265
|
+
else rules.push(rule);
|
|
266
|
+
} catch {
|
|
267
|
+
invalidRuleIndexes.push(index);
|
|
268
|
+
}
|
|
269
|
+
return Object.freeze({
|
|
270
|
+
rules: Object.freeze(rules),
|
|
271
|
+
invalidRuleIndexes: Object.freeze(invalidRuleIndexes),
|
|
272
|
+
invalidDeclaration: false
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
function safePackLabel(value) {
|
|
276
|
+
return isNonBlankString(value) ? `"${value}"` : "<unknown pack>";
|
|
277
|
+
}
|
|
278
|
+
var TranslationPackRegistry = class {
|
|
279
|
+
diagnostics;
|
|
280
|
+
#translations = /* @__PURE__ */ new Map();
|
|
281
|
+
#packIds = /* @__PURE__ */ new Set();
|
|
282
|
+
#domRules = [];
|
|
283
|
+
#packCount = 0;
|
|
284
|
+
#overrideCount = 0;
|
|
285
|
+
constructor(diagnostics) {
|
|
286
|
+
this.diagnostics = diagnostics;
|
|
287
|
+
}
|
|
288
|
+
register(pack) {
|
|
289
|
+
const source = pack;
|
|
290
|
+
let rawPackId;
|
|
291
|
+
try {
|
|
292
|
+
rawPackId = source.id;
|
|
293
|
+
} catch {
|
|
294
|
+
this.diagnostics.error("invalid_pack", "Pack <unknown pack> has an unreadable id and was ignored.");
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (!isNonBlankString(rawPackId)) {
|
|
298
|
+
this.diagnostics.error("invalid_pack", `Pack ${safePackLabel(rawPackId)} has an invalid id and was ignored.`);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const packId = rawPackId;
|
|
302
|
+
if (this.#packIds.has(packId)) {
|
|
303
|
+
this.diagnostics.error("duplicate_pack_id", `Duplicate pack id "${packId}" ignored.`);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
let translations;
|
|
307
|
+
try {
|
|
308
|
+
const rawTranslations = source.en;
|
|
309
|
+
translations = normalizeTranslations(rawTranslations);
|
|
310
|
+
} catch {
|
|
311
|
+
translations = void 0;
|
|
312
|
+
}
|
|
313
|
+
if (translations === void 0) {
|
|
314
|
+
this.diagnostics.error("invalid_pack", `Pack "${packId}" has invalid English translations and was ignored.`);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
let normalizedDomRules;
|
|
318
|
+
try {
|
|
319
|
+
const rawDom = source.dom;
|
|
320
|
+
normalizedDomRules = normalizeDomRules(rawDom);
|
|
321
|
+
} catch {
|
|
322
|
+
normalizedDomRules = Object.freeze({
|
|
323
|
+
rules: EMPTY_DOM_RULES,
|
|
324
|
+
invalidRuleIndexes: Object.freeze([]),
|
|
325
|
+
invalidDeclaration: true
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
this.#packIds.add(packId);
|
|
329
|
+
this.#packCount += 1;
|
|
330
|
+
let namespaces = this.#translations.get("en");
|
|
331
|
+
if (namespaces === void 0) {
|
|
332
|
+
namespaces = /* @__PURE__ */ new Map();
|
|
333
|
+
this.#translations.set("en", namespaces);
|
|
334
|
+
}
|
|
335
|
+
for (const { namespace, key, value } of translations) {
|
|
336
|
+
let entries = namespaces.get(namespace);
|
|
337
|
+
if (entries === void 0) {
|
|
338
|
+
entries = /* @__PURE__ */ new Map();
|
|
339
|
+
namespaces.set(namespace, entries);
|
|
340
|
+
}
|
|
341
|
+
const previous = entries.get(key);
|
|
342
|
+
if (previous !== void 0) {
|
|
343
|
+
this.diagnostics.error("duplicate_override", `Duplicate override en/${namespace}/${key}: keeping pack "${previous.packId}"; ignoring pack "${packId}".`);
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
entries.set(key, Object.freeze({
|
|
347
|
+
value,
|
|
348
|
+
packId
|
|
349
|
+
}));
|
|
350
|
+
this.#overrideCount += 1;
|
|
351
|
+
}
|
|
352
|
+
for (const rule of normalizedDomRules.rules) this.#domRules.push(rule);
|
|
353
|
+
if (normalizedDomRules.invalidDeclaration) this.diagnostics.error("invalid_dom_rule", `Pack "${packId}" DOM rules declaration is invalid and was ignored.`);
|
|
354
|
+
for (const index of normalizedDomRules.invalidRuleIndexes) this.diagnostics.error("invalid_dom_rule", `Pack "${packId}" DOM rule at index ${index} is invalid and was ignored.`);
|
|
355
|
+
if (normalizedDomRules.rules.some((rule) => rule.scope === "global")) this.diagnostics.warning("global_dom_scope", `Pack "${packId}" contains global DOM translation rules.`);
|
|
356
|
+
}
|
|
357
|
+
resolve(locale, namespace, key) {
|
|
358
|
+
return this.resolveEntry(locale, namespace, key)?.value;
|
|
359
|
+
}
|
|
360
|
+
resolveEntry(locale, namespace, key) {
|
|
361
|
+
return this.#translations.get(locale)?.get(namespace)?.get(key);
|
|
362
|
+
}
|
|
363
|
+
getStats() {
|
|
364
|
+
return Object.freeze({
|
|
365
|
+
packs: this.#packCount,
|
|
366
|
+
localeOverrides: this.#overrideCount,
|
|
367
|
+
domRules: this.#domRules.length
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
getDomRules(locale) {
|
|
371
|
+
return locale === "en" ? Object.freeze(this.#domRules.slice()) : EMPTY_DOM_RULES;
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
//#endregion
|
|
375
|
+
//#region src/runtime/dom-translator.ts
|
|
376
|
+
const DOM_TRANSLATION_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
377
|
+
"placeholder",
|
|
378
|
+
"title",
|
|
379
|
+
"aria-label",
|
|
380
|
+
"alt"
|
|
381
|
+
]);
|
|
382
|
+
const ELEMENT_NODE = 1;
|
|
383
|
+
const TEXT_NODE = 3;
|
|
384
|
+
const SHOW_ELEMENT_AND_TEXT = 5;
|
|
385
|
+
const KNOWN_PROTECTION_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
386
|
+
"class",
|
|
387
|
+
"contenteditable",
|
|
388
|
+
"data-no-translate",
|
|
389
|
+
"data-message-id",
|
|
390
|
+
"data-testid"
|
|
391
|
+
]);
|
|
392
|
+
const CLASS_PROTECTION_PATTERN = /conversation|message|markdown|editor|terminal|prompt/i;
|
|
393
|
+
const TEST_ID_PROTECTION_PATTERN = /conversation|message|markdown|editor|terminal|prompt|composer/i;
|
|
394
|
+
const SCOPE_AND_PROTECTION_ATTRIBUTES = [
|
|
395
|
+
"class",
|
|
396
|
+
"id",
|
|
397
|
+
...KNOWN_PROTECTION_ATTRIBUTES
|
|
398
|
+
];
|
|
399
|
+
const SHARED_PROTECTED_SURFACES = [
|
|
400
|
+
"[contenteditable]",
|
|
401
|
+
"[data-no-translate]",
|
|
402
|
+
"[data-message-id]",
|
|
403
|
+
"[data-testid*=\"conversation\" i]",
|
|
404
|
+
"[data-testid*=\"message\" i]",
|
|
405
|
+
"[data-testid*=\"markdown\" i]",
|
|
406
|
+
"[data-testid*=\"editor\" i]",
|
|
407
|
+
"[data-testid*=\"terminal\" i]",
|
|
408
|
+
"[data-testid*=\"prompt\" i]",
|
|
409
|
+
"[data-testid*=\"composer\" i]",
|
|
410
|
+
"[class*=\"conversation\" i]",
|
|
411
|
+
"[class*=\"message\" i]",
|
|
412
|
+
"[class*=\"markdown\" i]",
|
|
413
|
+
"[class*=\"editor\" i]",
|
|
414
|
+
"[class*=\"terminal\" i]",
|
|
415
|
+
"[class*=\"prompt\" i]"
|
|
416
|
+
];
|
|
417
|
+
const CODE_LIKE_PROTECTED_SURFACES = [
|
|
418
|
+
"pre",
|
|
419
|
+
"code",
|
|
420
|
+
"kbd",
|
|
421
|
+
"samp",
|
|
422
|
+
"script",
|
|
423
|
+
"style"
|
|
424
|
+
];
|
|
425
|
+
const TEXT_PROTECTED_SURFACE_SELECTOR = [
|
|
426
|
+
"input",
|
|
427
|
+
"textarea",
|
|
428
|
+
...CODE_LIKE_PROTECTED_SURFACES,
|
|
429
|
+
...SHARED_PROTECTED_SURFACES
|
|
430
|
+
].join(",");
|
|
431
|
+
const ATTRIBUTE_PROTECTED_SURFACE_SELECTOR = [...CODE_LIKE_PROTECTED_SURFACES, ...SHARED_PROTECTED_SURFACES].join(",");
|
|
432
|
+
function isScopeIdentifierStart(character) {
|
|
433
|
+
return character !== void 0 && /[A-Z_a-z\u0080-\uFFFF]/.test(character);
|
|
434
|
+
}
|
|
435
|
+
function consumeScopeIdentifier(scope, start) {
|
|
436
|
+
let index = start;
|
|
437
|
+
if (scope[index] === "-") {
|
|
438
|
+
index += 1;
|
|
439
|
+
if (scope[index] === "-") index += 1;
|
|
440
|
+
else if (!isScopeIdentifierStart(scope[index])) return start;
|
|
441
|
+
} else if (isScopeIdentifierStart(scope[index])) index += 1;
|
|
442
|
+
else return start;
|
|
443
|
+
while (index < scope.length && /[-0-9A-Z_a-z\u0080-\uFFFF]/.test(scope[index] ?? "")) index += 1;
|
|
444
|
+
return index;
|
|
445
|
+
}
|
|
446
|
+
function skipScopeWhitespace(scope, start) {
|
|
447
|
+
let index = start;
|
|
448
|
+
while (index < scope.length && /[\t\n\f\r ]/.test(scope[index] ?? "")) index += 1;
|
|
449
|
+
return index;
|
|
450
|
+
}
|
|
451
|
+
function parseScopeAttribute(scope, start) {
|
|
452
|
+
let index = skipScopeWhitespace(scope, start + 1);
|
|
453
|
+
const nameStart = index;
|
|
454
|
+
if (!/[A-Z_a-z]/.test(scope[index] ?? "")) return void 0;
|
|
455
|
+
index += 1;
|
|
456
|
+
while (index < scope.length && /[-0-9A-Z_a-z]/.test(scope[index] ?? "")) index += 1;
|
|
457
|
+
const dependency = scope.slice(nameStart, index).toLowerCase();
|
|
458
|
+
index = skipScopeWhitespace(scope, index);
|
|
459
|
+
if (scope[index] === "]") return {
|
|
460
|
+
dependency,
|
|
461
|
+
nextIndex: index + 1
|
|
462
|
+
};
|
|
463
|
+
const operatorStart = scope[index];
|
|
464
|
+
if (operatorStart === "=") index += 1;
|
|
465
|
+
else if (operatorStart !== void 0 && "~|^$*".includes(operatorStart) && scope[index + 1] === "=") index += 2;
|
|
466
|
+
else return;
|
|
467
|
+
index = skipScopeWhitespace(scope, index);
|
|
468
|
+
const quote = scope[index];
|
|
469
|
+
if (quote === "\"" || quote === "'") {
|
|
470
|
+
index += 1;
|
|
471
|
+
while (index < scope.length && scope[index] !== quote) index += 1;
|
|
472
|
+
if (scope[index] !== quote) return void 0;
|
|
473
|
+
index += 1;
|
|
474
|
+
} else {
|
|
475
|
+
const next = consumeScopeIdentifier(scope, index);
|
|
476
|
+
if (next === index) return void 0;
|
|
477
|
+
index = next;
|
|
478
|
+
}
|
|
479
|
+
const valueEnd = index;
|
|
480
|
+
index = skipScopeWhitespace(scope, index);
|
|
481
|
+
if (index > valueEnd && /[IiSs]/.test(scope[index] ?? "")) {
|
|
482
|
+
index += 1;
|
|
483
|
+
index = skipScopeWhitespace(scope, index);
|
|
484
|
+
}
|
|
485
|
+
if (scope[index] !== "]") return void 0;
|
|
486
|
+
return {
|
|
487
|
+
dependency,
|
|
488
|
+
nextIndex: index + 1
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
function parseSupportedScopeSelector(scope) {
|
|
492
|
+
if (scope.length === 0 || scope.includes("\\") || scope.includes(",") || scope.includes("/*") || scope.includes("*/")) return;
|
|
493
|
+
const dependencies = /* @__PURE__ */ new Set();
|
|
494
|
+
let index = 0;
|
|
495
|
+
let components = 0;
|
|
496
|
+
if (scope[index] === "*") {
|
|
497
|
+
index += 1;
|
|
498
|
+
components += 1;
|
|
499
|
+
} else {
|
|
500
|
+
const next = consumeScopeIdentifier(scope, index);
|
|
501
|
+
if (next !== index) {
|
|
502
|
+
index = next;
|
|
503
|
+
components += 1;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
while (index < scope.length) {
|
|
507
|
+
const character = scope[index];
|
|
508
|
+
if (character === "." || character === "#") {
|
|
509
|
+
const next = consumeScopeIdentifier(scope, index + 1);
|
|
510
|
+
if (next === index + 1) return void 0;
|
|
511
|
+
dependencies.add(character === "." ? "class" : "id");
|
|
512
|
+
index = next;
|
|
513
|
+
components += 1;
|
|
514
|
+
} else if (character === "[") {
|
|
515
|
+
const parsed = parseScopeAttribute(scope, index);
|
|
516
|
+
if (parsed === void 0) return void 0;
|
|
517
|
+
dependencies.add(parsed.dependency);
|
|
518
|
+
index = parsed.nextIndex;
|
|
519
|
+
components += 1;
|
|
520
|
+
} else return;
|
|
521
|
+
}
|
|
522
|
+
return components > 0 ? { dependencies } : void 0;
|
|
523
|
+
}
|
|
524
|
+
var DomTranslator = class {
|
|
525
|
+
document;
|
|
526
|
+
diagnostics;
|
|
527
|
+
#scopes;
|
|
528
|
+
#attributeFilter;
|
|
529
|
+
#scopeAndProtectionAttributes;
|
|
530
|
+
#textOwnership = /* @__PURE__ */ new WeakMap();
|
|
531
|
+
#ownedTextNodes = /* @__PURE__ */ new Set();
|
|
532
|
+
#attributeOwnership = /* @__PURE__ */ new WeakMap();
|
|
533
|
+
#ownedAttributeElements = /* @__PURE__ */ new Set();
|
|
534
|
+
#reportedFailures = /* @__PURE__ */ new Set();
|
|
535
|
+
#locale;
|
|
536
|
+
#disposed = false;
|
|
537
|
+
#observer;
|
|
538
|
+
constructor(document, rules, diagnostics) {
|
|
539
|
+
this.document = document;
|
|
540
|
+
this.diagnostics = diagnostics;
|
|
541
|
+
const scopes = /* @__PURE__ */ new Map();
|
|
542
|
+
for (const rule of rules) {
|
|
543
|
+
let scopeRules = scopes.get(rule.scope);
|
|
544
|
+
if (scopeRules === void 0) {
|
|
545
|
+
scopeRules = {
|
|
546
|
+
text: /* @__PURE__ */ new Map(),
|
|
547
|
+
attributes: /* @__PURE__ */ new Map()
|
|
548
|
+
};
|
|
549
|
+
scopes.set(rule.scope, scopeRules);
|
|
550
|
+
}
|
|
551
|
+
if (!scopeRules.text.has(rule.source)) scopeRules.text.set(rule.source, rule);
|
|
552
|
+
for (const attribute of rule.attributes ?? []) {
|
|
553
|
+
if (!DOM_TRANSLATION_ATTRIBUTES.has(attribute)) continue;
|
|
554
|
+
let attributeRules = scopeRules.attributes.get(attribute);
|
|
555
|
+
if (attributeRules === void 0) {
|
|
556
|
+
attributeRules = /* @__PURE__ */ new Map();
|
|
557
|
+
scopeRules.attributes.set(attribute, attributeRules);
|
|
558
|
+
}
|
|
559
|
+
if (!attributeRules.has(rule.source)) attributeRules.set(rule.source, rule);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
const validatedScopes = Array.from(scopes, ([scope, indexed]) => ({
|
|
563
|
+
scope,
|
|
564
|
+
text: indexed.text,
|
|
565
|
+
attributes: indexed.attributes
|
|
566
|
+
})).flatMap((indexed) => {
|
|
567
|
+
if (indexed.scope === "global") return [{
|
|
568
|
+
...indexed,
|
|
569
|
+
dependencies: /* @__PURE__ */ new Set()
|
|
570
|
+
}];
|
|
571
|
+
const parsed = parseSupportedScopeSelector(indexed.scope);
|
|
572
|
+
if (parsed === void 0) {
|
|
573
|
+
this.diagnostics.error("invalid_dom_scope", `Invalid DOM translation scope "${indexed.scope}" was ignored.`);
|
|
574
|
+
return [];
|
|
575
|
+
}
|
|
576
|
+
try {
|
|
577
|
+
this.document.createDocumentFragment().querySelector(indexed.scope);
|
|
578
|
+
} catch {
|
|
579
|
+
this.diagnostics.error("invalid_dom_scope", `Invalid DOM translation scope "${indexed.scope}" was ignored.`);
|
|
580
|
+
return [];
|
|
581
|
+
}
|
|
582
|
+
return [{
|
|
583
|
+
...indexed,
|
|
584
|
+
dependencies: parsed.dependencies
|
|
585
|
+
}];
|
|
586
|
+
});
|
|
587
|
+
this.#scopes = validatedScopes.map(({ scope, text, attributes, dependencies }) => {
|
|
588
|
+
const safeAttributes = new Map(attributes);
|
|
589
|
+
for (const attribute of attributes.keys()) {
|
|
590
|
+
if (!dependencies.has(attribute)) continue;
|
|
591
|
+
safeAttributes.delete(attribute);
|
|
592
|
+
this.diagnostics.error("invalid_dom_rule", `DOM attribute "${attribute}" was ignored because it controls scope "${scope}".`);
|
|
593
|
+
}
|
|
594
|
+
return {
|
|
595
|
+
scope,
|
|
596
|
+
text,
|
|
597
|
+
attributes: safeAttributes,
|
|
598
|
+
dependencies
|
|
599
|
+
};
|
|
600
|
+
});
|
|
601
|
+
const translationAttributes = this.#scopes.flatMap(({ attributes }) => [...attributes.keys()]);
|
|
602
|
+
const scopeAndProtectionAttributes = new Set(SCOPE_AND_PROTECTION_ATTRIBUTES);
|
|
603
|
+
for (const { dependencies } of this.#scopes) for (const attribute of dependencies) scopeAndProtectionAttributes.add(attribute);
|
|
604
|
+
this.#scopeAndProtectionAttributes = scopeAndProtectionAttributes;
|
|
605
|
+
this.#attributeFilter = Array.from(/* @__PURE__ */ new Set([...translationAttributes, ...scopeAndProtectionAttributes]));
|
|
606
|
+
}
|
|
607
|
+
setLocale(locale) {
|
|
608
|
+
if (this.#disposed || this.#locale === locale) return;
|
|
609
|
+
const previousLocale = this.#locale;
|
|
610
|
+
this.#locale = locale;
|
|
611
|
+
if (locale !== "en") {
|
|
612
|
+
if (previousLocale === "en") {
|
|
613
|
+
this.#disconnectObserver();
|
|
614
|
+
this.#restoreOwnedValues();
|
|
615
|
+
}
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
for (const scopeRules of this.#scopes) {
|
|
619
|
+
let roots;
|
|
620
|
+
try {
|
|
621
|
+
roots = scopeRules.scope === "global" ? this.document.body === null ? [] : [this.document.body] : Array.from(this.document.querySelectorAll(scopeRules.scope));
|
|
622
|
+
roots = this.#topmostRoots(roots);
|
|
623
|
+
} catch {
|
|
624
|
+
this.#reportOnce(`initial-query:${scopeRules.scope}`, "dom_translation_failed", `DOM translation roots could not be found for scope "${scopeRules.scope}".`);
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
for (const root of roots) this.#translateRoot(root, scopeRules);
|
|
628
|
+
}
|
|
629
|
+
this.#connectObserver();
|
|
630
|
+
}
|
|
631
|
+
dispose() {
|
|
632
|
+
if (this.#disposed) return;
|
|
633
|
+
this.#disconnectObserver();
|
|
634
|
+
this.#restoreOwnedValues();
|
|
635
|
+
this.#disposed = true;
|
|
636
|
+
}
|
|
637
|
+
#disconnectObserver() {
|
|
638
|
+
try {
|
|
639
|
+
this.#observer?.disconnect();
|
|
640
|
+
} catch {}
|
|
641
|
+
this.#observer = void 0;
|
|
642
|
+
}
|
|
643
|
+
#restoreOwnedValues() {
|
|
644
|
+
for (const node of this.#ownedTextNodes) {
|
|
645
|
+
const ownership = this.#textOwnership.get(node);
|
|
646
|
+
try {
|
|
647
|
+
if (ownership !== void 0 && node.textContent === ownership.translated) node.textContent = ownership.original;
|
|
648
|
+
} catch {}
|
|
649
|
+
this.#textOwnership.delete(node);
|
|
650
|
+
}
|
|
651
|
+
this.#ownedTextNodes.clear();
|
|
652
|
+
for (const element of this.#ownedAttributeElements) {
|
|
653
|
+
const attributes = this.#attributeOwnership.get(element);
|
|
654
|
+
if (attributes !== void 0) for (const [attribute, ownership] of attributes) try {
|
|
655
|
+
if (element.getAttribute(attribute) !== ownership.translated) continue;
|
|
656
|
+
if (ownership.wasPresent) element.setAttribute(attribute, ownership.original ?? "");
|
|
657
|
+
else element.removeAttribute(attribute);
|
|
658
|
+
} catch {}
|
|
659
|
+
this.#attributeOwnership.delete(element);
|
|
660
|
+
}
|
|
661
|
+
this.#ownedAttributeElements.clear();
|
|
662
|
+
}
|
|
663
|
+
#connectObserver() {
|
|
664
|
+
if (this.#scopes.length === 0) return;
|
|
665
|
+
let body;
|
|
666
|
+
let MutationObserverConstructor;
|
|
667
|
+
try {
|
|
668
|
+
body = this.document.body;
|
|
669
|
+
MutationObserverConstructor = this.document.defaultView?.MutationObserver;
|
|
670
|
+
} catch {
|
|
671
|
+
this.#reportOnce("dom-environment", "dom_translation_failed", "DOM translation APIs are unavailable.");
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
if (body === null || MutationObserverConstructor === void 0) {
|
|
675
|
+
this.#reportOnce("dom-environment", "dom_translation_failed", "DOM translation observation is unavailable.");
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
try {
|
|
679
|
+
this.#observer = new MutationObserverConstructor((records) => {
|
|
680
|
+
try {
|
|
681
|
+
this.#processMutations(records);
|
|
682
|
+
} catch {
|
|
683
|
+
this.#reportOnce("mutation-callback", "dom_translation_failed", "Dynamic DOM translation failed.");
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
this.#observer.observe(body, {
|
|
687
|
+
childList: true,
|
|
688
|
+
subtree: true,
|
|
689
|
+
characterData: true,
|
|
690
|
+
attributes: true,
|
|
691
|
+
attributeFilter: [...this.#attributeFilter],
|
|
692
|
+
attributeOldValue: true
|
|
693
|
+
});
|
|
694
|
+
} catch {
|
|
695
|
+
this.#observer = void 0;
|
|
696
|
+
this.#reportOnce("observer-construction", "dom_translation_failed", "DOM mutation observation could not be started.");
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
#processMutations(records) {
|
|
700
|
+
if (this.#disposed || this.#locale !== "en") return;
|
|
701
|
+
const touchedSubtrees = /* @__PURE__ */ new Set();
|
|
702
|
+
for (const record of records) {
|
|
703
|
+
try {
|
|
704
|
+
if (record.type === "characterData") {
|
|
705
|
+
this.#translateChangedText(record.target);
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
if (record.type === "attributes") {
|
|
709
|
+
if (record.target.nodeType === ELEMENT_NODE && record.attributeName !== null) {
|
|
710
|
+
const element = record.target;
|
|
711
|
+
if (this.#scopeAndProtectionAttributes.has(record.attributeName)) {
|
|
712
|
+
this.#reconcileOwnedWithin(element);
|
|
713
|
+
if (this.#isConnectedToDocument(element)) this.#reapplyAfterStructuralAttribute(element, record.attributeName, record.oldValue);
|
|
714
|
+
} else this.#translateChangedAttribute(element, record.attributeName);
|
|
715
|
+
}
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
} catch {
|
|
719
|
+
this.#reportMutationFailure();
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
for (const node of record.removedNodes) touchedSubtrees.add(node);
|
|
723
|
+
for (const node of record.addedNodes) touchedSubtrees.add(node);
|
|
724
|
+
}
|
|
725
|
+
for (const node of touchedSubtrees) try {
|
|
726
|
+
this.#reconcileSubtree(node);
|
|
727
|
+
if (this.#isConnectedToDocument(node)) this.#translateAddedNode(node);
|
|
728
|
+
} catch {
|
|
729
|
+
this.#reportMutationFailure();
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
#reportMutationFailure() {
|
|
733
|
+
this.#reportOnce("mutation-callback", "dom_translation_failed", "Dynamic DOM translation failed.");
|
|
734
|
+
}
|
|
735
|
+
#translateChangedText(node) {
|
|
736
|
+
this.#reconcileTextOwnership(node);
|
|
737
|
+
const parent = node.parentElement;
|
|
738
|
+
if (parent === null) return;
|
|
739
|
+
for (const scopeRules of this.#scopes) if (this.#isInScope(parent, scopeRules.scope)) this.#translateText(node, scopeRules);
|
|
740
|
+
}
|
|
741
|
+
#translateChangedAttribute(element, attributeName) {
|
|
742
|
+
this.#reconcileAttributeOwnership(element, attributeName);
|
|
743
|
+
for (const scopeRules of this.#scopes) {
|
|
744
|
+
const rules = scopeRules.attributes.get(attributeName);
|
|
745
|
+
if (rules !== void 0 && this.#isInScope(element, scopeRules.scope)) this.#translateAttribute(element, attributeName, rules, scopeRules.scope);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
#translateAddedNode(node) {
|
|
749
|
+
if (node.nodeType === TEXT_NODE) {
|
|
750
|
+
this.#translateChangedText(node);
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
if (node.nodeType !== ELEMENT_NODE) return;
|
|
754
|
+
const element = node;
|
|
755
|
+
for (const scopeRules of this.#scopes) {
|
|
756
|
+
if (this.#isInScope(element, scopeRules.scope)) {
|
|
757
|
+
this.#translateRoot(element, scopeRules);
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
if (scopeRules.scope === "global") continue;
|
|
761
|
+
const roots = this.#topmostRoots([...element.querySelectorAll(scopeRules.scope)]);
|
|
762
|
+
for (const root of roots) this.#translateRoot(root, scopeRules);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
#topmostRoots(roots) {
|
|
766
|
+
if (roots.length < 2) return roots;
|
|
767
|
+
const candidates = new Set(roots);
|
|
768
|
+
return roots.filter((root) => {
|
|
769
|
+
let ancestor = root.parentElement;
|
|
770
|
+
while (ancestor !== null) {
|
|
771
|
+
if (candidates.has(ancestor)) return false;
|
|
772
|
+
ancestor = ancestor.parentElement;
|
|
773
|
+
}
|
|
774
|
+
return true;
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
#reconcileSubtree(root) {
|
|
778
|
+
this.#reconcileOwnedNode(root);
|
|
779
|
+
if (root.nodeType !== ELEMENT_NODE) return;
|
|
780
|
+
const walker = this.document.createTreeWalker(root, SHOW_ELEMENT_AND_TEXT);
|
|
781
|
+
let node = walker.nextNode();
|
|
782
|
+
while (node !== null) {
|
|
783
|
+
try {
|
|
784
|
+
this.#reconcileOwnedNode(node);
|
|
785
|
+
} catch {
|
|
786
|
+
this.#reportMutationFailure();
|
|
787
|
+
}
|
|
788
|
+
node = walker.nextNode();
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
#reconcileOwnedWithin(root) {
|
|
792
|
+
for (const node of this.#ownedTextNodes) {
|
|
793
|
+
if (!root.contains(node)) continue;
|
|
794
|
+
try {
|
|
795
|
+
this.#reconcileTextOwnership(node);
|
|
796
|
+
} catch {
|
|
797
|
+
this.#reportMutationFailure();
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
for (const element of this.#ownedAttributeElements) {
|
|
801
|
+
if (element !== root && !root.contains(element)) continue;
|
|
802
|
+
try {
|
|
803
|
+
this.#reconcileElementOwnership(element);
|
|
804
|
+
} catch {
|
|
805
|
+
this.#reportMutationFailure();
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
#reapplyAfterStructuralAttribute(target, attribute, oldValue) {
|
|
810
|
+
const protectionRemoved = this.#protectionWasRemoved(target, attribute, oldValue);
|
|
811
|
+
for (const rules of this.#scopes) {
|
|
812
|
+
let shouldTranslate = false;
|
|
813
|
+
if (rules.dependencies.has(attribute)) shouldTranslate = rules.scope !== "global" && target.matches(rules.scope);
|
|
814
|
+
if (!shouldTranslate && protectionRemoved && this.#isInScope(target, rules.scope)) shouldTranslate = true;
|
|
815
|
+
if (shouldTranslate) this.#translateRoot(target, rules);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
#protectionWasRemoved(target, attribute, oldValue) {
|
|
819
|
+
if (oldValue === null || !KNOWN_PROTECTION_ATTRIBUTES.has(attribute) || target.closest(ATTRIBUTE_PROTECTED_SURFACE_SELECTOR) !== null) return false;
|
|
820
|
+
if (attribute === "class") return CLASS_PROTECTION_PATTERN.test(oldValue);
|
|
821
|
+
if (attribute === "data-testid") return TEST_ID_PROTECTION_PATTERN.test(oldValue);
|
|
822
|
+
return true;
|
|
823
|
+
}
|
|
824
|
+
#reconcileOwnedNode(node) {
|
|
825
|
+
if (node.nodeType === TEXT_NODE) this.#reconcileTextOwnership(node);
|
|
826
|
+
else if (node.nodeType === ELEMENT_NODE) this.#reconcileElementOwnership(node);
|
|
827
|
+
}
|
|
828
|
+
#reconcileTextOwnership(node) {
|
|
829
|
+
const ownership = this.#textOwnership.get(node);
|
|
830
|
+
if (ownership === void 0) return;
|
|
831
|
+
if ((node.textContent ?? "") !== ownership.translated) {
|
|
832
|
+
this.#releaseTextOwnership(node);
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
const parent = node.parentElement;
|
|
836
|
+
if (parent !== null && this.#isConnectedToDocument(node) && this.#isInScope(parent, ownership.scope) && parent.closest(TEXT_PROTECTED_SURFACE_SELECTOR) === null) return;
|
|
837
|
+
node.textContent = ownership.original;
|
|
838
|
+
this.#releaseTextOwnership(node);
|
|
839
|
+
}
|
|
840
|
+
#reconcileElementOwnership(element) {
|
|
841
|
+
const attributes = this.#attributeOwnership.get(element);
|
|
842
|
+
if (attributes === void 0) return;
|
|
843
|
+
for (const attribute of [...attributes.keys()]) this.#reconcileAttributeOwnership(element, attribute);
|
|
844
|
+
}
|
|
845
|
+
#reconcileAttributeOwnership(element, attribute) {
|
|
846
|
+
const ownership = this.#attributeOwnership.get(element)?.get(attribute);
|
|
847
|
+
if (ownership === void 0) return;
|
|
848
|
+
if (element.getAttribute(attribute) !== ownership.translated) {
|
|
849
|
+
this.#releaseAttributeOwnership(element, attribute);
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
if (this.#isConnectedToDocument(element) && this.#isInScope(element, ownership.scope) && element.closest(ATTRIBUTE_PROTECTED_SURFACE_SELECTOR) === null) return;
|
|
853
|
+
if (ownership.wasPresent) element.setAttribute(attribute, ownership.original ?? "");
|
|
854
|
+
else element.removeAttribute(attribute);
|
|
855
|
+
this.#releaseAttributeOwnership(element, attribute);
|
|
856
|
+
}
|
|
857
|
+
#releaseTextOwnership(node) {
|
|
858
|
+
this.#textOwnership.delete(node);
|
|
859
|
+
this.#ownedTextNodes.delete(node);
|
|
860
|
+
}
|
|
861
|
+
#releaseAttributeOwnership(element, attribute) {
|
|
862
|
+
const attributes = this.#attributeOwnership.get(element);
|
|
863
|
+
if (attributes === void 0) return;
|
|
864
|
+
attributes.delete(attribute);
|
|
865
|
+
if (attributes.size !== 0) return;
|
|
866
|
+
this.#attributeOwnership.delete(element);
|
|
867
|
+
this.#ownedAttributeElements.delete(element);
|
|
868
|
+
}
|
|
869
|
+
#isConnectedToDocument(node) {
|
|
870
|
+
return this.document.body?.contains(node) === true;
|
|
871
|
+
}
|
|
872
|
+
#translateRoot(root, rules) {
|
|
873
|
+
try {
|
|
874
|
+
this.#translateTree(root, rules);
|
|
875
|
+
} catch {
|
|
876
|
+
this.#reportOnce(`root:${rules.scope}`, "dom_translation_failed", `DOM translation failed for a root in scope "${rules.scope}".`);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
#isInScope(element, scope) {
|
|
880
|
+
return scope === "global" ? this.document.body?.contains(element) === true : element.closest(scope) !== null;
|
|
881
|
+
}
|
|
882
|
+
#translateTree(root, rules) {
|
|
883
|
+
this.#translateAttributesSafely(root, rules);
|
|
884
|
+
for (const element of root.querySelectorAll("*")) this.#translateAttributesSafely(element, rules);
|
|
885
|
+
const walker = this.document.createTreeWalker(root, 4);
|
|
886
|
+
let node = walker.nextNode();
|
|
887
|
+
while (node !== null) {
|
|
888
|
+
try {
|
|
889
|
+
this.#translateText(node, rules);
|
|
890
|
+
} catch {
|
|
891
|
+
this.#reportDescendantFailure(rules.scope);
|
|
892
|
+
}
|
|
893
|
+
node = walker.nextNode();
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
#translateAttributesSafely(element, rules) {
|
|
897
|
+
try {
|
|
898
|
+
this.#translateAttributes(element, rules);
|
|
899
|
+
} catch {
|
|
900
|
+
this.#reportDescendantFailure(rules.scope);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
#reportDescendantFailure(scope) {
|
|
904
|
+
this.#reportOnce(`descendant:${scope}`, "dom_translation_failed", `A DOM descendant in scope "${scope}" could not be translated.`);
|
|
905
|
+
}
|
|
906
|
+
#translateText(node, rules) {
|
|
907
|
+
const value = node.textContent ?? "";
|
|
908
|
+
let ownership = this.#textOwnership.get(node);
|
|
909
|
+
if (ownership !== void 0 && value === ownership.translated) return;
|
|
910
|
+
const rule = rules.text.get(value.trim());
|
|
911
|
+
const parent = node.parentElement;
|
|
912
|
+
if (rule === void 0 || parent === null || parent.closest(TEXT_PROTECTED_SURFACE_SELECTOR) !== null) return;
|
|
913
|
+
const start = value.search(/\S/);
|
|
914
|
+
const end = value.search(/\s*$/);
|
|
915
|
+
const translated = `${value.slice(0, start)}${rule.target}${value.slice(end)}`;
|
|
916
|
+
if (ownership === void 0) {
|
|
917
|
+
ownership = {
|
|
918
|
+
original: value,
|
|
919
|
+
translated,
|
|
920
|
+
scope: rules.scope
|
|
921
|
+
};
|
|
922
|
+
this.#textOwnership.set(node, ownership);
|
|
923
|
+
this.#ownedTextNodes.add(node);
|
|
924
|
+
} else {
|
|
925
|
+
ownership.original = value;
|
|
926
|
+
ownership.translated = translated;
|
|
927
|
+
ownership.scope = rules.scope;
|
|
928
|
+
}
|
|
929
|
+
node.textContent = translated;
|
|
930
|
+
}
|
|
931
|
+
#translateAttributes(element, rules) {
|
|
932
|
+
if (rules.attributes.size === 0 || element.closest(ATTRIBUTE_PROTECTED_SURFACE_SELECTOR) !== null) return;
|
|
933
|
+
for (const [attribute, attributeRules] of rules.attributes) this.#translateAttribute(element, attribute, attributeRules, rules.scope);
|
|
934
|
+
}
|
|
935
|
+
#translateAttribute(element, attribute, rules, scope) {
|
|
936
|
+
if (element.closest(ATTRIBUTE_PROTECTED_SURFACE_SELECTOR) !== null) return;
|
|
937
|
+
const value = element.getAttribute(attribute);
|
|
938
|
+
if (value === null) return;
|
|
939
|
+
let attributes = this.#attributeOwnership.get(element);
|
|
940
|
+
let ownership = attributes?.get(attribute);
|
|
941
|
+
if (ownership !== void 0 && value === ownership.translated) return;
|
|
942
|
+
const rule = rules.get(value);
|
|
943
|
+
if (rule === void 0) return;
|
|
944
|
+
if (attributes === void 0) {
|
|
945
|
+
attributes = /* @__PURE__ */ new Map();
|
|
946
|
+
this.#attributeOwnership.set(element, attributes);
|
|
947
|
+
this.#ownedAttributeElements.add(element);
|
|
948
|
+
}
|
|
949
|
+
if (ownership === void 0) {
|
|
950
|
+
ownership = {
|
|
951
|
+
wasPresent: element.hasAttribute(attribute),
|
|
952
|
+
original: value,
|
|
953
|
+
translated: rule.target,
|
|
954
|
+
scope
|
|
955
|
+
};
|
|
956
|
+
attributes.set(attribute, ownership);
|
|
957
|
+
} else {
|
|
958
|
+
ownership.wasPresent = element.hasAttribute(attribute);
|
|
959
|
+
ownership.original = value;
|
|
960
|
+
ownership.translated = rule.target;
|
|
961
|
+
ownership.scope = scope;
|
|
962
|
+
}
|
|
963
|
+
element.setAttribute(attribute, rule.target);
|
|
964
|
+
}
|
|
965
|
+
#reportOnce(key, code, message) {
|
|
966
|
+
if (this.#reportedFailures.has(key)) return;
|
|
967
|
+
this.#reportedFailures.add(key);
|
|
968
|
+
try {
|
|
969
|
+
this.diagnostics.error(code, message);
|
|
970
|
+
} catch {}
|
|
971
|
+
}
|
|
972
|
+
};
|
|
973
|
+
//#endregion
|
|
974
|
+
//#region src/runtime/interpolate.ts
|
|
975
|
+
function interpolate(template, params) {
|
|
976
|
+
if (params === void 0) return template;
|
|
977
|
+
return template.replace(/\{(\w+)\}/g, (match, name) => {
|
|
978
|
+
try {
|
|
979
|
+
if (!Object.hasOwn(params, name)) return match;
|
|
980
|
+
const value = params[name];
|
|
981
|
+
if (value === null || value === void 0) return match;
|
|
982
|
+
return String(value);
|
|
983
|
+
} catch {
|
|
984
|
+
return match;
|
|
985
|
+
}
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
//#endregion
|
|
989
|
+
//#region src/runtime/locale-hook.ts
|
|
990
|
+
const installedHooks = /* @__PURE__ */ new WeakMap();
|
|
991
|
+
function installLocaleHook(runtime, registry, diagnostics) {
|
|
992
|
+
const adapter = adaptDshLocaleRuntime(runtime);
|
|
993
|
+
if (adapter === void 0) {
|
|
994
|
+
diagnostics.error("incompatible_locale_runtime", "The DSH locale runtime is incompatible; locale overrides were not installed.");
|
|
995
|
+
return () => true;
|
|
996
|
+
}
|
|
997
|
+
if (installedHooks.has(adapter.runtime)) {
|
|
998
|
+
diagnostics.warning("duplicate_locale_hook", "A locale override hook is already installed on this DSH runtime.");
|
|
999
|
+
return () => true;
|
|
1000
|
+
}
|
|
1001
|
+
let snapshotFailureDiagnosed = false;
|
|
1002
|
+
const wrapper = function(...args) {
|
|
1003
|
+
const [namespace, key, params] = args;
|
|
1004
|
+
const locale = adapter.getActiveLocale();
|
|
1005
|
+
if (!locale.ok && !snapshotFailureDiagnosed) {
|
|
1006
|
+
snapshotFailureDiagnosed = true;
|
|
1007
|
+
diagnostics.error("locale_snapshot_failed", "The active DSH locale could not be read; the original translator will be used.");
|
|
1008
|
+
}
|
|
1009
|
+
if (locale.ok && locale.value === "en") {
|
|
1010
|
+
const entry = registry.resolveEntry(locale.value, namespace, key);
|
|
1011
|
+
if (entry !== void 0) {
|
|
1012
|
+
diagnostics.debug("locale_override_hit", `Locale override hit locale=${locale.value} namespace=${namespace} key=${key} pack=${entry.packId}.`);
|
|
1013
|
+
return interpolate(entry.value, params);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
return adapter.callOriginal(args);
|
|
1017
|
+
};
|
|
1018
|
+
installedHooks.set(adapter.runtime, wrapper);
|
|
1019
|
+
const installed = adapter.install(wrapper);
|
|
1020
|
+
if (!installed.ok) {
|
|
1021
|
+
diagnostics.error("locale_hook_install_failed", "The DSH locale translator could not be patched; locale overrides were not installed.");
|
|
1022
|
+
if (!installed.runtimeMayBePatched) {
|
|
1023
|
+
installedHooks.delete(adapter.runtime);
|
|
1024
|
+
return () => true;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
let disposed = false;
|
|
1028
|
+
let restoreFailureDiagnosed = false;
|
|
1029
|
+
return () => {
|
|
1030
|
+
if (disposed) return true;
|
|
1031
|
+
const restored = adapter.restoreOriginalIfCurrent(wrapper);
|
|
1032
|
+
if (!restored.ok && !restoreFailureDiagnosed) {
|
|
1033
|
+
restoreFailureDiagnosed = true;
|
|
1034
|
+
diagnostics.error("locale_hook_restore_failed", "The DSH locale translator could not be inspected or restored during disposal.");
|
|
1035
|
+
}
|
|
1036
|
+
if (!restored.ok) return false;
|
|
1037
|
+
disposed = true;
|
|
1038
|
+
installedHooks.delete(adapter.runtime);
|
|
1039
|
+
return true;
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
//#endregion
|
|
1043
|
+
//#region src/client/index.ts
|
|
1044
|
+
const inject = ["locale"];
|
|
1045
|
+
const sharedInstallations = /* @__PURE__ */ new WeakMap();
|
|
1046
|
+
const terminalCleanup = () => true;
|
|
1047
|
+
function createCleanupResource(code, message) {
|
|
1048
|
+
return {
|
|
1049
|
+
state: "done",
|
|
1050
|
+
run: terminalCleanup,
|
|
1051
|
+
diagnosed: false,
|
|
1052
|
+
code,
|
|
1053
|
+
message
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
function armCleanup(resource, run) {
|
|
1057
|
+
resource.state = "idle";
|
|
1058
|
+
resource.run = run;
|
|
1059
|
+
}
|
|
1060
|
+
function attemptCleanup(installation, resource) {
|
|
1061
|
+
if (resource.state !== "idle") return;
|
|
1062
|
+
resource.state = "running";
|
|
1063
|
+
try {
|
|
1064
|
+
if (resource.run() === false) {
|
|
1065
|
+
resource.state = "idle";
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
resource.state = "done";
|
|
1069
|
+
resource.run = terminalCleanup;
|
|
1070
|
+
} catch {
|
|
1071
|
+
resource.state = "idle";
|
|
1072
|
+
if (resource.diagnosed) return;
|
|
1073
|
+
resource.diagnosed = true;
|
|
1074
|
+
installation.diagnostics.error(resource.code, resource.message);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
function cleanupInstallation(installation) {
|
|
1078
|
+
attemptCleanup(installation, installation.domCleanup);
|
|
1079
|
+
attemptCleanup(installation, installation.listenerCleanup);
|
|
1080
|
+
attemptCleanup(installation, installation.localeCleanup);
|
|
1081
|
+
if (!(installation.domCleanup.state === "done" && installation.listenerCleanup.state === "done" && installation.localeCleanup.state === "done")) return false;
|
|
1082
|
+
const { runtimeKey } = installation;
|
|
1083
|
+
if (runtimeKey !== void 0 && sharedInstallations.get(runtimeKey) === installation) sharedInstallations.delete(runtimeKey);
|
|
1084
|
+
return true;
|
|
1085
|
+
}
|
|
1086
|
+
function createLease(installation) {
|
|
1087
|
+
let released = false;
|
|
1088
|
+
let terminal = false;
|
|
1089
|
+
return () => {
|
|
1090
|
+
if (terminal) return;
|
|
1091
|
+
if (!released) {
|
|
1092
|
+
released = true;
|
|
1093
|
+
installation.refs -= 1;
|
|
1094
|
+
if (installation.refs > 0) {
|
|
1095
|
+
terminal = true;
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
if (cleanupInstallation(installation)) terminal = true;
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
function selectDocument(options) {
|
|
1103
|
+
if (options.document !== void 0) return options.document ?? void 0;
|
|
1104
|
+
return typeof document === "undefined" ? void 0 : document;
|
|
1105
|
+
}
|
|
1106
|
+
function describeCount(count, singular, plural) {
|
|
1107
|
+
return `${count} ${count === 1 ? singular : plural}`;
|
|
1108
|
+
}
|
|
1109
|
+
function apply(ctx, options = {}) {
|
|
1110
|
+
let localeRuntime;
|
|
1111
|
+
try {
|
|
1112
|
+
localeRuntime = ctx.locale;
|
|
1113
|
+
} catch {
|
|
1114
|
+
localeRuntime = void 0;
|
|
1115
|
+
}
|
|
1116
|
+
const localeAdapter = adaptDshLocaleRuntime(localeRuntime);
|
|
1117
|
+
const runtimeKey = localeAdapter?.runtime;
|
|
1118
|
+
if (runtimeKey !== void 0) {
|
|
1119
|
+
const existing = sharedInstallations.get(runtimeKey);
|
|
1120
|
+
if (existing !== void 0) {
|
|
1121
|
+
existing.refs += 1;
|
|
1122
|
+
return createLease(existing);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
const diagnostics = new Diagnostics(options.logger ?? console, options.debug === void 0 ? {} : { debug: options.debug });
|
|
1126
|
+
const registry = new TranslationPackRegistry(diagnostics);
|
|
1127
|
+
const installation = {
|
|
1128
|
+
refs: 1,
|
|
1129
|
+
runtimeKey,
|
|
1130
|
+
diagnostics,
|
|
1131
|
+
domCleanup: createCleanupResource("dom_dispose_failed", "DOM translation could not be fully restored during disposal."),
|
|
1132
|
+
listenerCleanup: createCleanupResource("locale_listener_dispose_failed", "The locale change listener could not be removed during disposal."),
|
|
1133
|
+
localeCleanup: createCleanupResource("locale_hook_dispose_failed", "The locale override hook could not be restored during disposal.")
|
|
1134
|
+
};
|
|
1135
|
+
if (runtimeKey !== void 0) sharedInstallations.set(runtimeKey, installation);
|
|
1136
|
+
for (const pack of translationPacks) try {
|
|
1137
|
+
registry.register(pack);
|
|
1138
|
+
} catch {
|
|
1139
|
+
diagnostics.error("pack_registration_failed", `Pack "${pack.id}" could not be registered and was ignored.`);
|
|
1140
|
+
}
|
|
1141
|
+
const stats = registry.getStats();
|
|
1142
|
+
diagnostics.info("startup_summary", `Loaded ${describeCount(stats.packs, "pack", "packs")}, ${describeCount(stats.localeOverrides, "locale override", "locale overrides")}, and ${describeCount(stats.domRules, "DOM rule", "DOM rules")}.`);
|
|
1143
|
+
try {
|
|
1144
|
+
armCleanup(installation.localeCleanup, installLocaleHook(localeRuntime, registry, diagnostics));
|
|
1145
|
+
} catch {
|
|
1146
|
+
diagnostics.error("locale_hook_startup_failed", "The locale override hook could not be started.");
|
|
1147
|
+
}
|
|
1148
|
+
let candidateDocument;
|
|
1149
|
+
try {
|
|
1150
|
+
candidateDocument = selectDocument(options);
|
|
1151
|
+
} catch {
|
|
1152
|
+
candidateDocument = void 0;
|
|
1153
|
+
}
|
|
1154
|
+
let domAvailable = false;
|
|
1155
|
+
if (candidateDocument !== void 0) try {
|
|
1156
|
+
domAvailable = candidateDocument.body !== null && candidateDocument.defaultView?.MutationObserver !== void 0;
|
|
1157
|
+
} catch {}
|
|
1158
|
+
if (!domAvailable) diagnostics.warning("dom_unavailable", "DOM translation is unavailable; locale overrides remain active.");
|
|
1159
|
+
let domTranslator;
|
|
1160
|
+
if (domAvailable && candidateDocument !== void 0 && localeAdapter) try {
|
|
1161
|
+
const translator = new DomTranslator(candidateDocument, registry.getDomRules("en"), diagnostics);
|
|
1162
|
+
domTranslator = translator;
|
|
1163
|
+
armCleanup(installation.domCleanup, () => {
|
|
1164
|
+
translator.dispose();
|
|
1165
|
+
return true;
|
|
1166
|
+
});
|
|
1167
|
+
} catch {
|
|
1168
|
+
diagnostics.error("dom_startup_failed", "DOM translation could not be started; locale overrides remain active.");
|
|
1169
|
+
}
|
|
1170
|
+
if (domTranslator !== void 0 && localeAdapter !== void 0) {
|
|
1171
|
+
const initialLocale = localeAdapter.getActiveLocale();
|
|
1172
|
+
if (initialLocale.ok) try {
|
|
1173
|
+
domTranslator.setLocale(initialLocale.value);
|
|
1174
|
+
} catch {
|
|
1175
|
+
diagnostics.error("dom_initial_locale_failed", "The initial locale could not be applied to DOM translation.");
|
|
1176
|
+
}
|
|
1177
|
+
else diagnostics.error("initial_locale_snapshot_failed", "The initial locale could not be read; DOM translation was not activated.");
|
|
1178
|
+
}
|
|
1179
|
+
if (domTranslator !== void 0) {
|
|
1180
|
+
let switchingFailureDiagnosed = false;
|
|
1181
|
+
try {
|
|
1182
|
+
const unsubscribe = ctx.on("locale/change", (snapshot) => {
|
|
1183
|
+
try {
|
|
1184
|
+
domTranslator?.setLocale(snapshot.active);
|
|
1185
|
+
} catch {
|
|
1186
|
+
if (switchingFailureDiagnosed) return;
|
|
1187
|
+
switchingFailureDiagnosed = true;
|
|
1188
|
+
diagnostics.error("locale_switch_failed", "A locale change could not be applied to DOM translation.");
|
|
1189
|
+
}
|
|
1190
|
+
});
|
|
1191
|
+
if (typeof unsubscribe === "function") armCleanup(installation.listenerCleanup, () => {
|
|
1192
|
+
unsubscribe();
|
|
1193
|
+
return true;
|
|
1194
|
+
});
|
|
1195
|
+
else diagnostics.error("locale_listener_failed", "The locale change listener could not be registered; DOM language switching is disabled.");
|
|
1196
|
+
} catch {
|
|
1197
|
+
diagnostics.error("locale_listener_failed", "The locale change listener could not be registered; DOM language switching is disabled.");
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
return createLease(installation);
|
|
1201
|
+
}
|
|
1202
|
+
//#endregion
|
|
1203
|
+
exports.apply = apply;
|
|
1204
|
+
exports.inject = inject;
|
|
1205
|
+
return module.exports;
|
|
1206
|
+
}
|
|
1207
|
+
});
|