@dabalabs/lang 0.0.2-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 +196 -2
- package/dist/dabalang.cjs.map +1 -1
- package/dist/dabalang.d.cts +22 -0
- package/dist/dabalang.d.ts +22 -0
- package/dist/dabalang.js +196 -2
- package/dist/dabalang.js.map +1 -1
- package/dist/dabalang.min.js +2 -2
- package/dist/dabalang.min.js.map +1 -1
- package/package.json +1 -1
package/dist/dabalang.d.cts
CHANGED
|
@@ -88,6 +88,9 @@ declare class DabaLang {
|
|
|
88
88
|
private switcher;
|
|
89
89
|
private metadata;
|
|
90
90
|
private pathRouter;
|
|
91
|
+
private navWatcher;
|
|
92
|
+
/** Tail of the re-translation chain; see retranslateCurrentPage. */
|
|
93
|
+
private retranslating;
|
|
91
94
|
/** Currently active language (null = original). Tracked here rather
|
|
92
95
|
* than via the applier because a site-translated page (see
|
|
93
96
|
* isSiteTranslated) never runs the applier at all. */
|
|
@@ -95,6 +98,13 @@ declare class DabaLang {
|
|
|
95
98
|
private readonly ready;
|
|
96
99
|
constructor(options: DabaLangOptions);
|
|
97
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;
|
|
98
108
|
/** True when the site serves this language's content itself (CMS,
|
|
99
109
|
* localized build): a pre-translated ("developer") language. Combined
|
|
100
110
|
* with a prefix match, the widget never machine-translates that page.
|
|
@@ -118,6 +128,18 @@ declare class DabaLang {
|
|
|
118
128
|
/** Resolves once the initial metadata fetch + pill render has settled
|
|
119
129
|
* (successfully or not) — mainly useful in tests. */
|
|
120
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>;
|
|
121
143
|
currentLanguage(): string | null;
|
|
122
144
|
destroy(): void;
|
|
123
145
|
}
|
package/dist/dabalang.d.ts
CHANGED
|
@@ -88,6 +88,9 @@ declare class DabaLang {
|
|
|
88
88
|
private switcher;
|
|
89
89
|
private metadata;
|
|
90
90
|
private pathRouter;
|
|
91
|
+
private navWatcher;
|
|
92
|
+
/** Tail of the re-translation chain; see retranslateCurrentPage. */
|
|
93
|
+
private retranslating;
|
|
91
94
|
/** Currently active language (null = original). Tracked here rather
|
|
92
95
|
* than via the applier because a site-translated page (see
|
|
93
96
|
* isSiteTranslated) never runs the applier at all. */
|
|
@@ -95,6 +98,13 @@ declare class DabaLang {
|
|
|
95
98
|
private readonly ready;
|
|
96
99
|
constructor(options: DabaLangOptions);
|
|
97
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;
|
|
98
108
|
/** True when the site serves this language's content itself (CMS,
|
|
99
109
|
* localized build): a pre-translated ("developer") language. Combined
|
|
100
110
|
* with a prefix match, the widget never machine-translates that page.
|
|
@@ -118,6 +128,18 @@ declare class DabaLang {
|
|
|
118
128
|
/** Resolves once the initial metadata fetch + pill render has settled
|
|
119
129
|
* (successfully or not) — mainly useful in tests. */
|
|
120
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>;
|
|
121
143
|
currentLanguage(): string | null;
|
|
122
144
|
destroy(): void;
|
|
123
145
|
}
|
package/dist/dabalang.js
CHANGED
|
@@ -207,13 +207,38 @@ var TranslationApplier = class {
|
|
|
207
207
|
this.apiKey = apiKey;
|
|
208
208
|
this.fetchedLanguages = /* @__PURE__ */ new Map();
|
|
209
209
|
this.activeLang = null;
|
|
210
|
-
|
|
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);
|
|
211
236
|
}
|
|
212
237
|
/** Re-crawls the current DOM — call after content is known to have
|
|
213
238
|
* changed (e.g. after a route change in an SPA). Not called
|
|
214
239
|
* automatically; dabalang does one crawl per page load. */
|
|
215
240
|
recrawl() {
|
|
216
|
-
this.groups =
|
|
241
|
+
this.groups = this.absorb(crawlVisibleText(this.root));
|
|
217
242
|
}
|
|
218
243
|
currentLanguage() {
|
|
219
244
|
return this.activeLang;
|
|
@@ -435,6 +460,97 @@ var InlineEditor = class {
|
|
|
435
460
|
}
|
|
436
461
|
};
|
|
437
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
|
+
|
|
438
554
|
// src/path-routing.ts
|
|
439
555
|
var PathRouter = class {
|
|
440
556
|
constructor(languages) {
|
|
@@ -478,6 +594,26 @@ var PathRouter = class {
|
|
|
478
594
|
}
|
|
479
595
|
};
|
|
480
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
|
+
|
|
481
617
|
// src/ui/flag-svgs.ts
|
|
482
618
|
var FLAG_IMAGES = {
|
|
483
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==",
|
|
@@ -1236,6 +1372,9 @@ var DabaLang = class {
|
|
|
1236
1372
|
this.switcher = null;
|
|
1237
1373
|
this.metadata = null;
|
|
1238
1374
|
this.pathRouter = null;
|
|
1375
|
+
this.navWatcher = null;
|
|
1376
|
+
/** Tail of the re-translation chain; see retranslateCurrentPage. */
|
|
1377
|
+
this.retranslating = Promise.resolve();
|
|
1239
1378
|
/** Currently active language (null = original). Tracked here rather
|
|
1240
1379
|
* than via the applier because a site-translated page (see
|
|
1241
1380
|
* isSiteTranslated) never runs the applier at all. */
|
|
@@ -1271,6 +1410,12 @@ var DabaLang = class {
|
|
|
1271
1410
|
},
|
|
1272
1411
|
this.options.view ?? "modal"
|
|
1273
1412
|
);
|
|
1413
|
+
if (typeof window !== "undefined") {
|
|
1414
|
+
this.navWatcher = new NavigationWatcher(() => {
|
|
1415
|
+
void this.retranslateCurrentPage();
|
|
1416
|
+
});
|
|
1417
|
+
this.navWatcher.start();
|
|
1418
|
+
}
|
|
1274
1419
|
if (this.metadata.enablePathRouting && typeof window !== "undefined") {
|
|
1275
1420
|
this.pathRouter = new PathRouter(this.metadata.languages);
|
|
1276
1421
|
const detected = this.pathRouter.detect(window.location.pathname);
|
|
@@ -1286,6 +1431,37 @@ var DabaLang = class {
|
|
|
1286
1431
|
}
|
|
1287
1432
|
}
|
|
1288
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;
|
|
1289
1465
|
}
|
|
1290
1466
|
/** True when the site serves this language's content itself (CMS,
|
|
1291
1467
|
* localized build): a pre-translated ("developer") language. Combined
|
|
@@ -1333,6 +1509,7 @@ var DabaLang = class {
|
|
|
1333
1509
|
}
|
|
1334
1510
|
this.activeLang = langCode;
|
|
1335
1511
|
this.switcher.setActive(langCode);
|
|
1512
|
+
writePreferredLanguage(this.options.projectId, langCode);
|
|
1336
1513
|
this.options.onLanguageChange?.(langCode ?? this.metadata?.sourceLang ?? "");
|
|
1337
1514
|
} catch (err) {
|
|
1338
1515
|
this.handleError(err);
|
|
@@ -1355,10 +1532,27 @@ var DabaLang = class {
|
|
|
1355
1532
|
whenReady() {
|
|
1356
1533
|
return this.ready;
|
|
1357
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
|
+
}
|
|
1358
1550
|
currentLanguage() {
|
|
1359
1551
|
return this.activeLang;
|
|
1360
1552
|
}
|
|
1361
1553
|
destroy() {
|
|
1554
|
+
this.navWatcher?.stop();
|
|
1555
|
+
this.navWatcher = null;
|
|
1362
1556
|
this.editor?.detach();
|
|
1363
1557
|
this.switcher?.destroy();
|
|
1364
1558
|
this.docLang.restore();
|