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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Davlatbek Aliev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# translate-shield
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/translate-shield)
|
|
6
|
+
[](https://bundlephobia.com/package/translate-shield)
|
|
7
|
+

|
|
8
|
+
[](LICENSE)
|
|
9
|
+
|
|
10
|
+
`NotFoundError: Failed to execute 'removeChild' on 'Node'` after a reader turns on Chrome
|
|
11
|
+
translation. Or a counter that freezes while the rest of the app works.
|
|
12
|
+
|
|
13
|
+
Chrome replaces your TextNode with `<font style="vertical-align: inherit">` and detaches the
|
|
14
|
+
original. React still holds the detached node, so `removeChild` and `insertBefore` throw and text
|
|
15
|
+
writes go nowhere.
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install translate-shield
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { initTranslateShield } from 'translate-shield'
|
|
23
|
+
|
|
24
|
+
initTranslateShield()
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The whole setup. No-op on the server, armed only after a translator rewrites the page, inert on
|
|
28
|
+
engines that do not need it. No dependencies, about 14 kB packed.
|
|
29
|
+
|
|
30
|
+
Firefox and Edge readers do not hit this bug. Check [Browser support](#browser-support) first.
|
|
31
|
+
|
|
32
|
+
## What the reader sees
|
|
33
|
+
|
|
34
|
+
Four updates to a value in view, real Chrome, live Dutch translation.
|
|
35
|
+
|
|
36
|
+
| | No protection | The pasted crash guard | A restore-based library | translate-shield |
|
|
37
|
+
|---|---|---|---|---|
|
|
38
|
+
| `NotFoundError` crash | app unmounts | fixed | fixed | fixed |
|
|
39
|
+
| Value keeps updating | frozen | still frozen | yes | yes |
|
|
40
|
+
| Removed text disappears | n/a | stays on screen | yes | yes |
|
|
41
|
+
| Language while updating | n/a | n/a | 150-200 ms of source language per update | 0 ms |
|
|
42
|
+
| Text when the reader looks | `Er zijn 4 lampen!`, stale | n/a | `There are 7 lights!` | `Er zijn 7 lampen!` |
|
|
43
|
+
|
|
44
|
+
First three rows: `research/comparison.json`. Language row: `research/flicker.json`, where
|
|
45
|
+
restore-and-retranslate spends 700 ms of the sequence in source language and mirroring 0 ms. Last
|
|
46
|
+
row: `research/head-to-head-real-chrome.json`.
|
|
47
|
+
|
|
48
|
+
## Next.js
|
|
49
|
+
|
|
50
|
+
Call the initializer from a client component in the root layout.
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
'use client'
|
|
54
|
+
|
|
55
|
+
import { useEffect } from 'react'
|
|
56
|
+
import { initTranslateShield } from 'translate-shield'
|
|
57
|
+
|
|
58
|
+
export const TranslateShield = () => {
|
|
59
|
+
useEffect(() => initTranslateShield().stop, [])
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
import { TranslateShield } from './translate-shield'
|
|
66
|
+
|
|
67
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
68
|
+
return (
|
|
69
|
+
<html lang="en">
|
|
70
|
+
<body>
|
|
71
|
+
<TranslateShield />
|
|
72
|
+
{children}
|
|
73
|
+
</body>
|
|
74
|
+
</html>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
A second call returns the existing handle, so a double-invoked development effect leaves one
|
|
80
|
+
shield.
|
|
81
|
+
|
|
82
|
+
## Protecting a value
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
import { NoTranslate } from 'translate-shield/react'
|
|
86
|
+
|
|
87
|
+
export const OrderTotal = ({ total }: { total: number }) => (
|
|
88
|
+
<p>
|
|
89
|
+
Order total: <NoTranslate>{formatPrice(total)}</NoTranslate> including VAT
|
|
90
|
+
</p>
|
|
91
|
+
)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The sentence is translated; the price is not. Use it for prices, counters, order numbers,
|
|
95
|
+
tracking codes, IBANs and dates, never prose. Every real engine recorded honours `translate="no"`
|
|
96
|
+
(`research/fingerprints/summary.json`); `NoTranslate` also sets `class="notranslate"`, because
|
|
97
|
+
Yandex translated the class probe anyway (`research/measurements.md`).
|
|
98
|
+
|
|
99
|
+
## Detecting a translated page
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
'use client'
|
|
103
|
+
|
|
104
|
+
import { useTranslationDetected } from 'translate-shield/react'
|
|
105
|
+
|
|
106
|
+
export const TranslationBanner = () => {
|
|
107
|
+
const { isTranslated, engine, lang } = useTranslationDetected()
|
|
108
|
+
if (!isTranslated) return null
|
|
109
|
+
return <p>Machine translated to {lang} by {engine}.</p>
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Idle on the server and on the first client render, so no hydration mismatch. `engine` is
|
|
114
|
+
`'google'`, `'yandex'`, `'edge'`, `'firefox'`, or `null`.
|
|
115
|
+
|
|
116
|
+
## API
|
|
117
|
+
|
|
118
|
+
### `initTranslateShield(options?): ShieldHandle`
|
|
119
|
+
|
|
120
|
+
| Option | Type | Default | Meaning |
|
|
121
|
+
|---|---|---|---|
|
|
122
|
+
| `root` | `Element` | `document.body` | Subtree to observe |
|
|
123
|
+
| `wrapperTags` | `string[]` | `[]` | Extra wrapper tag names to recognise beyond the built-in fingerprints |
|
|
124
|
+
| `onTranslationDetected` | `(info: TranslationInfo) => void` | none | Called once, with `lang`, `engine`, and `wrapperTag` |
|
|
125
|
+
| `onRecoveredError` | `(error: RecoveredError) => void` | none | Called when `removeChild`, `insertBefore` or `replaceChild` was redirected instead of throwing |
|
|
126
|
+
| `onConflict` | `(surfaces: PatchedSurface[]) => void` | none | Called at install when another shim has already replaced a DOM surface |
|
|
127
|
+
| `debug` | `boolean` | `false` | Log every detection and recovery to the console |
|
|
128
|
+
|
|
129
|
+
`ShieldHandle` carries `stop()`, `isTranslated()`, `engine()`, and `conflicts()`. On the server it
|
|
130
|
+
is inert.
|
|
131
|
+
|
|
132
|
+
Two shims on one page is a silent draw, not a crash: whichever repairs second decides, and the
|
|
133
|
+
other quietly does nothing. `conflicts()` names any DOM surface already replaced when the shield
|
|
134
|
+
installed, and a console warning fires whether or not you pass `onConflict`.
|
|
135
|
+
|
|
136
|
+
### `mergeIntoTranslated(previousSource, nextSource, translated, locale): string`
|
|
137
|
+
|
|
138
|
+
Writes changed digits into a sentence the translator already produced.
|
|
139
|
+
|
|
140
|
+
| Outcome | What the reader gets |
|
|
141
|
+
|---|---|
|
|
142
|
+
| Merged | the translated sentence, keeping its separators and digit system: `19,99` updated from `29.99` becomes `29,99` |
|
|
143
|
+
| Refused | `nextSource` untranslated, so the value is right and the language is not (`research/head-to-head-real-chrome.json`, Russian arm) |
|
|
144
|
+
|
|
145
|
+
| Refuses when | Because |
|
|
146
|
+
|---|---|
|
|
147
|
+
| `Intl.PluralRules` puts the old and the new value in different categories for `locale` | Russian `4` is `few` and `7` is `many`, so `Здесь 4 лампочки!` never becomes `Здесь 7 лампочки!` |
|
|
148
|
+
| The sentence around the numbers changed, not just the numbers | The translation no longer matches the new source |
|
|
149
|
+
| The digit count changed | The positions no longer line up |
|
|
150
|
+
| The locale is empty or unknown to `Intl.PluralRules` | Grammar cannot be checked |
|
|
151
|
+
|
|
152
|
+
## Browser support
|
|
153
|
+
|
|
154
|
+
Rows: `research/fingerprints/summary.json`, recorded on Windows 2026-09-02; versions in
|
|
155
|
+
`research/provenance.json`.
|
|
156
|
+
|
|
157
|
+
| Engine | Wrapper | Detaches the original | Does this library help |
|
|
158
|
+
|---|---|---|---|
|
|
159
|
+
| Chrome built-in | `<font style="vertical-align: inherit">` | yes | yes |
|
|
160
|
+
| Google `translate_a/element.js` bundle | `<font style="vertical-align: inherit">` | yes | yes |
|
|
161
|
+
| Yandex | `<ya-tr-span>` | yes | crash and frozen values only |
|
|
162
|
+
| Edge built-in | none, stamps `_msttexthash` / `_msthash` | no | no |
|
|
163
|
+
| Firefox built-in | none, no markers | no | no |
|
|
164
|
+
| Safari built-in | unmeasured | unknown | unknown |
|
|
165
|
+
|
|
166
|
+
Firefox and Edge rewrite the text node in place and leave it connected, so React's writes reach
|
|
167
|
+
the screen. Three qualifications:
|
|
168
|
+
|
|
169
|
+
| Qualification | Evidence |
|
|
170
|
+
|---|---|
|
|
171
|
+
| In-place engines do detach when they merge adjacent text runs | the three-TextNode `interpolated` probe reports two detached nodes on Firefox and on Edge, against three on Chrome and Yandex |
|
|
172
|
+
| On Yandex the detached node already holds translated text, so the language row does not apply there; the crash and freeze fixes do | `research/measurements.md` |
|
|
173
|
+
| Safari does not run on Windows and is unmeasured | the WebKit recording, `google-element-webkit-nl.json`, is the Google bundle under WebKit, not Safari's own translator |
|
|
174
|
+
|
|
175
|
+
## Limits
|
|
176
|
+
|
|
177
|
+
- It does not translate anything or replace an i18n library.
|
|
178
|
+
- It does not stop the translator rewriting text; `NoTranslate` does.
|
|
179
|
+
- Beyond what `mergeIntoTranslated` verifies, a value merged into a neighbouring sentence is
|
|
180
|
+
unprotected: on refusal it is correct but in the source language.
|
|
181
|
+
- Safari behaviour is unknown, not assumed.
|
|
182
|
+
|
|
183
|
+
Why it works this way: [DESIGN.md](DESIGN.md).
|
|
184
|
+
|
|
185
|
+
## Prior art
|
|
186
|
+
|
|
187
|
+
| Prior work | What it does |
|
|
188
|
+
|---|---|
|
|
189
|
+
| [facebook/react#11538](https://github.com/facebook/react/issues/11538) | Where the crash was described. shuhei identified the sibling condition in 2018; Dan Abramov closed it as won't-fix the same year. The guard people paste into their apps comes from that thread and stops the crash without unfreezing anything. |
|
|
190
|
+
| [`translation-resilience`](https://www.npmjs.com/package/translation-resilience) by Alex Speller, first published 2026-07-10 | Fixes the crash and the frozen value by restoring the original node and letting the translator catch up. That is the strategy the language row measures. |
|
|
191
|
+
| `react-google-translate-shim` | A similar restore-based approach. |
|
|
192
|
+
| [`eslint-plugin-react-google-translate`](https://www.npmjs.com/package/eslint-plugin-react-google-translate) | Catches the risky JSX patterns at lint time. Worth running alongside this. |
|
|
193
|
+
|
|
194
|
+
The full comparison: `research/article.md`.
|
|
195
|
+
|
|
196
|
+
## Reproducing the measurements
|
|
197
|
+
|
|
198
|
+
Every figure comes from `research/`, produced by a spec in `tests/specs/`.
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
npx playwright test flicker # research/flicker.json
|
|
202
|
+
npx playwright test visibility-confound # research/visibility-confound.json
|
|
203
|
+
npx playwright test provocation # research/provocation.json
|
|
204
|
+
npx playwright test head-to-head-real-chrome # research/head-to-head-real-chrome.json
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Recordings under `research/fingerprints/` come from `research/recorder`, driven by hand in each
|
|
208
|
+
browser (`research/recorder/README.md`).
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
npm run summarise
|
|
212
|
+
npm run typecheck
|
|
213
|
+
npm run build
|
|
214
|
+
npx publint --strict
|
|
215
|
+
npx @arethetypeswrong/cli --pack .
|
|
216
|
+
npx playwright test
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Licence
|
|
220
|
+
|
|
221
|
+
MIT, Davlatbek Aliev
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One row per translator, and everything else in this file is derived from it.
|
|
3
|
+
* Adding an engine means adding a row, not editing four predicates that have to
|
|
4
|
+
* agree with each other.
|
|
5
|
+
*
|
|
6
|
+
* A bare tag is never enough. Apps emit `<font>` through rendered markdown, CMS
|
|
7
|
+
* content and `dangerouslySetInnerHTML`, so Google's wrapper is only recognised
|
|
8
|
+
* with the inline `vertical-align: inherit` it always carries, read off the
|
|
9
|
+
* `style` attribute because `getComputedStyle` resolves `inherit` to `baseline`.
|
|
10
|
+
*
|
|
11
|
+
* Edge and Firefox rewrite the live text node and inject no wrapper, so they
|
|
12
|
+
* have markers but no tag. They are named for observability; there is nothing
|
|
13
|
+
* for the shield to mirror into.
|
|
14
|
+
*/ const ENGINES = [
|
|
15
|
+
{
|
|
16
|
+
name: 'google',
|
|
17
|
+
wrapperTag: 'FONT',
|
|
18
|
+
wrapperStyle: 'vertical-align: inherit'
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'yandex',
|
|
22
|
+
wrapperTag: 'YA-TR-SPAN'
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'edge',
|
|
26
|
+
markerAttributes: [
|
|
27
|
+
'_msttexthash',
|
|
28
|
+
'_msthash'
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: 'firefox',
|
|
33
|
+
markerAttributes: [
|
|
34
|
+
'data-moz-translations-id'
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
];
|
|
38
|
+
const MARKER_ATTRIBUTES = ENGINES.flatMap((engine)=>engine.markerAttributes ?? []);
|
|
39
|
+
const isText = (node)=>node.nodeType === Node.TEXT_NODE;
|
|
40
|
+
const isElement = (node)=>node.nodeType === Node.ELEMENT_NODE;
|
|
41
|
+
const matchesFingerprint = (element, engine)=>{
|
|
42
|
+
if (engine.wrapperTag !== element.tagName) return false;
|
|
43
|
+
if (!engine.wrapperStyle) return true;
|
|
44
|
+
return (element.getAttribute('style') ?? '').includes(engine.wrapperStyle);
|
|
45
|
+
};
|
|
46
|
+
const engineOwningTag = (tagName)=>ENGINES.find((engine)=>engine.wrapperTag === tagName);
|
|
47
|
+
/**
|
|
48
|
+
* Recognises the element a translator injected in place of a TextNode.
|
|
49
|
+
*
|
|
50
|
+
* `extraTags` widens the set for an engine we do not ship a row for. It cannot
|
|
51
|
+
* narrow or replace a built-in fingerprint: passing `FONT`, the tag every bug
|
|
52
|
+
* report names, must not turn every `<font>` on the page into a wrapper.
|
|
53
|
+
*/ const isTranslatorWrapper = (node, extraTags)=>{
|
|
54
|
+
if (!isElement(node)) return false;
|
|
55
|
+
const owner = engineOwningTag(node.tagName);
|
|
56
|
+
if (owner) return matchesFingerprint(node, owner);
|
|
57
|
+
return extraTags.some((tag)=>tag.toUpperCase() === node.tagName);
|
|
58
|
+
};
|
|
59
|
+
/** Names the engine that produced a wrapper the observer already matched. */ const engineOfWrapper = (wrapper)=>engineOwningTag(wrapper.tagName)?.name ?? null;
|
|
60
|
+
/** Names the translator working on a document, wrapper or marker attributes alike. */ const detectEngine = (root)=>{
|
|
61
|
+
for (const engine of ENGINES){
|
|
62
|
+
const selectors = [
|
|
63
|
+
engine.wrapperTag && `${engine.wrapperTag.toLowerCase()}${engine.wrapperStyle ? `[style*="${engine.wrapperStyle}"]` : ''}`,
|
|
64
|
+
...(engine.markerAttributes ?? []).map((attribute)=>`[${attribute}]`)
|
|
65
|
+
].filter(Boolean);
|
|
66
|
+
if (selectors.length > 0 && root.querySelector(selectors.join(','))) return engine.name;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export { MARKER_ATTRIBUTES as M, isElement as a, isTranslatorWrapper as b, detectEngine as d, engineOfWrapper as e, isText as i };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One row per translator, and everything else in this file is derived from it.
|
|
3
|
+
* Adding an engine means adding a row, not editing four predicates that have to
|
|
4
|
+
* agree with each other.
|
|
5
|
+
*
|
|
6
|
+
* A bare tag is never enough. Apps emit `<font>` through rendered markdown, CMS
|
|
7
|
+
* content and `dangerouslySetInnerHTML`, so Google's wrapper is only recognised
|
|
8
|
+
* with the inline `vertical-align: inherit` it always carries, read off the
|
|
9
|
+
* `style` attribute because `getComputedStyle` resolves `inherit` to `baseline`.
|
|
10
|
+
*
|
|
11
|
+
* Edge and Firefox rewrite the live text node and inject no wrapper, so they
|
|
12
|
+
* have markers but no tag. They are named for observability; there is nothing
|
|
13
|
+
* for the shield to mirror into.
|
|
14
|
+
*/ const ENGINES = [
|
|
15
|
+
{
|
|
16
|
+
name: 'google',
|
|
17
|
+
wrapperTag: 'FONT',
|
|
18
|
+
wrapperStyle: 'vertical-align: inherit'
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'yandex',
|
|
22
|
+
wrapperTag: 'YA-TR-SPAN'
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'edge',
|
|
26
|
+
markerAttributes: [
|
|
27
|
+
'_msttexthash',
|
|
28
|
+
'_msthash'
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: 'firefox',
|
|
33
|
+
markerAttributes: [
|
|
34
|
+
'data-moz-translations-id'
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
];
|
|
38
|
+
const MARKER_ATTRIBUTES = ENGINES.flatMap((engine)=>engine.markerAttributes ?? []);
|
|
39
|
+
const isText = (node)=>node.nodeType === Node.TEXT_NODE;
|
|
40
|
+
const isElement = (node)=>node.nodeType === Node.ELEMENT_NODE;
|
|
41
|
+
const matchesFingerprint = (element, engine)=>{
|
|
42
|
+
if (engine.wrapperTag !== element.tagName) return false;
|
|
43
|
+
if (!engine.wrapperStyle) return true;
|
|
44
|
+
return (element.getAttribute('style') ?? '').includes(engine.wrapperStyle);
|
|
45
|
+
};
|
|
46
|
+
const engineOwningTag = (tagName)=>ENGINES.find((engine)=>engine.wrapperTag === tagName);
|
|
47
|
+
/**
|
|
48
|
+
* Recognises the element a translator injected in place of a TextNode.
|
|
49
|
+
*
|
|
50
|
+
* `extraTags` widens the set for an engine we do not ship a row for. It cannot
|
|
51
|
+
* narrow or replace a built-in fingerprint: passing `FONT`, the tag every bug
|
|
52
|
+
* report names, must not turn every `<font>` on the page into a wrapper.
|
|
53
|
+
*/ const isTranslatorWrapper = (node, extraTags)=>{
|
|
54
|
+
if (!isElement(node)) return false;
|
|
55
|
+
const owner = engineOwningTag(node.tagName);
|
|
56
|
+
if (owner) return matchesFingerprint(node, owner);
|
|
57
|
+
return extraTags.some((tag)=>tag.toUpperCase() === node.tagName);
|
|
58
|
+
};
|
|
59
|
+
/** Names the engine that produced a wrapper the observer already matched. */ const engineOfWrapper = (wrapper)=>engineOwningTag(wrapper.tagName)?.name ?? null;
|
|
60
|
+
/** Names the translator working on a document, wrapper or marker attributes alike. */ const detectEngine = (root)=>{
|
|
61
|
+
for (const engine of ENGINES){
|
|
62
|
+
const selectors = [
|
|
63
|
+
engine.wrapperTag && `${engine.wrapperTag.toLowerCase()}${engine.wrapperStyle ? `[style*="${engine.wrapperStyle}"]` : ''}`,
|
|
64
|
+
...(engine.markerAttributes ?? []).map((attribute)=>`[${attribute}]`)
|
|
65
|
+
].filter(Boolean);
|
|
66
|
+
if (selectors.length > 0 && root.querySelector(selectors.join(','))) return engine.name;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
exports.MARKER_ATTRIBUTES = MARKER_ATTRIBUTES;
|
|
72
|
+
exports.detectEngine = detectEngine;
|
|
73
|
+
exports.engineOfWrapper = engineOfWrapper;
|
|
74
|
+
exports.isElement = isElement;
|
|
75
|
+
exports.isText = isText;
|
|
76
|
+
exports.isTranslatorWrapper = isTranslatorWrapper;
|