@dabalabs/lang 0.0.1-beta → 0.0.3-beta
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/dabalang.cjs +229 -10
- package/dist/dabalang.cjs.map +1 -1
- package/dist/dabalang.d.cts +38 -0
- package/dist/dabalang.d.ts +38 -0
- package/dist/dabalang.js +229 -10
- package/dist/dabalang.js.map +1 -1
- package/dist/dabalang.min.js +14 -8
- package/dist/dabalang.min.js.map +1 -1
- package/package.json +1 -1
package/dist/dabalang.d.cts
CHANGED
|
@@ -51,6 +51,12 @@ interface LanguageConfig {
|
|
|
51
51
|
pathPrefix: string | null;
|
|
52
52
|
/** Language applied on un-prefixed paths when path routing is on. */
|
|
53
53
|
isDefault: boolean;
|
|
54
|
+
/** False while the language is still being populated. The switcher
|
|
55
|
+
* omits it, so a visitor is never offered a language that would
|
|
56
|
+
* translate the page live in front of them. Absent (older gateways)
|
|
57
|
+
* counts as ready — a widget must not hide working languages because
|
|
58
|
+
* the server predates the field. */
|
|
59
|
+
ready: boolean;
|
|
54
60
|
}
|
|
55
61
|
interface ProjectMetadata {
|
|
56
62
|
projectId: string;
|
|
@@ -82,6 +88,9 @@ declare class DabaLang {
|
|
|
82
88
|
private switcher;
|
|
83
89
|
private metadata;
|
|
84
90
|
private pathRouter;
|
|
91
|
+
private navWatcher;
|
|
92
|
+
/** Tail of the re-translation chain; see retranslateCurrentPage. */
|
|
93
|
+
private retranslating;
|
|
85
94
|
/** Currently active language (null = original). Tracked here rather
|
|
86
95
|
* than via the applier because a site-translated page (see
|
|
87
96
|
* isSiteTranslated) never runs the applier at all. */
|
|
@@ -89,11 +98,28 @@ declare class DabaLang {
|
|
|
89
98
|
private readonly ready;
|
|
90
99
|
constructor(options: DabaLangOptions);
|
|
91
100
|
private init;
|
|
101
|
+
/**
|
|
102
|
+
* Re-applies the active language to content that arrived after init.
|
|
103
|
+
*
|
|
104
|
+
* A no-op in the source language — there is nothing to apply — and on
|
|
105
|
+
* a site-translated page, where the page already *is* the translation.
|
|
106
|
+
*/
|
|
107
|
+
private retranslateCurrentPage;
|
|
92
108
|
/** True when the site serves this language's content itself (CMS,
|
|
93
109
|
* localized build): a pre-translated ("developer") language. Combined
|
|
94
110
|
* with a prefix match, the widget never machine-translates that page.
|
|
95
111
|
* (A prefix-less developer language instead swaps its uploaded
|
|
96
112
|
* catalogue in place.) */
|
|
113
|
+
/** Target languages that actually have translations behind them.
|
|
114
|
+
*
|
|
115
|
+
* A configured-but-unpopulated language is worse than a missing one: a
|
|
116
|
+
* visitor picks it and watches the page translate live, paragraph by
|
|
117
|
+
* paragraph, or lands on a half-translated page while a run is still
|
|
118
|
+
* going. Languages with no config row at all are legacy
|
|
119
|
+
* target_languages entries that predate per-language config — they are
|
|
120
|
+
* shown, because hiding a language that has been serving traffic would
|
|
121
|
+
* be a regression. */
|
|
122
|
+
private readyLanguages;
|
|
97
123
|
private isSiteTranslated;
|
|
98
124
|
private selectLanguage;
|
|
99
125
|
/** Overridable seam for tests — jsdom can't perform real navigation. */
|
|
@@ -102,6 +128,18 @@ declare class DabaLang {
|
|
|
102
128
|
/** Resolves once the initial metadata fetch + pill render has settled
|
|
103
129
|
* (successfully or not) — mainly useful in tests. */
|
|
104
130
|
whenReady(): Promise<void>;
|
|
131
|
+
/**
|
|
132
|
+
* Switch language programmatically — the same path the switcher takes,
|
|
133
|
+
* so the choice is applied, persisted and reported identically.
|
|
134
|
+
*
|
|
135
|
+
* Exists because a host page often has its own reason to change
|
|
136
|
+
* language: a footer link, a country picker, a preference synced from
|
|
137
|
+
* an account. Without it those would have to fake a click on the
|
|
138
|
+
* widget's own DOM.
|
|
139
|
+
*
|
|
140
|
+
* Pass null to return to the source language.
|
|
141
|
+
*/
|
|
142
|
+
setLanguage(langCode: string | null): Promise<void>;
|
|
105
143
|
currentLanguage(): string | null;
|
|
106
144
|
destroy(): void;
|
|
107
145
|
}
|
package/dist/dabalang.d.ts
CHANGED
|
@@ -51,6 +51,12 @@ interface LanguageConfig {
|
|
|
51
51
|
pathPrefix: string | null;
|
|
52
52
|
/** Language applied on un-prefixed paths when path routing is on. */
|
|
53
53
|
isDefault: boolean;
|
|
54
|
+
/** False while the language is still being populated. The switcher
|
|
55
|
+
* omits it, so a visitor is never offered a language that would
|
|
56
|
+
* translate the page live in front of them. Absent (older gateways)
|
|
57
|
+
* counts as ready — a widget must not hide working languages because
|
|
58
|
+
* the server predates the field. */
|
|
59
|
+
ready: boolean;
|
|
54
60
|
}
|
|
55
61
|
interface ProjectMetadata {
|
|
56
62
|
projectId: string;
|
|
@@ -82,6 +88,9 @@ declare class DabaLang {
|
|
|
82
88
|
private switcher;
|
|
83
89
|
private metadata;
|
|
84
90
|
private pathRouter;
|
|
91
|
+
private navWatcher;
|
|
92
|
+
/** Tail of the re-translation chain; see retranslateCurrentPage. */
|
|
93
|
+
private retranslating;
|
|
85
94
|
/** Currently active language (null = original). Tracked here rather
|
|
86
95
|
* than via the applier because a site-translated page (see
|
|
87
96
|
* isSiteTranslated) never runs the applier at all. */
|
|
@@ -89,11 +98,28 @@ declare class DabaLang {
|
|
|
89
98
|
private readonly ready;
|
|
90
99
|
constructor(options: DabaLangOptions);
|
|
91
100
|
private init;
|
|
101
|
+
/**
|
|
102
|
+
* Re-applies the active language to content that arrived after init.
|
|
103
|
+
*
|
|
104
|
+
* A no-op in the source language — there is nothing to apply — and on
|
|
105
|
+
* a site-translated page, where the page already *is* the translation.
|
|
106
|
+
*/
|
|
107
|
+
private retranslateCurrentPage;
|
|
92
108
|
/** True when the site serves this language's content itself (CMS,
|
|
93
109
|
* localized build): a pre-translated ("developer") language. Combined
|
|
94
110
|
* with a prefix match, the widget never machine-translates that page.
|
|
95
111
|
* (A prefix-less developer language instead swaps its uploaded
|
|
96
112
|
* catalogue in place.) */
|
|
113
|
+
/** Target languages that actually have translations behind them.
|
|
114
|
+
*
|
|
115
|
+
* A configured-but-unpopulated language is worse than a missing one: a
|
|
116
|
+
* visitor picks it and watches the page translate live, paragraph by
|
|
117
|
+
* paragraph, or lands on a half-translated page while a run is still
|
|
118
|
+
* going. Languages with no config row at all are legacy
|
|
119
|
+
* target_languages entries that predate per-language config — they are
|
|
120
|
+
* shown, because hiding a language that has been serving traffic would
|
|
121
|
+
* be a regression. */
|
|
122
|
+
private readyLanguages;
|
|
97
123
|
private isSiteTranslated;
|
|
98
124
|
private selectLanguage;
|
|
99
125
|
/** Overridable seam for tests — jsdom can't perform real navigation. */
|
|
@@ -102,6 +128,18 @@ declare class DabaLang {
|
|
|
102
128
|
/** Resolves once the initial metadata fetch + pill render has settled
|
|
103
129
|
* (successfully or not) — mainly useful in tests. */
|
|
104
130
|
whenReady(): Promise<void>;
|
|
131
|
+
/**
|
|
132
|
+
* Switch language programmatically — the same path the switcher takes,
|
|
133
|
+
* so the choice is applied, persisted and reported identically.
|
|
134
|
+
*
|
|
135
|
+
* Exists because a host page often has its own reason to change
|
|
136
|
+
* language: a footer link, a country picker, a preference synced from
|
|
137
|
+
* an account. Without it those would have to fake a click on the
|
|
138
|
+
* widget's own DOM.
|
|
139
|
+
*
|
|
140
|
+
* Pass null to return to the source language.
|
|
141
|
+
*/
|
|
142
|
+
setLanguage(langCode: string | null): Promise<void>;
|
|
105
143
|
currentLanguage(): string | null;
|
|
106
144
|
destroy(): void;
|
|
107
145
|
}
|
package/dist/dabalang.js
CHANGED
|
@@ -95,7 +95,10 @@ async function fetchProjectMetadata(gatewayUrl, projectId, apiKey) {
|
|
|
95
95
|
code: l.code,
|
|
96
96
|
provider: l.provider,
|
|
97
97
|
pathPrefix: l.path_prefix,
|
|
98
|
-
isDefault: l.is_default
|
|
98
|
+
isDefault: l.is_default,
|
|
99
|
+
// Absent means an older gateway that has no readiness concept —
|
|
100
|
+
// treat as ready rather than hiding every language.
|
|
101
|
+
ready: l.ready ?? true
|
|
99
102
|
}))
|
|
100
103
|
};
|
|
101
104
|
}
|
|
@@ -204,13 +207,38 @@ var TranslationApplier = class {
|
|
|
204
207
|
this.apiKey = apiKey;
|
|
205
208
|
this.fetchedLanguages = /* @__PURE__ */ new Map();
|
|
206
209
|
this.activeLang = null;
|
|
207
|
-
|
|
210
|
+
/** The true source text of every node the widget has ever crawled.
|
|
211
|
+
*
|
|
212
|
+
* Needed because a re-crawl reads the DOM as it stands, and after a
|
|
213
|
+
* translation the DOM no longer holds the source. On a single-page app
|
|
214
|
+
* the shell — header, nav, footer — stays mounted across a navigation
|
|
215
|
+
* with its text already translated, so a naive re-crawl would record
|
|
216
|
+
* "Начать" as the source for the Get Started link. Everything
|
|
217
|
+
* downstream then works from a corrupted map: the widget pays to
|
|
218
|
+
* translate Russian into Russian, and restoreOriginal() writes the
|
|
219
|
+
* Russian back as if it were the original, leaving the shell stuck in a
|
|
220
|
+
* language the visitor just switched out of.
|
|
221
|
+
*
|
|
222
|
+
* Weak so it holds no node alive: entries vanish with the nodes. */
|
|
223
|
+
this.sourceByNode = /* @__PURE__ */ new WeakMap();
|
|
224
|
+
this.groups = this.absorb(crawlVisibleText(root));
|
|
225
|
+
}
|
|
226
|
+
/** Groups a crawl, preferring each node's remembered source text over
|
|
227
|
+
* whatever it currently displays, and remembering the rest. */
|
|
228
|
+
absorb(crawled) {
|
|
229
|
+
const corrected = crawled.map(({ node, sourceText }) => {
|
|
230
|
+
const known = this.sourceByNode.get(node);
|
|
231
|
+
if (known !== void 0) return { node, sourceText: known };
|
|
232
|
+
this.sourceByNode.set(node, sourceText);
|
|
233
|
+
return { node, sourceText };
|
|
234
|
+
});
|
|
235
|
+
return groupByText(corrected);
|
|
208
236
|
}
|
|
209
237
|
/** Re-crawls the current DOM — call after content is known to have
|
|
210
238
|
* changed (e.g. after a route change in an SPA). Not called
|
|
211
239
|
* automatically; dabalang does one crawl per page load. */
|
|
212
240
|
recrawl() {
|
|
213
|
-
this.groups =
|
|
241
|
+
this.groups = this.absorb(crawlVisibleText(this.root));
|
|
214
242
|
}
|
|
215
243
|
currentLanguage() {
|
|
216
244
|
return this.activeLang;
|
|
@@ -432,6 +460,97 @@ var InlineEditor = class {
|
|
|
432
460
|
}
|
|
433
461
|
};
|
|
434
462
|
|
|
463
|
+
// src/dom/navigation-watcher.ts
|
|
464
|
+
var SETTLE_MS = 250;
|
|
465
|
+
var FOLLOW_UP_PASSES = 3;
|
|
466
|
+
var subscribers = /* @__PURE__ */ new Set();
|
|
467
|
+
var unpatchHistory = null;
|
|
468
|
+
function subscribeToHistory(fn) {
|
|
469
|
+
subscribers.add(fn);
|
|
470
|
+
if (!unpatchHistory) {
|
|
471
|
+
const original = {
|
|
472
|
+
pushState: window.history.pushState,
|
|
473
|
+
replaceState: window.history.replaceState
|
|
474
|
+
};
|
|
475
|
+
for (const method of ["pushState", "replaceState"]) {
|
|
476
|
+
window.history[method] = function(...args) {
|
|
477
|
+
const result = original[method].apply(this, args);
|
|
478
|
+
for (const sub of [...subscribers]) sub();
|
|
479
|
+
return result;
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
unpatchHistory = () => {
|
|
483
|
+
window.history.pushState = original.pushState;
|
|
484
|
+
window.history.replaceState = original.replaceState;
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
return () => {
|
|
488
|
+
subscribers.delete(fn);
|
|
489
|
+
if (subscribers.size === 0 && unpatchHistory) {
|
|
490
|
+
unpatchHistory();
|
|
491
|
+
unpatchHistory = null;
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
var NavigationWatcher = class {
|
|
496
|
+
constructor(onNavigated) {
|
|
497
|
+
this.onNavigated = onNavigated;
|
|
498
|
+
this.observer = null;
|
|
499
|
+
this.timer = null;
|
|
500
|
+
this.lastPath = "";
|
|
501
|
+
this.passesLeft = 0;
|
|
502
|
+
this.restorers = [];
|
|
503
|
+
}
|
|
504
|
+
start() {
|
|
505
|
+
if (typeof window === "undefined" || this.observer) return;
|
|
506
|
+
this.lastPath = this.currentPath();
|
|
507
|
+
this.restorers.push(subscribeToHistory(() => this.onRouteMaybeChanged()));
|
|
508
|
+
const onPop = () => this.onRouteMaybeChanged();
|
|
509
|
+
window.addEventListener("popstate", onPop);
|
|
510
|
+
this.restorers.push(() => window.removeEventListener("popstate", onPop));
|
|
511
|
+
this.observer = new MutationObserver(() => {
|
|
512
|
+
if (this.currentPath() !== this.lastPath) {
|
|
513
|
+
this.schedule();
|
|
514
|
+
} else if (this.passesLeft > 0) {
|
|
515
|
+
this.schedule();
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
this.observer.observe(document.body, { childList: true, subtree: true });
|
|
519
|
+
}
|
|
520
|
+
currentPath() {
|
|
521
|
+
return window.location.pathname + window.location.search;
|
|
522
|
+
}
|
|
523
|
+
onRouteMaybeChanged() {
|
|
524
|
+
if (this.currentPath() === this.lastPath) return;
|
|
525
|
+
this.schedule();
|
|
526
|
+
}
|
|
527
|
+
/** Debounced: a route change produces a burst of mutations, and the
|
|
528
|
+
* handler must run once, after the last one. */
|
|
529
|
+
schedule() {
|
|
530
|
+
if (this.timer) clearTimeout(this.timer);
|
|
531
|
+
this.timer = setTimeout(() => {
|
|
532
|
+
this.timer = null;
|
|
533
|
+
const path = this.currentPath();
|
|
534
|
+
if (path !== this.lastPath) {
|
|
535
|
+
this.lastPath = path;
|
|
536
|
+
this.passesLeft = FOLLOW_UP_PASSES;
|
|
537
|
+
this.onNavigated();
|
|
538
|
+
} else if (this.passesLeft > 0) {
|
|
539
|
+
this.passesLeft -= 1;
|
|
540
|
+
this.onNavigated();
|
|
541
|
+
}
|
|
542
|
+
}, SETTLE_MS);
|
|
543
|
+
}
|
|
544
|
+
stop() {
|
|
545
|
+
this.observer?.disconnect();
|
|
546
|
+
this.observer = null;
|
|
547
|
+
if (this.timer) clearTimeout(this.timer);
|
|
548
|
+
this.timer = null;
|
|
549
|
+
this.passesLeft = 0;
|
|
550
|
+
while (this.restorers.length) this.restorers.pop()?.();
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
|
|
435
554
|
// src/path-routing.ts
|
|
436
555
|
var PathRouter = class {
|
|
437
556
|
constructor(languages) {
|
|
@@ -475,6 +594,26 @@ var PathRouter = class {
|
|
|
475
594
|
}
|
|
476
595
|
};
|
|
477
596
|
|
|
597
|
+
// src/preference.ts
|
|
598
|
+
var PREFIX = "dabalang:lang:";
|
|
599
|
+
function readPreferredLanguage(projectId) {
|
|
600
|
+
try {
|
|
601
|
+
return window.localStorage.getItem(PREFIX + projectId);
|
|
602
|
+
} catch {
|
|
603
|
+
return null;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
function writePreferredLanguage(projectId, langCode) {
|
|
607
|
+
try {
|
|
608
|
+
if (langCode === null) {
|
|
609
|
+
window.localStorage.removeItem(PREFIX + projectId);
|
|
610
|
+
} else {
|
|
611
|
+
window.localStorage.setItem(PREFIX + projectId, langCode);
|
|
612
|
+
}
|
|
613
|
+
} catch {
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
478
617
|
// src/ui/flag-svgs.ts
|
|
479
618
|
var FLAG_IMAGES = {
|
|
480
619
|
us: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAAtCAMAAADFqPh+AAACClBMVEUZL13jrrHw0tQaMF7nurzsxsj46uv///+9PUT03t++rLjhqKv68PCcOkkfNWEwQ20lOmUcMV8dMmAvQ2wxRW4nPGc6TXXR1d9hcJAeNGE4S3MeM2A3SnI2SXEkOWV+iqQtQWs+UHdwfptndZQrP2o8T3Zba4s5THQoPWi+xNFSYoUgNmJKW39jcpEhNmPx8vVPYINZaYozR29ebY1reZc9UHbh5OpYaIkpPmiYordfb49IWX5MXYGTnbPS1t98iaM0R3BVZYcwRG4uQmwyRm8sQGqbpLh/i6WosMEqPmnn6e6Qm7FDVXtNXoLV2eFCVXqAjKU1SHE5THMmOmZod5XQ1d6apLiMl64iN2OGkqqHk6vc3+Z9iqTJztmcpbnN0tyPmrBUZIbf4ui5j5w/UXjNydO+nqnIu8UbMV7AxtNWZojCp7N4haD9/f7o6u6NmK8bMV+1vMvZ3eTM0dv8/f16hqGiq75sepccMl+eqLvr7fHL0Nr7/PyUnrNzgZ2WoLU7TnVldJJGWH3k5+yEkKmCjqeRm7G7ws/X2+Owt8dgb497iKJOX4JBU3nx8/Xi5etAUnh5haDHzNi6wc9ebo6OmbDFy9b09fd0gZ3Fsb2ep7onO2dicZAjOGSmrsBHWX3U2OFxf5tEVnustMW3vsyIk6tJWn9RYoTCyNR3hJ+oscJygJydprrny88smqUCAAAACXBIWXMAAC4jAAAuIwF4pT92AAACiUlEQVRIx9WURXMbQRCFOxk7tgMLkqyILLAtiy2DZEuKZWaMmSnMzMzMzMzwH/NGqcptd6uyB5Xf4aup3p6dhukh0tSafCXhY5nAXSwc21KcvY5/Bk61zaUb2hlR3e4yuO7dIxKVP3loJyqKdsDQES1SP3lTnIzkdphAr0cGPR5A8PpAn1egjauUlInNdrCYhz8aJpKnAm1Epgc3EU1wNMi/rs1TEj4ysp+uBAfGWsCRzwB7bAFtYzZQdbO0Kw4Pt0vkITS8JApZmpCz39UIQ6PLT5tXKwm+9ePw8jytA13TTkTy6SOWhd08leLuQvWCiSKZKeFsA0W3zIb+GoQEp5gQ1DcTDR04DKZqcLicnnUTVdVPIpWmu02al8RAhoYgeM/VAj56RjJj92uxdMw4QNqSoyRckvQOnOJN3sZ/Ks6ibBMN7604PInGUThZpVpttn09vIpqesEzJ2XwzRwg9JeAJf2Cep+dEq0jJ+OUJmTDB5IkesusQsYgWGlrgZIyBTNfvASej/ahYC/etaC901eQSu2dWs2CFZIhEgbPWdxgMEWv7ezVLSwHbwyC6lO1vxinXAj08RSvomChuR4+VYFqGKoDGlPVfhRove4BI0fwH5riFbR2VoKVnVbamaskPhisPObHMAzHnIyONUcExkIjpfuISTEJZvVqExlPXOYhnhoAj18bJuqq6eFdP1ShOZImsttsYKupCzSbyV/OfD4sjc1GkJ4vVxJR27cZJPql/isfotlmPEM/fuGGOeYzl2TeoV6wyQgvWDrOH5NxfsO+/+Q3bGERXFzQnKr/fnpXauq3cs55OqRvc64OUb4OZXFzgQ5lsVU5OrRUW7VCh7LYqmU6tERb9Qf3Y5Nqb/wo6gAAAABJRU5ErkJggg==",
|
|
@@ -629,10 +768,14 @@ function injectStyles() {
|
|
|
629
768
|
display: inline-flex;
|
|
630
769
|
align-items: center;
|
|
631
770
|
gap: 6px;
|
|
632
|
-
border: 1px solid rgba(0, 0, 0, 0.12);
|
|
633
|
-
background: #ffffff;
|
|
634
|
-
color: rgba(0, 0, 0, 0.75);
|
|
635
|
-
|
|
771
|
+
border: 1px solid var(--dabalang-border-color, rgba(0, 0, 0, 0.12));
|
|
772
|
+
background: var(--dabalang-bg, #ffffff);
|
|
773
|
+
color: var(--dabalang-color, rgba(0, 0, 0, 0.75));
|
|
774
|
+
/* Themeable so the switcher can match the host's own buttons. A widget
|
|
775
|
+
cannot know whether a site uses pills or 6px corners, and defaulting
|
|
776
|
+
to a pill made this the one control on the page that did not match.
|
|
777
|
+
Set --dabalang-radius on any ancestor to align it. */
|
|
778
|
+
border-radius: var(--dabalang-radius, 999px);
|
|
636
779
|
padding: 6px 12px;
|
|
637
780
|
cursor: pointer;
|
|
638
781
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
@@ -899,8 +1042,10 @@ function injectStyles() {
|
|
|
899
1042
|
max-height: 320px;
|
|
900
1043
|
overflow-y: auto;
|
|
901
1044
|
background: #ffffff;
|
|
902
|
-
border: 1px solid rgba(0, 0, 0, 0.1);
|
|
903
|
-
|
|
1045
|
+
border: 1px solid var(--dabalang-border-color, rgba(0, 0, 0, 0.1));
|
|
1046
|
+
/* Panel corners follow the trigger's family, one step softer, so a
|
|
1047
|
+
square-cornered host does not get a pill-shaped dropdown. */
|
|
1048
|
+
border-radius: var(--dabalang-panel-radius, 14px);
|
|
904
1049
|
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.14);
|
|
905
1050
|
padding: 6px;
|
|
906
1051
|
box-sizing: border-box;
|
|
@@ -1227,6 +1372,9 @@ var DabaLang = class {
|
|
|
1227
1372
|
this.switcher = null;
|
|
1228
1373
|
this.metadata = null;
|
|
1229
1374
|
this.pathRouter = null;
|
|
1375
|
+
this.navWatcher = null;
|
|
1376
|
+
/** Tail of the re-translation chain; see retranslateCurrentPage. */
|
|
1377
|
+
this.retranslating = Promise.resolve();
|
|
1230
1378
|
/** Currently active language (null = original). Tracked here rather
|
|
1231
1379
|
* than via the applier because a site-translated page (see
|
|
1232
1380
|
* isSiteTranslated) never runs the applier at all. */
|
|
@@ -1255,13 +1403,19 @@ var DabaLang = class {
|
|
|
1255
1403
|
});
|
|
1256
1404
|
this.switcher = renderLanguageSwitcher(
|
|
1257
1405
|
this.container,
|
|
1258
|
-
this.
|
|
1406
|
+
this.readyLanguages(),
|
|
1259
1407
|
this.metadata.sourceLang,
|
|
1260
1408
|
(langCode) => {
|
|
1261
1409
|
void this.selectLanguage(langCode);
|
|
1262
1410
|
},
|
|
1263
1411
|
this.options.view ?? "modal"
|
|
1264
1412
|
);
|
|
1413
|
+
if (typeof window !== "undefined") {
|
|
1414
|
+
this.navWatcher = new NavigationWatcher(() => {
|
|
1415
|
+
void this.retranslateCurrentPage();
|
|
1416
|
+
});
|
|
1417
|
+
this.navWatcher.start();
|
|
1418
|
+
}
|
|
1265
1419
|
if (this.metadata.enablePathRouting && typeof window !== "undefined") {
|
|
1266
1420
|
this.pathRouter = new PathRouter(this.metadata.languages);
|
|
1267
1421
|
const detected = this.pathRouter.detect(window.location.pathname);
|
|
@@ -1277,12 +1431,59 @@ var DabaLang = class {
|
|
|
1277
1431
|
}
|
|
1278
1432
|
}
|
|
1279
1433
|
}
|
|
1434
|
+
if (this.activeLang === null && !this.pathRouter) {
|
|
1435
|
+
const remembered = readPreferredLanguage(this.options.projectId);
|
|
1436
|
+
if (remembered) {
|
|
1437
|
+
const stillOffered = (this.metadata?.targetLanguages ?? []).includes(remembered);
|
|
1438
|
+
if (!stillOffered) {
|
|
1439
|
+
writePreferredLanguage(this.options.projectId, null);
|
|
1440
|
+
} else if (this.readyLanguages().includes(remembered)) {
|
|
1441
|
+
await this.selectLanguage(remembered, { navigate: false });
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
/**
|
|
1447
|
+
* Re-applies the active language to content that arrived after init.
|
|
1448
|
+
*
|
|
1449
|
+
* A no-op in the source language — there is nothing to apply — and on
|
|
1450
|
+
* a site-translated page, where the page already *is* the translation.
|
|
1451
|
+
*/
|
|
1452
|
+
async retranslateCurrentPage() {
|
|
1453
|
+
if (!this.applier || this.activeLang === null) return;
|
|
1454
|
+
if (this.isSiteTranslated(this.activeLang)) return;
|
|
1455
|
+
this.retranslating = this.retranslating.catch(() => void 0).then(async () => {
|
|
1456
|
+
if (!this.applier || this.activeLang === null) return;
|
|
1457
|
+
this.applier.recrawl();
|
|
1458
|
+
try {
|
|
1459
|
+
await this.applier.applyLanguage(this.activeLang);
|
|
1460
|
+
} catch (err) {
|
|
1461
|
+
this.handleError(err);
|
|
1462
|
+
}
|
|
1463
|
+
});
|
|
1464
|
+
await this.retranslating;
|
|
1280
1465
|
}
|
|
1281
1466
|
/** True when the site serves this language's content itself (CMS,
|
|
1282
1467
|
* localized build): a pre-translated ("developer") language. Combined
|
|
1283
1468
|
* with a prefix match, the widget never machine-translates that page.
|
|
1284
1469
|
* (A prefix-less developer language instead swaps its uploaded
|
|
1285
1470
|
* catalogue in place.) */
|
|
1471
|
+
/** Target languages that actually have translations behind them.
|
|
1472
|
+
*
|
|
1473
|
+
* A configured-but-unpopulated language is worse than a missing one: a
|
|
1474
|
+
* visitor picks it and watches the page translate live, paragraph by
|
|
1475
|
+
* paragraph, or lands on a half-translated page while a run is still
|
|
1476
|
+
* going. Languages with no config row at all are legacy
|
|
1477
|
+
* target_languages entries that predate per-language config — they are
|
|
1478
|
+
* shown, because hiding a language that has been serving traffic would
|
|
1479
|
+
* be a regression. */
|
|
1480
|
+
readyLanguages() {
|
|
1481
|
+
const configs = this.metadata?.languages ?? [];
|
|
1482
|
+
return (this.metadata?.targetLanguages ?? []).filter((code) => {
|
|
1483
|
+
const config = configs.find((l) => l.code === code);
|
|
1484
|
+
return config ? config.ready : true;
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1286
1487
|
isSiteTranslated(langCode) {
|
|
1287
1488
|
const config = this.metadata?.languages.find((l) => l.code === langCode);
|
|
1288
1489
|
return config?.provider === "developer";
|
|
@@ -1308,6 +1509,7 @@ var DabaLang = class {
|
|
|
1308
1509
|
}
|
|
1309
1510
|
this.activeLang = langCode;
|
|
1310
1511
|
this.switcher.setActive(langCode);
|
|
1512
|
+
writePreferredLanguage(this.options.projectId, langCode);
|
|
1311
1513
|
this.options.onLanguageChange?.(langCode ?? this.metadata?.sourceLang ?? "");
|
|
1312
1514
|
} catch (err) {
|
|
1313
1515
|
this.handleError(err);
|
|
@@ -1330,10 +1532,27 @@ var DabaLang = class {
|
|
|
1330
1532
|
whenReady() {
|
|
1331
1533
|
return this.ready;
|
|
1332
1534
|
}
|
|
1535
|
+
/**
|
|
1536
|
+
* Switch language programmatically — the same path the switcher takes,
|
|
1537
|
+
* so the choice is applied, persisted and reported identically.
|
|
1538
|
+
*
|
|
1539
|
+
* Exists because a host page often has its own reason to change
|
|
1540
|
+
* language: a footer link, a country picker, a preference synced from
|
|
1541
|
+
* an account. Without it those would have to fake a click on the
|
|
1542
|
+
* widget's own DOM.
|
|
1543
|
+
*
|
|
1544
|
+
* Pass null to return to the source language.
|
|
1545
|
+
*/
|
|
1546
|
+
async setLanguage(langCode) {
|
|
1547
|
+
await this.ready;
|
|
1548
|
+
await this.selectLanguage(langCode);
|
|
1549
|
+
}
|
|
1333
1550
|
currentLanguage() {
|
|
1334
1551
|
return this.activeLang;
|
|
1335
1552
|
}
|
|
1336
1553
|
destroy() {
|
|
1554
|
+
this.navWatcher?.stop();
|
|
1555
|
+
this.navWatcher = null;
|
|
1337
1556
|
this.editor?.detach();
|
|
1338
1557
|
this.switcher?.destroy();
|
|
1339
1558
|
this.docLang.restore();
|