translate-shield 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 +221 -0
- package/dist/dom-crorgc8a.js +71 -0
- package/dist/dom-dauvp8pk.cjs +76 -0
- package/dist/index.cjs +508 -0
- package/dist/index.d.cts +21 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +505 -0
- package/dist/react.cjs +78 -0
- package/dist/react.d.cts +37 -0
- package/dist/react.d.ts +37 -0
- package/dist/react.js +75 -0
- package/dist/types-fjbg60f8.d.ts +28 -0
- package/package.json +112 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
2
|
+
|
|
3
|
+
var dom = require('./dom-dauvp8pk.cjs');
|
|
4
|
+
|
|
5
|
+
const NATIVE = /\[native code\]/;
|
|
6
|
+
const isPatched = (candidate)=>typeof candidate === 'function' && !NATIVE.test(Function.prototype.toString.call(candidate));
|
|
7
|
+
const setterOf = (target, property)=>Object.getOwnPropertyDescriptor(target, property)?.set;
|
|
8
|
+
/**
|
|
9
|
+
* Names the DOM surfaces another shim has already replaced.
|
|
10
|
+
*
|
|
11
|
+
* Two shims on one page is not a crash, it is a silent draw. Ours writes the new
|
|
12
|
+
* value into the translator's wrapper; a restore-based shim puts the original
|
|
13
|
+
* node back and deletes that wrapper. Whichever runs second decides, and the
|
|
14
|
+
* loser degrades to doing nothing. Reporting it is the only way a developer
|
|
15
|
+
* finds out, because nothing throws.
|
|
16
|
+
*/ const findConflicts = ()=>{
|
|
17
|
+
if (typeof Node === 'undefined') return [];
|
|
18
|
+
const surfaces = [
|
|
19
|
+
[
|
|
20
|
+
'Node.prototype.removeChild',
|
|
21
|
+
Node.prototype.removeChild
|
|
22
|
+
],
|
|
23
|
+
[
|
|
24
|
+
'Node.prototype.insertBefore',
|
|
25
|
+
Node.prototype.insertBefore
|
|
26
|
+
],
|
|
27
|
+
[
|
|
28
|
+
'Node.prototype.replaceChild',
|
|
29
|
+
Node.prototype.replaceChild
|
|
30
|
+
],
|
|
31
|
+
[
|
|
32
|
+
'Node.prototype.nodeValue',
|
|
33
|
+
setterOf(Node.prototype, 'nodeValue')
|
|
34
|
+
],
|
|
35
|
+
[
|
|
36
|
+
'Node.prototype.textContent',
|
|
37
|
+
setterOf(Node.prototype, 'textContent')
|
|
38
|
+
],
|
|
39
|
+
[
|
|
40
|
+
'CharacterData.prototype.data',
|
|
41
|
+
setterOf(CharacterData.prototype, 'data')
|
|
42
|
+
],
|
|
43
|
+
[
|
|
44
|
+
'Element.prototype.attachShadow',
|
|
45
|
+
Element.prototype.attachShadow
|
|
46
|
+
]
|
|
47
|
+
];
|
|
48
|
+
return surfaces.filter(([, value])=>isPatched(value)).map(([name])=>name);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const NUMBER_PATTERN = /\d+(?:[.,\s]\d+)*/g;
|
|
52
|
+
const TRANSLATED_NUMBER_PATTERN = /\p{Nd}+(?:[.,\s]\p{Nd}+)*/gu;
|
|
53
|
+
const DIGIT_ZEROS = [
|
|
54
|
+
0x0030,
|
|
55
|
+
0x0660,
|
|
56
|
+
0x06f0,
|
|
57
|
+
0x0966,
|
|
58
|
+
0x09e6,
|
|
59
|
+
0x0a66,
|
|
60
|
+
0x0ae6,
|
|
61
|
+
0x0b66,
|
|
62
|
+
0x0be6,
|
|
63
|
+
0x0c66,
|
|
64
|
+
0x0ce6,
|
|
65
|
+
0x0d66,
|
|
66
|
+
0x0e50,
|
|
67
|
+
0x0ed0,
|
|
68
|
+
0x0f20,
|
|
69
|
+
0x1040,
|
|
70
|
+
0x17e0,
|
|
71
|
+
0x1810,
|
|
72
|
+
0xff10
|
|
73
|
+
];
|
|
74
|
+
const numbersOf = (value)=>value.match(NUMBER_PATTERN) ?? [];
|
|
75
|
+
const skeletonOf = (value)=>value.replace(NUMBER_PATTERN, '');
|
|
76
|
+
const digitsOf = (value)=>value.replace(/\D/g, '');
|
|
77
|
+
const zeroCodePointFor = (codePoint)=>{
|
|
78
|
+
for (const zero of DIGIT_ZEROS){
|
|
79
|
+
if (codePoint >= zero && codePoint <= zero + 9) return zero;
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
};
|
|
83
|
+
const asNumber = (value)=>{
|
|
84
|
+
const digits = digitsOf(value);
|
|
85
|
+
if (!digits) return null;
|
|
86
|
+
const fraction = value.match(/[.,](\d{1,2})$/)?.[1];
|
|
87
|
+
if (!fraction) return Number(digits);
|
|
88
|
+
const whole = digits.slice(0, digits.length - fraction.length);
|
|
89
|
+
return Number(`${whole || '0'}.${fraction}`);
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* True when swapping one value for another cannot change the grammatical number
|
|
93
|
+
* of the sentence. Russian "4 lampochki" (few) and "7 lampochek" (many) take
|
|
94
|
+
* different noun forms, so a digit swap there produces fluent-looking wrong text.
|
|
95
|
+
*/ const ruleCache = new Map();
|
|
96
|
+
const pluralRulesFor = (locale)=>{
|
|
97
|
+
const cached = ruleCache.get(locale);
|
|
98
|
+
if (cached !== undefined) return cached;
|
|
99
|
+
let rules = null;
|
|
100
|
+
try {
|
|
101
|
+
rules = new Intl.PluralRules(locale);
|
|
102
|
+
} catch {
|
|
103
|
+
rules = null;
|
|
104
|
+
}
|
|
105
|
+
ruleCache.set(locale, rules);
|
|
106
|
+
return rules;
|
|
107
|
+
};
|
|
108
|
+
const keepsPluralCategory = (previous, next, locale)=>{
|
|
109
|
+
if (!locale) return false;
|
|
110
|
+
const rules = pluralRulesFor(locale);
|
|
111
|
+
if (!rules) return false;
|
|
112
|
+
return previous.every((value, index)=>{
|
|
113
|
+
const before = asNumber(value);
|
|
114
|
+
const after = asNumber(next[index] ?? '');
|
|
115
|
+
if (before === null || after === null) return false;
|
|
116
|
+
return rules.select(before) === rules.select(after);
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* Writes new digits into a number the translator already formatted, keeping that
|
|
121
|
+
* number's own separators and digit system, so "19,99" updated from "29.99"
|
|
122
|
+
* becomes "29,99" rather than "29.99".
|
|
123
|
+
*/ const reskinNumber = (translatedNumber, sourceDigits)=>{
|
|
124
|
+
const characters = Array.from(translatedNumber);
|
|
125
|
+
const digitCount = characters.filter((character)=>zeroCodePointFor(character.codePointAt(0) ?? 0) !== null).length;
|
|
126
|
+
if (digitCount !== sourceDigits.length) return null;
|
|
127
|
+
let index = 0;
|
|
128
|
+
return characters.map((character)=>{
|
|
129
|
+
const zero = zeroCodePointFor(character.codePointAt(0) ?? 0);
|
|
130
|
+
if (zero === null) return character;
|
|
131
|
+
const digit = Number(sourceDigits[index] ?? '0');
|
|
132
|
+
index += 1;
|
|
133
|
+
return String.fromCodePoint(zero + digit);
|
|
134
|
+
}).join('');
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* Merges a new source value into an already translated string. The merge only
|
|
138
|
+
* happens when it cannot change meaning: the sentence around the numbers is
|
|
139
|
+
* identical, every number keeps its plural category in the target language, and
|
|
140
|
+
* the digits line up one for one. Otherwise the correct value is returned in the
|
|
141
|
+
* source language, because a visibly untranslated number beats a fluent-looking
|
|
142
|
+
* wrong one.
|
|
143
|
+
*/ const mergeIntoTranslated = (previousSource, nextSource, translated, locale)=>{
|
|
144
|
+
if (!translated) return nextSource;
|
|
145
|
+
if (previousSource === nextSource) return translated;
|
|
146
|
+
if (skeletonOf(previousSource) !== skeletonOf(nextSource)) return nextSource;
|
|
147
|
+
const previousNumbers = numbersOf(previousSource);
|
|
148
|
+
const nextNumbers = numbersOf(nextSource);
|
|
149
|
+
if (previousNumbers.length !== nextNumbers.length) return nextSource;
|
|
150
|
+
if (!keepsPluralCategory(previousNumbers, nextNumbers, locale)) return nextSource;
|
|
151
|
+
const translatedNumbers = translated.match(TRANSLATED_NUMBER_PATTERN) ?? [];
|
|
152
|
+
if (translatedNumbers.length !== nextNumbers.length) return nextSource;
|
|
153
|
+
const replacements = translatedNumbers.map((translatedNumber, index)=>reskinNumber(translatedNumber, digitsOf(nextNumbers[index] ?? '')));
|
|
154
|
+
if (replacements.includes(null)) return nextSource;
|
|
155
|
+
let cursor = 0;
|
|
156
|
+
return translated.replace(TRANSLATED_NUMBER_PATTERN, (match)=>{
|
|
157
|
+
const replacement = replacements[cursor];
|
|
158
|
+
cursor += 1;
|
|
159
|
+
return replacement ?? match;
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const INTERCEPTED_PROPERTIES = [
|
|
164
|
+
'nodeValue',
|
|
165
|
+
'data',
|
|
166
|
+
'textContent'
|
|
167
|
+
];
|
|
168
|
+
let links = new WeakMap();
|
|
169
|
+
let wrappers = new WeakSet();
|
|
170
|
+
let forwarders = null;
|
|
171
|
+
let natives = null;
|
|
172
|
+
const nativeDescriptorOf = (property)=>{
|
|
173
|
+
if (property === 'data') return Object.getOwnPropertyDescriptor(CharacterData.prototype, 'data');
|
|
174
|
+
return Object.getOwnPropertyDescriptor(Node.prototype, property);
|
|
175
|
+
};
|
|
176
|
+
/**
|
|
177
|
+
* Resolves the native accessors once, on first use rather than at module scope.
|
|
178
|
+
* Reading `Node.prototype` when the module loads throws `ReferenceError: Node is
|
|
179
|
+
* not defined` on the server, which would break any app importing this from
|
|
180
|
+
* shared code, so nothing here may run before a DOM exists.
|
|
181
|
+
*/ const nativeAccessors = ()=>{
|
|
182
|
+
if (natives) return natives;
|
|
183
|
+
if (typeof Node === 'undefined') return null;
|
|
184
|
+
natives = {
|
|
185
|
+
nodeValue: nativeDescriptorOf('nodeValue'),
|
|
186
|
+
data: nativeDescriptorOf('data'),
|
|
187
|
+
textContent: nativeDescriptorOf('textContent')
|
|
188
|
+
};
|
|
189
|
+
return natives;
|
|
190
|
+
};
|
|
191
|
+
const readProperty = (node, property)=>{
|
|
192
|
+
const getter = nativeAccessors()?.[property]?.get;
|
|
193
|
+
if (!getter) return '';
|
|
194
|
+
return String(getter.call(node) ?? '');
|
|
195
|
+
};
|
|
196
|
+
const forwardWrite = (node, next)=>{
|
|
197
|
+
const link = links.get(node);
|
|
198
|
+
if (!link?.wrapper.isConnected) return;
|
|
199
|
+
const translated = readProperty(link.wrapper, 'textContent');
|
|
200
|
+
const merged = mergeIntoTranslated(link.sourceText, next, translated, document.documentElement.lang);
|
|
201
|
+
link.sourceText = next;
|
|
202
|
+
if (merged === translated) return;
|
|
203
|
+
nativeAccessors()?.textContent?.set?.call(link.wrapper, merged);
|
|
204
|
+
};
|
|
205
|
+
/**
|
|
206
|
+
* Builds the forwarding accessors once per property. They close over nothing
|
|
207
|
+
* per node, so a full-page translation linking thousands of text nodes reuses
|
|
208
|
+
* three descriptors instead of allocating nine objects for each one.
|
|
209
|
+
*/ const forwardingDescriptors = ()=>{
|
|
210
|
+
if (forwarders) return forwarders;
|
|
211
|
+
const accessors = nativeAccessors();
|
|
212
|
+
if (!accessors) return null;
|
|
213
|
+
const build = (property)=>{
|
|
214
|
+
const native = accessors[property];
|
|
215
|
+
if (!native?.get || !native.set) return null;
|
|
216
|
+
const nativeGet = native.get;
|
|
217
|
+
const nativeSet = native.set;
|
|
218
|
+
return {
|
|
219
|
+
configurable: true,
|
|
220
|
+
enumerable: false,
|
|
221
|
+
get () {
|
|
222
|
+
return nativeGet.call(this);
|
|
223
|
+
},
|
|
224
|
+
set (value) {
|
|
225
|
+
nativeSet.call(this, value);
|
|
226
|
+
forwardWrite(this, String(value ?? ''));
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
};
|
|
230
|
+
const nodeValue = build('nodeValue');
|
|
231
|
+
const data = build('data');
|
|
232
|
+
const textContent = build('textContent');
|
|
233
|
+
if (!nodeValue || !data || !textContent) return null;
|
|
234
|
+
forwarders = {
|
|
235
|
+
nodeValue,
|
|
236
|
+
data,
|
|
237
|
+
textContent
|
|
238
|
+
};
|
|
239
|
+
return forwarders;
|
|
240
|
+
};
|
|
241
|
+
/**
|
|
242
|
+
* Links a TextNode the translator detached to the element that replaced it, so
|
|
243
|
+
* later framework writes reach the node the user actually sees.
|
|
244
|
+
*/ const linkNodes = (detached, wrapper)=>{
|
|
245
|
+
wrappers.add(wrapper);
|
|
246
|
+
const existing = links.get(detached);
|
|
247
|
+
if (existing) {
|
|
248
|
+
existing.wrapper = wrapper;
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
links.set(detached, {
|
|
252
|
+
wrapper,
|
|
253
|
+
sourceText: readProperty(detached, 'nodeValue')
|
|
254
|
+
});
|
|
255
|
+
const descriptors = forwardingDescriptors();
|
|
256
|
+
if (!descriptors) return;
|
|
257
|
+
INTERCEPTED_PROPERTIES.forEach((property)=>{
|
|
258
|
+
Object.defineProperty(detached, property, descriptors[property]);
|
|
259
|
+
});
|
|
260
|
+
};
|
|
261
|
+
const wrapperFor = (node)=>{
|
|
262
|
+
if (!dom.isText(node)) return null;
|
|
263
|
+
const link = links.get(node);
|
|
264
|
+
if (!link?.wrapper.isConnected) return null;
|
|
265
|
+
return link.wrapper;
|
|
266
|
+
};
|
|
267
|
+
const isWrapper = (node)=>dom.isElement(node) && wrappers.has(node);
|
|
268
|
+
/**
|
|
269
|
+
* Drops every link, which turns each intercepted accessor into a pass-through:
|
|
270
|
+
* the getter still delegates to the native one and the setter forwards nowhere.
|
|
271
|
+
* The own properties stay defined, because deleting them would need a list of
|
|
272
|
+
* every node ever linked, and that list grows for the lifetime of a page that
|
|
273
|
+
* keeps translating.
|
|
274
|
+
*/ const releaseInterceptors = ()=>{
|
|
275
|
+
links = new WeakMap();
|
|
276
|
+
wrappers = new WeakSet();
|
|
277
|
+
forwarders = null;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Makes the three DOM calls that take a child reference survive nodes a
|
|
282
|
+
* translator detached. Real children take the native path untouched; detached
|
|
283
|
+
* ones are redirected to the visible wrapper instead of throwing NotFoundError.
|
|
284
|
+
*
|
|
285
|
+
* `appendChild` is deliberately not patched. It takes no reference child, so it
|
|
286
|
+
* cannot raise the NotFoundError this repairs; wrapping it would cost every
|
|
287
|
+
* insertion on the page and buy nothing.
|
|
288
|
+
*/ const patchDom = (report)=>{
|
|
289
|
+
const nativeRemoveChild = Node.prototype.removeChild;
|
|
290
|
+
const nativeInsertBefore = Node.prototype.insertBefore;
|
|
291
|
+
const nativeReplaceChild = Node.prototype.replaceChild;
|
|
292
|
+
Node.prototype.removeChild = function removeChild(child) {
|
|
293
|
+
if (child.parentNode === this) {
|
|
294
|
+
nativeRemoveChild.call(this, child);
|
|
295
|
+
return child;
|
|
296
|
+
}
|
|
297
|
+
const wrapper = wrapperFor(child);
|
|
298
|
+
if (wrapper && wrapper.parentNode === this) {
|
|
299
|
+
nativeRemoveChild.call(this, wrapper);
|
|
300
|
+
report({
|
|
301
|
+
method: 'removeChild',
|
|
302
|
+
redirected: true
|
|
303
|
+
});
|
|
304
|
+
return child;
|
|
305
|
+
}
|
|
306
|
+
report({
|
|
307
|
+
method: 'removeChild',
|
|
308
|
+
redirected: false
|
|
309
|
+
});
|
|
310
|
+
return child;
|
|
311
|
+
};
|
|
312
|
+
Node.prototype.insertBefore = function insertBefore(node, reference) {
|
|
313
|
+
if (!reference || reference.parentNode === this) {
|
|
314
|
+
nativeInsertBefore.call(this, node, reference);
|
|
315
|
+
return node;
|
|
316
|
+
}
|
|
317
|
+
const wrapper = wrapperFor(reference);
|
|
318
|
+
if (wrapper && wrapper.parentNode === this) {
|
|
319
|
+
nativeInsertBefore.call(this, node, wrapper);
|
|
320
|
+
report({
|
|
321
|
+
method: 'insertBefore',
|
|
322
|
+
redirected: true
|
|
323
|
+
});
|
|
324
|
+
return node;
|
|
325
|
+
}
|
|
326
|
+
nativeInsertBefore.call(this, node, null);
|
|
327
|
+
report({
|
|
328
|
+
method: 'insertBefore',
|
|
329
|
+
redirected: false
|
|
330
|
+
});
|
|
331
|
+
return node;
|
|
332
|
+
};
|
|
333
|
+
Node.prototype.replaceChild = function replaceChild(node, child) {
|
|
334
|
+
if (child.parentNode === this) {
|
|
335
|
+
nativeReplaceChild.call(this, node, child);
|
|
336
|
+
return child;
|
|
337
|
+
}
|
|
338
|
+
const wrapper = wrapperFor(child);
|
|
339
|
+
if (wrapper && wrapper.parentNode === this) {
|
|
340
|
+
nativeReplaceChild.call(this, node, wrapper);
|
|
341
|
+
report({
|
|
342
|
+
method: 'replaceChild',
|
|
343
|
+
redirected: true
|
|
344
|
+
});
|
|
345
|
+
return child;
|
|
346
|
+
}
|
|
347
|
+
report({
|
|
348
|
+
method: 'replaceChild',
|
|
349
|
+
redirected: false
|
|
350
|
+
});
|
|
351
|
+
return child;
|
|
352
|
+
};
|
|
353
|
+
return ()=>{
|
|
354
|
+
Node.prototype.removeChild = nativeRemoveChild;
|
|
355
|
+
Node.prototype.insertBefore = nativeInsertBefore;
|
|
356
|
+
Node.prototype.replaceChild = nativeReplaceChild;
|
|
357
|
+
};
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
const OBSERVE_OPTIONS = {
|
|
361
|
+
childList: true,
|
|
362
|
+
subtree: true
|
|
363
|
+
};
|
|
364
|
+
const pushInto = (bucket, key, value)=>{
|
|
365
|
+
const existing = bucket.get(key);
|
|
366
|
+
if (!existing) {
|
|
367
|
+
bucket.set(key, [
|
|
368
|
+
value
|
|
369
|
+
]);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
existing.push(value);
|
|
373
|
+
};
|
|
374
|
+
/**
|
|
375
|
+
* Watches for the translator's TextNode-to-wrapper swap and links each pair.
|
|
376
|
+
* Pairing is positional per parent: within one observer batch the n-th injected
|
|
377
|
+
* wrapper belongs to the n-th detached TextNode of that parent.
|
|
378
|
+
*
|
|
379
|
+
* Open shadow roots are observed too. Chrome and Yandex translate through the
|
|
380
|
+
* boundary and detach the nodes inside it, and a `MutationObserver` on a host
|
|
381
|
+
* element never reports what happens in its shadow root. `attachShadow` is
|
|
382
|
+
* wrapped so roots opened after install are picked up as well: attaching a
|
|
383
|
+
* shadow root produces no mutation record, so there is nothing else to watch.
|
|
384
|
+
*/ const startObserver = ({ root, wrapperTags, onTranslationDetected })=>{
|
|
385
|
+
let detected = false;
|
|
386
|
+
const observedRoots = new WeakSet();
|
|
387
|
+
const observer = new MutationObserver((records)=>{
|
|
388
|
+
const detachedTexts = new Map();
|
|
389
|
+
const injectedWrappers = new Map();
|
|
390
|
+
records.forEach((record)=>{
|
|
391
|
+
if (isWrapper(record.target)) return;
|
|
392
|
+
record.removedNodes.forEach((node)=>{
|
|
393
|
+
if (dom.isText(node)) pushInto(detachedTexts, record.target, node);
|
|
394
|
+
});
|
|
395
|
+
record.addedNodes.forEach((node)=>{
|
|
396
|
+
if (dom.isTranslatorWrapper(node, wrapperTags)) {
|
|
397
|
+
pushInto(injectedWrappers, record.target, node);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (dom.isElement(node)) observeShadowRootsUnder(node);
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
if (injectedWrappers.size === 0) return;
|
|
404
|
+
injectedWrappers.forEach((elements, parent)=>{
|
|
405
|
+
const texts = detachedTexts.get(parent) ?? [];
|
|
406
|
+
elements.forEach((wrapper, index)=>{
|
|
407
|
+
const detachedText = texts[index];
|
|
408
|
+
if (!detachedText) return;
|
|
409
|
+
linkNodes(detachedText, wrapper);
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
if (detected) return;
|
|
413
|
+
detected = true;
|
|
414
|
+
const [firstBatch] = injectedWrappers.values();
|
|
415
|
+
const wrapper = firstBatch?.[0];
|
|
416
|
+
onTranslationDetected({
|
|
417
|
+
lang: document.documentElement.lang,
|
|
418
|
+
engine: wrapper ? dom.engineOfWrapper(wrapper) : null,
|
|
419
|
+
wrapperTag: wrapper?.tagName ?? ''
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
function observeRoot(node) {
|
|
423
|
+
if (observedRoots.has(node)) return;
|
|
424
|
+
observedRoots.add(node);
|
|
425
|
+
observer.observe(node, OBSERVE_OPTIONS);
|
|
426
|
+
}
|
|
427
|
+
function observeShadowRootsUnder(scope) {
|
|
428
|
+
const hosts = dom.isElement(scope) ? [
|
|
429
|
+
scope,
|
|
430
|
+
...scope.querySelectorAll('*')
|
|
431
|
+
] : scope.querySelectorAll('*');
|
|
432
|
+
hosts.forEach((host)=>{
|
|
433
|
+
if (!host.shadowRoot) return;
|
|
434
|
+
observeRoot(host.shadowRoot);
|
|
435
|
+
observeShadowRootsUnder(host.shadowRoot);
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
const nativeAttachShadow = Element.prototype.attachShadow;
|
|
439
|
+
Element.prototype.attachShadow = function attachShadow(init) {
|
|
440
|
+
const shadow = nativeAttachShadow.call(this, init);
|
|
441
|
+
if (init.mode === 'open' && root.contains(this)) observeRoot(shadow);
|
|
442
|
+
return shadow;
|
|
443
|
+
};
|
|
444
|
+
observeRoot(root);
|
|
445
|
+
observeShadowRootsUnder(root);
|
|
446
|
+
return ()=>{
|
|
447
|
+
Element.prototype.attachShadow = nativeAttachShadow;
|
|
448
|
+
observer.disconnect();
|
|
449
|
+
};
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
const inertHandle = {
|
|
453
|
+
stop: ()=>undefined,
|
|
454
|
+
isTranslated: ()=>false,
|
|
455
|
+
engine: ()=>null,
|
|
456
|
+
conflicts: ()=>[]
|
|
457
|
+
};
|
|
458
|
+
let activeHandle = null;
|
|
459
|
+
/**
|
|
460
|
+
* Installs the shield: keeps the DOM operations React relies on from throwing
|
|
461
|
+
* once a translator has rewritten the page, and mirrors framework text updates
|
|
462
|
+
* into the elements the user actually sees.
|
|
463
|
+
*/ const initTranslateShield = (options = {})=>{
|
|
464
|
+
if (typeof document === 'undefined') return inertHandle;
|
|
465
|
+
if (activeHandle) return activeHandle;
|
|
466
|
+
const { root = document.body || document.documentElement, wrapperTags = [], debug = false } = options;
|
|
467
|
+
if (!root) return inertHandle;
|
|
468
|
+
let translation = null;
|
|
469
|
+
const conflicts = findConflicts();
|
|
470
|
+
if (conflicts.length > 0) {
|
|
471
|
+
options.onConflict?.(conflicts);
|
|
472
|
+
console.warn(`[translate-shield] another shim already replaced ${conflicts.join(', ')}. Both will run, but only one repair strategy can win and the other silently does nothing. Install one.`);
|
|
473
|
+
}
|
|
474
|
+
const report = (error)=>{
|
|
475
|
+
options.onRecoveredError?.(error);
|
|
476
|
+
if (!debug) return;
|
|
477
|
+
console.warn('[translate-shield] recovered', error.method, {
|
|
478
|
+
redirected: error.redirected
|
|
479
|
+
});
|
|
480
|
+
};
|
|
481
|
+
const handleTranslationDetected = (info)=>{
|
|
482
|
+
translation = info;
|
|
483
|
+
options.onTranslationDetected?.(info);
|
|
484
|
+
if (!debug) return;
|
|
485
|
+
console.warn('[translate-shield] translation detected', info);
|
|
486
|
+
};
|
|
487
|
+
const restoreDom = patchDom(report);
|
|
488
|
+
const stopObserver = startObserver({
|
|
489
|
+
root,
|
|
490
|
+
wrapperTags,
|
|
491
|
+
onTranslationDetected: handleTranslationDetected
|
|
492
|
+
});
|
|
493
|
+
activeHandle = {
|
|
494
|
+
stop: ()=>{
|
|
495
|
+
stopObserver();
|
|
496
|
+
restoreDom();
|
|
497
|
+
releaseInterceptors();
|
|
498
|
+
activeHandle = null;
|
|
499
|
+
},
|
|
500
|
+
isTranslated: ()=>translation !== null,
|
|
501
|
+
engine: ()=>translation?.engine ?? null,
|
|
502
|
+
conflicts: ()=>conflicts
|
|
503
|
+
};
|
|
504
|
+
return activeHandle;
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
exports.initTranslateShield = initTranslateShield;
|
|
508
|
+
exports.mergeIntoTranslated = mergeIntoTranslated;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { S as ShieldOptions, a as ShieldHandle } from './types-fjbg60f8.js';
|
|
2
|
+
export { P as PatchedSurface, R as RecoveredError, T as TranslationInfo, b as TranslatorEngine } from './types-fjbg60f8.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Merges a new source value into an already translated string. The merge only
|
|
6
|
+
* happens when it cannot change meaning: the sentence around the numbers is
|
|
7
|
+
* identical, every number keeps its plural category in the target language, and
|
|
8
|
+
* the digits line up one for one. Otherwise the correct value is returned in the
|
|
9
|
+
* source language, because a visibly untranslated number beats a fluent-looking
|
|
10
|
+
* wrong one.
|
|
11
|
+
*/
|
|
12
|
+
declare const mergeIntoTranslated: (previousSource: string, nextSource: string, translated: string, locale: string) => string;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Installs the shield: keeps the DOM operations React relies on from throwing
|
|
16
|
+
* once a translator has rewritten the page, and mirrors framework text updates
|
|
17
|
+
* into the elements the user actually sees.
|
|
18
|
+
*/
|
|
19
|
+
declare const initTranslateShield: (options?: ShieldOptions) => ShieldHandle;
|
|
20
|
+
|
|
21
|
+
export { ShieldHandle, ShieldOptions, initTranslateShield, mergeIntoTranslated };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { S as ShieldOptions, a as ShieldHandle } from './types-fjbg60f8.js';
|
|
2
|
+
export { P as PatchedSurface, R as RecoveredError, T as TranslationInfo, b as TranslatorEngine } from './types-fjbg60f8.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Merges a new source value into an already translated string. The merge only
|
|
6
|
+
* happens when it cannot change meaning: the sentence around the numbers is
|
|
7
|
+
* identical, every number keeps its plural category in the target language, and
|
|
8
|
+
* the digits line up one for one. Otherwise the correct value is returned in the
|
|
9
|
+
* source language, because a visibly untranslated number beats a fluent-looking
|
|
10
|
+
* wrong one.
|
|
11
|
+
*/
|
|
12
|
+
declare const mergeIntoTranslated: (previousSource: string, nextSource: string, translated: string, locale: string) => string;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Installs the shield: keeps the DOM operations React relies on from throwing
|
|
16
|
+
* once a translator has rewritten the page, and mirrors framework text updates
|
|
17
|
+
* into the elements the user actually sees.
|
|
18
|
+
*/
|
|
19
|
+
declare const initTranslateShield: (options?: ShieldOptions) => ShieldHandle;
|
|
20
|
+
|
|
21
|
+
export { ShieldHandle, ShieldOptions, initTranslateShield, mergeIntoTranslated };
|