@dabalabs/lang 0.0.2-beta → 0.0.4-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 +199 -5
- 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 +199 -5
- 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 +2 -2
package/dist/dabalang.cjs
CHANGED
|
@@ -209,13 +209,38 @@ var TranslationApplier = class {
|
|
|
209
209
|
this.apiKey = apiKey;
|
|
210
210
|
this.fetchedLanguages = /* @__PURE__ */ new Map();
|
|
211
211
|
this.activeLang = null;
|
|
212
|
-
|
|
212
|
+
/** The true source text of every node the widget has ever crawled.
|
|
213
|
+
*
|
|
214
|
+
* Needed because a re-crawl reads the DOM as it stands, and after a
|
|
215
|
+
* translation the DOM no longer holds the source. On a single-page app
|
|
216
|
+
* the shell — header, nav, footer — stays mounted across a navigation
|
|
217
|
+
* with its text already translated, so a naive re-crawl would record
|
|
218
|
+
* "Начать" as the source for the Get Started link. Everything
|
|
219
|
+
* downstream then works from a corrupted map: the widget pays to
|
|
220
|
+
* translate Russian into Russian, and restoreOriginal() writes the
|
|
221
|
+
* Russian back as if it were the original, leaving the shell stuck in a
|
|
222
|
+
* language the visitor just switched out of.
|
|
223
|
+
*
|
|
224
|
+
* Weak so it holds no node alive: entries vanish with the nodes. */
|
|
225
|
+
this.sourceByNode = /* @__PURE__ */ new WeakMap();
|
|
226
|
+
this.groups = this.absorb(crawlVisibleText(root));
|
|
227
|
+
}
|
|
228
|
+
/** Groups a crawl, preferring each node's remembered source text over
|
|
229
|
+
* whatever it currently displays, and remembering the rest. */
|
|
230
|
+
absorb(crawled) {
|
|
231
|
+
const corrected = crawled.map(({ node, sourceText }) => {
|
|
232
|
+
const known = this.sourceByNode.get(node);
|
|
233
|
+
if (known !== void 0) return { node, sourceText: known };
|
|
234
|
+
this.sourceByNode.set(node, sourceText);
|
|
235
|
+
return { node, sourceText };
|
|
236
|
+
});
|
|
237
|
+
return groupByText(corrected);
|
|
213
238
|
}
|
|
214
239
|
/** Re-crawls the current DOM — call after content is known to have
|
|
215
240
|
* changed (e.g. after a route change in an SPA). Not called
|
|
216
241
|
* automatically; dabalang does one crawl per page load. */
|
|
217
242
|
recrawl() {
|
|
218
|
-
this.groups =
|
|
243
|
+
this.groups = this.absorb(crawlVisibleText(this.root));
|
|
219
244
|
}
|
|
220
245
|
currentLanguage() {
|
|
221
246
|
return this.activeLang;
|
|
@@ -437,6 +462,97 @@ var InlineEditor = class {
|
|
|
437
462
|
}
|
|
438
463
|
};
|
|
439
464
|
|
|
465
|
+
// src/dom/navigation-watcher.ts
|
|
466
|
+
var SETTLE_MS = 250;
|
|
467
|
+
var FOLLOW_UP_PASSES = 3;
|
|
468
|
+
var subscribers = /* @__PURE__ */ new Set();
|
|
469
|
+
var unpatchHistory = null;
|
|
470
|
+
function subscribeToHistory(fn) {
|
|
471
|
+
subscribers.add(fn);
|
|
472
|
+
if (!unpatchHistory) {
|
|
473
|
+
const original = {
|
|
474
|
+
pushState: window.history.pushState,
|
|
475
|
+
replaceState: window.history.replaceState
|
|
476
|
+
};
|
|
477
|
+
for (const method of ["pushState", "replaceState"]) {
|
|
478
|
+
window.history[method] = function(...args) {
|
|
479
|
+
const result = original[method].apply(this, args);
|
|
480
|
+
for (const sub of [...subscribers]) sub();
|
|
481
|
+
return result;
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
unpatchHistory = () => {
|
|
485
|
+
window.history.pushState = original.pushState;
|
|
486
|
+
window.history.replaceState = original.replaceState;
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
return () => {
|
|
490
|
+
subscribers.delete(fn);
|
|
491
|
+
if (subscribers.size === 0 && unpatchHistory) {
|
|
492
|
+
unpatchHistory();
|
|
493
|
+
unpatchHistory = null;
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
var NavigationWatcher = class {
|
|
498
|
+
constructor(onNavigated) {
|
|
499
|
+
this.onNavigated = onNavigated;
|
|
500
|
+
this.observer = null;
|
|
501
|
+
this.timer = null;
|
|
502
|
+
this.lastPath = "";
|
|
503
|
+
this.passesLeft = 0;
|
|
504
|
+
this.restorers = [];
|
|
505
|
+
}
|
|
506
|
+
start() {
|
|
507
|
+
if (typeof window === "undefined" || this.observer) return;
|
|
508
|
+
this.lastPath = this.currentPath();
|
|
509
|
+
this.restorers.push(subscribeToHistory(() => this.onRouteMaybeChanged()));
|
|
510
|
+
const onPop = () => this.onRouteMaybeChanged();
|
|
511
|
+
window.addEventListener("popstate", onPop);
|
|
512
|
+
this.restorers.push(() => window.removeEventListener("popstate", onPop));
|
|
513
|
+
this.observer = new MutationObserver(() => {
|
|
514
|
+
if (this.currentPath() !== this.lastPath) {
|
|
515
|
+
this.schedule();
|
|
516
|
+
} else if (this.passesLeft > 0) {
|
|
517
|
+
this.schedule();
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
this.observer.observe(document.body, { childList: true, subtree: true });
|
|
521
|
+
}
|
|
522
|
+
currentPath() {
|
|
523
|
+
return window.location.pathname + window.location.search;
|
|
524
|
+
}
|
|
525
|
+
onRouteMaybeChanged() {
|
|
526
|
+
if (this.currentPath() === this.lastPath) return;
|
|
527
|
+
this.schedule();
|
|
528
|
+
}
|
|
529
|
+
/** Debounced: a route change produces a burst of mutations, and the
|
|
530
|
+
* handler must run once, after the last one. */
|
|
531
|
+
schedule() {
|
|
532
|
+
if (this.timer) clearTimeout(this.timer);
|
|
533
|
+
this.timer = setTimeout(() => {
|
|
534
|
+
this.timer = null;
|
|
535
|
+
const path = this.currentPath();
|
|
536
|
+
if (path !== this.lastPath) {
|
|
537
|
+
this.lastPath = path;
|
|
538
|
+
this.passesLeft = FOLLOW_UP_PASSES;
|
|
539
|
+
this.onNavigated();
|
|
540
|
+
} else if (this.passesLeft > 0) {
|
|
541
|
+
this.passesLeft -= 1;
|
|
542
|
+
this.onNavigated();
|
|
543
|
+
}
|
|
544
|
+
}, SETTLE_MS);
|
|
545
|
+
}
|
|
546
|
+
stop() {
|
|
547
|
+
this.observer?.disconnect();
|
|
548
|
+
this.observer = null;
|
|
549
|
+
if (this.timer) clearTimeout(this.timer);
|
|
550
|
+
this.timer = null;
|
|
551
|
+
this.passesLeft = 0;
|
|
552
|
+
while (this.restorers.length) this.restorers.pop()?.();
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
|
|
440
556
|
// src/path-routing.ts
|
|
441
557
|
var PathRouter = class {
|
|
442
558
|
constructor(languages) {
|
|
@@ -480,6 +596,26 @@ var PathRouter = class {
|
|
|
480
596
|
}
|
|
481
597
|
};
|
|
482
598
|
|
|
599
|
+
// src/preference.ts
|
|
600
|
+
var PREFIX = "dabalang:lang:";
|
|
601
|
+
function readPreferredLanguage(projectId) {
|
|
602
|
+
try {
|
|
603
|
+
return window.localStorage.getItem(PREFIX + projectId);
|
|
604
|
+
} catch {
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
function writePreferredLanguage(projectId, langCode) {
|
|
609
|
+
try {
|
|
610
|
+
if (langCode === null) {
|
|
611
|
+
window.localStorage.removeItem(PREFIX + projectId);
|
|
612
|
+
} else {
|
|
613
|
+
window.localStorage.setItem(PREFIX + projectId, langCode);
|
|
614
|
+
}
|
|
615
|
+
} catch {
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
483
619
|
// src/ui/flag-svgs.ts
|
|
484
620
|
var FLAG_IMAGES = {
|
|
485
621
|
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==",
|
|
@@ -534,15 +670,15 @@ var FLAG_IMAGES = {
|
|
|
534
670
|
var STYLE_ID = "dabalang-switcher-styles";
|
|
535
671
|
var LANGUAGE_LABELS = {
|
|
536
672
|
en: "English",
|
|
537
|
-
es: "Espa\
|
|
673
|
+
es: "Espa\xF1ol",
|
|
538
674
|
fr: "Fran\xE7ais",
|
|
539
|
-
de: "
|
|
675
|
+
de: "Deutsch",
|
|
540
676
|
pt: "Portugu\xEAs",
|
|
541
677
|
it: "Italiano",
|
|
542
678
|
ja: "\u65E5\u672C\u8A9E",
|
|
543
679
|
ko: "\uD55C\uAD6D\uC5B4",
|
|
544
680
|
zh: "\u4E2D\u6587",
|
|
545
|
-
ar: "\u0639\u0631\u0628\u064A",
|
|
681
|
+
ar: "\u0627\u0644\u0639\u0631\u0628\u064A\u0629",
|
|
546
682
|
hi: "\u0939\u093F\u0928\u094D\u0926\u0940",
|
|
547
683
|
ru: "\u0420\u0443\u0441\u0441\u043A\u0438\u0439",
|
|
548
684
|
nl: "Nederlands",
|
|
@@ -1238,6 +1374,9 @@ var DabaLang = class {
|
|
|
1238
1374
|
this.switcher = null;
|
|
1239
1375
|
this.metadata = null;
|
|
1240
1376
|
this.pathRouter = null;
|
|
1377
|
+
this.navWatcher = null;
|
|
1378
|
+
/** Tail of the re-translation chain; see retranslateCurrentPage. */
|
|
1379
|
+
this.retranslating = Promise.resolve();
|
|
1241
1380
|
/** Currently active language (null = original). Tracked here rather
|
|
1242
1381
|
* than via the applier because a site-translated page (see
|
|
1243
1382
|
* isSiteTranslated) never runs the applier at all. */
|
|
@@ -1273,6 +1412,12 @@ var DabaLang = class {
|
|
|
1273
1412
|
},
|
|
1274
1413
|
this.options.view ?? "modal"
|
|
1275
1414
|
);
|
|
1415
|
+
if (typeof window !== "undefined") {
|
|
1416
|
+
this.navWatcher = new NavigationWatcher(() => {
|
|
1417
|
+
void this.retranslateCurrentPage();
|
|
1418
|
+
});
|
|
1419
|
+
this.navWatcher.start();
|
|
1420
|
+
}
|
|
1276
1421
|
if (this.metadata.enablePathRouting && typeof window !== "undefined") {
|
|
1277
1422
|
this.pathRouter = new PathRouter(this.metadata.languages);
|
|
1278
1423
|
const detected = this.pathRouter.detect(window.location.pathname);
|
|
@@ -1288,6 +1433,37 @@ var DabaLang = class {
|
|
|
1288
1433
|
}
|
|
1289
1434
|
}
|
|
1290
1435
|
}
|
|
1436
|
+
if (this.activeLang === null && !this.pathRouter) {
|
|
1437
|
+
const remembered = readPreferredLanguage(this.options.projectId);
|
|
1438
|
+
if (remembered) {
|
|
1439
|
+
const stillOffered = (this.metadata?.targetLanguages ?? []).includes(remembered);
|
|
1440
|
+
if (!stillOffered) {
|
|
1441
|
+
writePreferredLanguage(this.options.projectId, null);
|
|
1442
|
+
} else if (this.readyLanguages().includes(remembered)) {
|
|
1443
|
+
await this.selectLanguage(remembered, { navigate: false });
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
/**
|
|
1449
|
+
* Re-applies the active language to content that arrived after init.
|
|
1450
|
+
*
|
|
1451
|
+
* A no-op in the source language — there is nothing to apply — and on
|
|
1452
|
+
* a site-translated page, where the page already *is* the translation.
|
|
1453
|
+
*/
|
|
1454
|
+
async retranslateCurrentPage() {
|
|
1455
|
+
if (!this.applier || this.activeLang === null) return;
|
|
1456
|
+
if (this.isSiteTranslated(this.activeLang)) return;
|
|
1457
|
+
this.retranslating = this.retranslating.catch(() => void 0).then(async () => {
|
|
1458
|
+
if (!this.applier || this.activeLang === null) return;
|
|
1459
|
+
this.applier.recrawl();
|
|
1460
|
+
try {
|
|
1461
|
+
await this.applier.applyLanguage(this.activeLang);
|
|
1462
|
+
} catch (err) {
|
|
1463
|
+
this.handleError(err);
|
|
1464
|
+
}
|
|
1465
|
+
});
|
|
1466
|
+
await this.retranslating;
|
|
1291
1467
|
}
|
|
1292
1468
|
/** True when the site serves this language's content itself (CMS,
|
|
1293
1469
|
* localized build): a pre-translated ("developer") language. Combined
|
|
@@ -1335,6 +1511,7 @@ var DabaLang = class {
|
|
|
1335
1511
|
}
|
|
1336
1512
|
this.activeLang = langCode;
|
|
1337
1513
|
this.switcher.setActive(langCode);
|
|
1514
|
+
writePreferredLanguage(this.options.projectId, langCode);
|
|
1338
1515
|
this.options.onLanguageChange?.(langCode ?? this.metadata?.sourceLang ?? "");
|
|
1339
1516
|
} catch (err) {
|
|
1340
1517
|
this.handleError(err);
|
|
@@ -1357,10 +1534,27 @@ var DabaLang = class {
|
|
|
1357
1534
|
whenReady() {
|
|
1358
1535
|
return this.ready;
|
|
1359
1536
|
}
|
|
1537
|
+
/**
|
|
1538
|
+
* Switch language programmatically — the same path the switcher takes,
|
|
1539
|
+
* so the choice is applied, persisted and reported identically.
|
|
1540
|
+
*
|
|
1541
|
+
* Exists because a host page often has its own reason to change
|
|
1542
|
+
* language: a footer link, a country picker, a preference synced from
|
|
1543
|
+
* an account. Without it those would have to fake a click on the
|
|
1544
|
+
* widget's own DOM.
|
|
1545
|
+
*
|
|
1546
|
+
* Pass null to return to the source language.
|
|
1547
|
+
*/
|
|
1548
|
+
async setLanguage(langCode) {
|
|
1549
|
+
await this.ready;
|
|
1550
|
+
await this.selectLanguage(langCode);
|
|
1551
|
+
}
|
|
1360
1552
|
currentLanguage() {
|
|
1361
1553
|
return this.activeLang;
|
|
1362
1554
|
}
|
|
1363
1555
|
destroy() {
|
|
1556
|
+
this.navWatcher?.stop();
|
|
1557
|
+
this.navWatcher = null;
|
|
1364
1558
|
this.editor?.detach();
|
|
1365
1559
|
this.switcher?.destroy();
|
|
1366
1560
|
this.docLang.restore();
|