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