@dabalabs/lang 0.0.6-beta → 0.0.8-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 +202 -14
- package/dist/dabalang.cjs.map +1 -1
- package/dist/dabalang.d.cts +30 -1
- package/dist/dabalang.d.ts +30 -1
- package/dist/dabalang.js +200 -15
- 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.cjs
CHANGED
|
@@ -145,6 +145,35 @@ async function submitCorrection(gatewayUrl, projectId, apiKey, targetLang, sourc
|
|
|
145
145
|
await parseOrThrow(response);
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
// src/translation-cache.ts
|
|
149
|
+
var PREFIX = "dabalang:tcache:";
|
|
150
|
+
var VERSION = 1;
|
|
151
|
+
var MAX_ENTRY_BYTES = 512 * 1024;
|
|
152
|
+
function key(projectId, lang) {
|
|
153
|
+
return `${PREFIX}${projectId}:${lang}`;
|
|
154
|
+
}
|
|
155
|
+
function readCachedTranslations(projectId, lang) {
|
|
156
|
+
try {
|
|
157
|
+
const raw = window.localStorage.getItem(key(projectId, lang));
|
|
158
|
+
if (!raw) return null;
|
|
159
|
+
const parsed = JSON.parse(raw);
|
|
160
|
+
if (!parsed || parsed.v !== VERSION || typeof parsed.t !== "object") return null;
|
|
161
|
+
return new Map(Object.entries(parsed.t));
|
|
162
|
+
} catch {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function writeCachedTranslations(projectId, lang, translations) {
|
|
167
|
+
try {
|
|
168
|
+
if (translations.size === 0) return;
|
|
169
|
+
const payload = { v: VERSION, t: Object.fromEntries(translations) };
|
|
170
|
+
const serialised = JSON.stringify(payload);
|
|
171
|
+
if (serialised.length > MAX_ENTRY_BYTES) return;
|
|
172
|
+
window.localStorage.setItem(key(projectId, lang), serialised);
|
|
173
|
+
} catch {
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
148
177
|
// src/dom/ignore.ts
|
|
149
178
|
var IGNORE_ATTR = "data-dabalang-ignore";
|
|
150
179
|
var SENSITIVE_ELEMENT_SELECTOR = `[${IGNORE_ATTR}], input[type="password"], input[type="hidden"], [autocomplete="current-password"], [autocomplete="new-password"], [autocomplete^="cc-"]`;
|
|
@@ -275,8 +304,63 @@ var TranslationApplier = class {
|
|
|
275
304
|
}
|
|
276
305
|
return null;
|
|
277
306
|
}
|
|
307
|
+
/** Writes every translation this map covers into the DOM. */
|
|
308
|
+
paint(translations) {
|
|
309
|
+
let painted = 0;
|
|
310
|
+
for (const [sourceText, nodes] of this.groups) {
|
|
311
|
+
const translated = translations.get(sourceText);
|
|
312
|
+
if (translated === void 0) continue;
|
|
313
|
+
for (const node of nodes) node.textContent = translated;
|
|
314
|
+
painted += 1;
|
|
315
|
+
}
|
|
316
|
+
return painted;
|
|
317
|
+
}
|
|
318
|
+
/** True when every string on this page is already translated for
|
|
319
|
+
* `targetLang`, so warming it would be a request that changes nothing. */
|
|
320
|
+
isFullyCached(targetLang) {
|
|
321
|
+
const cached = this.fetchedLanguages.get(targetLang) ?? readCachedTranslations(this.projectId, targetLang) ?? void 0;
|
|
322
|
+
if (!cached) return false;
|
|
323
|
+
return this.sourceTexts().every((t) => cached.has(t));
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Translates this page into `targetLang` WITHOUT touching the DOM.
|
|
327
|
+
*
|
|
328
|
+
* Used to warm languages the visitor is not reading. The gateway caches
|
|
329
|
+
* by content hash, so the first visitor to a page pays for it and every
|
|
330
|
+
* later visitor — in any language — gets it instantly. Painting here
|
|
331
|
+
* would replace the text the visitor is currently reading with a
|
|
332
|
+
* language they did not ask for, so this is deliberately fetch-only and
|
|
333
|
+
* shares nothing with applyLanguage beyond the cache it fills.
|
|
334
|
+
*/
|
|
335
|
+
async warmLanguage(targetLang) {
|
|
336
|
+
const existing = this.fetchedLanguages.get(targetLang) ?? readCachedTranslations(this.projectId, targetLang) ?? /* @__PURE__ */ new Map();
|
|
337
|
+
const missing = this.sourceTexts().filter((t) => !existing.has(t));
|
|
338
|
+
if (missing.length === 0) {
|
|
339
|
+
this.fetchedLanguages.set(targetLang, existing);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const results = await batchTranslate(
|
|
343
|
+
this.gatewayUrl,
|
|
344
|
+
this.projectId,
|
|
345
|
+
this.apiKey,
|
|
346
|
+
targetLang,
|
|
347
|
+
missing
|
|
348
|
+
);
|
|
349
|
+
for (const r of results) existing.set(r.sourceText, r.translatedText);
|
|
350
|
+
this.fetchedLanguages.set(targetLang, existing);
|
|
351
|
+
writeCachedTranslations(this.projectId, targetLang, existing);
|
|
352
|
+
}
|
|
278
353
|
async applyLanguage(targetLang) {
|
|
279
|
-
|
|
354
|
+
let cached = this.fetchedLanguages.get(targetLang);
|
|
355
|
+
if (!cached) {
|
|
356
|
+
const stored = readCachedTranslations(this.projectId, targetLang);
|
|
357
|
+
if (stored && stored.size > 0) {
|
|
358
|
+
cached = stored;
|
|
359
|
+
this.fetchedLanguages.set(targetLang, stored);
|
|
360
|
+
this.paint(stored);
|
|
361
|
+
this.activeLang = targetLang;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
280
364
|
const allTexts = this.sourceTexts();
|
|
281
365
|
const missing = cached ? allTexts.filter((t) => !cached.has(t)) : allTexts;
|
|
282
366
|
if (missing.length > 0) {
|
|
@@ -284,13 +368,9 @@ var TranslationApplier = class {
|
|
|
284
368
|
const map = cached ?? /* @__PURE__ */ new Map();
|
|
285
369
|
for (const r of results) map.set(r.sourceText, r.translatedText);
|
|
286
370
|
this.fetchedLanguages.set(targetLang, map);
|
|
371
|
+
writeCachedTranslations(this.projectId, targetLang, map);
|
|
287
372
|
}
|
|
288
|
-
|
|
289
|
-
for (const [sourceText, nodes] of this.groups) {
|
|
290
|
-
const translated = translations.get(sourceText);
|
|
291
|
-
if (translated === void 0) continue;
|
|
292
|
-
for (const node of nodes) node.textContent = translated;
|
|
293
|
-
}
|
|
373
|
+
this.paint(this.fetchedLanguages.get(targetLang));
|
|
294
374
|
this.activeLang = targetLang;
|
|
295
375
|
}
|
|
296
376
|
restoreOriginal() {
|
|
@@ -602,10 +682,10 @@ var PathRouter = class {
|
|
|
602
682
|
};
|
|
603
683
|
|
|
604
684
|
// src/preference.ts
|
|
605
|
-
var
|
|
685
|
+
var PREFIX2 = "dabalang:lang:";
|
|
606
686
|
function readPreferredLanguage(projectId) {
|
|
607
687
|
try {
|
|
608
|
-
return window.localStorage.getItem(
|
|
688
|
+
return window.localStorage.getItem(PREFIX2 + projectId);
|
|
609
689
|
} catch {
|
|
610
690
|
return null;
|
|
611
691
|
}
|
|
@@ -613,14 +693,101 @@ function readPreferredLanguage(projectId) {
|
|
|
613
693
|
function writePreferredLanguage(projectId, langCode) {
|
|
614
694
|
try {
|
|
615
695
|
if (langCode === null) {
|
|
616
|
-
window.localStorage.removeItem(
|
|
696
|
+
window.localStorage.removeItem(PREFIX2 + projectId);
|
|
617
697
|
} else {
|
|
618
|
-
window.localStorage.setItem(
|
|
698
|
+
window.localStorage.setItem(PREFIX2 + projectId, langCode);
|
|
619
699
|
}
|
|
620
700
|
} catch {
|
|
621
701
|
}
|
|
622
702
|
}
|
|
623
703
|
|
|
704
|
+
// src/prevent-flash.ts
|
|
705
|
+
var STYLE_ID = "dabalang-prevent-flash";
|
|
706
|
+
var READY_ATTR = "data-dabalang-ready";
|
|
707
|
+
var PREF_PREFIX = "dabalang:lang:";
|
|
708
|
+
var MAX_HIDE_MS = 1200;
|
|
709
|
+
function preventFlash(projectId) {
|
|
710
|
+
if (typeof document === "undefined") return;
|
|
711
|
+
let preferred = null;
|
|
712
|
+
try {
|
|
713
|
+
preferred = window.localStorage.getItem(PREF_PREFIX + projectId);
|
|
714
|
+
} catch {
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
if (!preferred) return;
|
|
718
|
+
if (document.getElementById(STYLE_ID)) return;
|
|
719
|
+
const style = document.createElement("style");
|
|
720
|
+
style.id = STYLE_ID;
|
|
721
|
+
style.textContent = `html:not([${READY_ATTR}]) body :not([data-dabalang-ignore]):not([data-dabalang-ignore] *) { visibility: hidden !important; }html:not([${READY_ATTR}]) body [data-dabalang-ignore], html:not([${READY_ATTR}]) body [data-dabalang-ignore] * { visibility: visible !important; }`;
|
|
722
|
+
(document.head || document.documentElement).appendChild(style);
|
|
723
|
+
window.setTimeout(reveal, MAX_HIDE_MS);
|
|
724
|
+
}
|
|
725
|
+
function reveal() {
|
|
726
|
+
if (typeof document === "undefined") return;
|
|
727
|
+
document.documentElement.setAttribute(READY_ATTR, "");
|
|
728
|
+
}
|
|
729
|
+
function inlineSnippet(projectId) {
|
|
730
|
+
return `<script>(function(){try{if(!localStorage.getItem('${PREF_PREFIX}${projectId}'))return;var s=document.createElement('style');s.id='${STYLE_ID}';s.textContent='html:not([${READY_ATTR}]) body :not([data-dabalang-ignore]):not([data-dabalang-ignore] *){visibility:hidden!important}';(document.head||document.documentElement).appendChild(s);setTimeout(function(){document.documentElement.setAttribute('${READY_ATTR}','')},${MAX_HIDE_MS});}catch(e){}})();</script>`;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// src/prewarm.ts
|
|
734
|
+
var GAP_MS = 600;
|
|
735
|
+
function whenIdle(fn) {
|
|
736
|
+
const ric = window.requestIdleCallback;
|
|
737
|
+
if (typeof ric === "function") {
|
|
738
|
+
ric(fn, { timeout: 3e3 });
|
|
739
|
+
} else {
|
|
740
|
+
window.setTimeout(fn, 1200);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
function warmOrder(targetLanguages, activeLang) {
|
|
744
|
+
return targetLanguages.filter((code) => code !== activeLang);
|
|
745
|
+
}
|
|
746
|
+
var Prewarmer = class {
|
|
747
|
+
constructor(target) {
|
|
748
|
+
this.target = target;
|
|
749
|
+
this.running = false;
|
|
750
|
+
this.cancelled = false;
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Warms `languages` one at a time. Calling again while a run is in
|
|
754
|
+
* flight cancels the old one first, so a visitor clicking quickly
|
|
755
|
+
* through pages warms the page they landed on rather than queueing up
|
|
756
|
+
* every page they passed through.
|
|
757
|
+
*/
|
|
758
|
+
start(languages) {
|
|
759
|
+
this.cancel();
|
|
760
|
+
const queue = languages.filter((lang) => !this.target.isFullyCached(lang));
|
|
761
|
+
if (queue.length === 0) return;
|
|
762
|
+
this.cancelled = false;
|
|
763
|
+
whenIdle(() => {
|
|
764
|
+
if (this.cancelled) return;
|
|
765
|
+
void this.run(queue);
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
async run(queue) {
|
|
769
|
+
if (this.running) return;
|
|
770
|
+
this.running = true;
|
|
771
|
+
try {
|
|
772
|
+
for (const lang of queue) {
|
|
773
|
+
if (this.cancelled) return;
|
|
774
|
+
try {
|
|
775
|
+
await this.target.warm(lang);
|
|
776
|
+
} catch {
|
|
777
|
+
}
|
|
778
|
+
if (this.cancelled) return;
|
|
779
|
+
await new Promise((r) => window.setTimeout(r, GAP_MS));
|
|
780
|
+
}
|
|
781
|
+
} finally {
|
|
782
|
+
this.running = false;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
/** Stops after the language currently in flight. */
|
|
786
|
+
cancel() {
|
|
787
|
+
this.cancelled = true;
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
|
|
624
791
|
// src/ui/flag-svgs.ts
|
|
625
792
|
var FLAG_IMAGES = {
|
|
626
793
|
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==",
|
|
@@ -672,7 +839,7 @@ var FLAG_IMAGES = {
|
|
|
672
839
|
};
|
|
673
840
|
|
|
674
841
|
// src/ui/language-switcher.ts
|
|
675
|
-
var
|
|
842
|
+
var STYLE_ID2 = "dabalang-switcher-styles";
|
|
676
843
|
var LANGUAGE_LABELS = {
|
|
677
844
|
en: "English",
|
|
678
845
|
es: "Espa\xF1ol",
|
|
@@ -766,9 +933,9 @@ var ALL_WORLD_LANGUAGES = [
|
|
|
766
933
|
{ code: "is", label: "\xCDslenska", flag: "is" }
|
|
767
934
|
];
|
|
768
935
|
function injectStyles() {
|
|
769
|
-
if (document.getElementById(
|
|
936
|
+
if (document.getElementById(STYLE_ID2)) return;
|
|
770
937
|
const style = document.createElement("style");
|
|
771
|
-
style.id =
|
|
938
|
+
style.id = STYLE_ID2;
|
|
772
939
|
style.textContent = `
|
|
773
940
|
.dabalang-trigger {
|
|
774
941
|
appearance: none;
|
|
@@ -1382,6 +1549,7 @@ var DabaLang = class {
|
|
|
1382
1549
|
this.metadata = null;
|
|
1383
1550
|
this.pathRouter = null;
|
|
1384
1551
|
this.navWatcher = null;
|
|
1552
|
+
this.prewarmer = null;
|
|
1385
1553
|
/** Tail of the re-translation chain; see retranslateCurrentPage. */
|
|
1386
1554
|
this.retranslating = Promise.resolve();
|
|
1387
1555
|
/** Currently active language (null = original). Tracked here rather
|
|
@@ -1397,9 +1565,11 @@ var DabaLang = class {
|
|
|
1397
1565
|
this.ready = this.init();
|
|
1398
1566
|
}
|
|
1399
1567
|
async init() {
|
|
1568
|
+
preventFlash(this.options.projectId);
|
|
1400
1569
|
try {
|
|
1401
1570
|
this.metadata = await fetchProjectMetadata(this.gatewayUrl, this.options.projectId, this.options.apiKey);
|
|
1402
1571
|
} catch (err) {
|
|
1572
|
+
reveal();
|
|
1403
1573
|
this.handleError(err);
|
|
1404
1574
|
return;
|
|
1405
1575
|
}
|
|
@@ -1451,6 +1621,8 @@ var DabaLang = class {
|
|
|
1451
1621
|
}
|
|
1452
1622
|
}
|
|
1453
1623
|
}
|
|
1624
|
+
reveal();
|
|
1625
|
+
this.startPrewarm();
|
|
1454
1626
|
}
|
|
1455
1627
|
/**
|
|
1456
1628
|
* Re-applies the active language to content that arrived after init.
|
|
@@ -1458,6 +1630,16 @@ var DabaLang = class {
|
|
|
1458
1630
|
* A no-op in the source language — there is nothing to apply — and on
|
|
1459
1631
|
* a site-translated page, where the page already *is* the translation.
|
|
1460
1632
|
*/
|
|
1633
|
+
/** Warms every configured language except the one on screen. */
|
|
1634
|
+
startPrewarm() {
|
|
1635
|
+
if (!this.options.prewarm || !this.applier) return;
|
|
1636
|
+
const applier = this.applier;
|
|
1637
|
+
this.prewarmer ?? (this.prewarmer = new Prewarmer({
|
|
1638
|
+
warm: (lang) => applier.warmLanguage(lang),
|
|
1639
|
+
isFullyCached: (lang) => applier.isFullyCached(lang)
|
|
1640
|
+
}));
|
|
1641
|
+
this.prewarmer.start(warmOrder(this.readyLanguages(), this.activeLang));
|
|
1642
|
+
}
|
|
1461
1643
|
async retranslateCurrentPage() {
|
|
1462
1644
|
if (!this.applier || this.activeLang === null) return;
|
|
1463
1645
|
if (this.isSiteTranslated(this.activeLang)) return;
|
|
@@ -1469,6 +1651,7 @@ var DabaLang = class {
|
|
|
1469
1651
|
} catch (err) {
|
|
1470
1652
|
this.handleError(err);
|
|
1471
1653
|
}
|
|
1654
|
+
this.startPrewarm();
|
|
1472
1655
|
});
|
|
1473
1656
|
await this.retranslating;
|
|
1474
1657
|
}
|
|
@@ -1519,6 +1702,7 @@ var DabaLang = class {
|
|
|
1519
1702
|
this.activeLang = langCode;
|
|
1520
1703
|
this.switcher.setActive(langCode);
|
|
1521
1704
|
writePreferredLanguage(this.options.projectId, langCode);
|
|
1705
|
+
this.startPrewarm();
|
|
1522
1706
|
this.options.onLanguageChange?.(langCode ?? this.metadata?.sourceLang ?? "");
|
|
1523
1707
|
} catch (err) {
|
|
1524
1708
|
this.handleError(err);
|
|
@@ -1561,6 +1745,7 @@ var DabaLang = class {
|
|
|
1561
1745
|
}
|
|
1562
1746
|
destroy() {
|
|
1563
1747
|
this.navWatcher?.stop();
|
|
1748
|
+
this.prewarmer?.cancel();
|
|
1564
1749
|
this.navWatcher = null;
|
|
1565
1750
|
this.editor?.detach();
|
|
1566
1751
|
this.switcher?.destroy();
|
|
@@ -1570,5 +1755,8 @@ var DabaLang = class {
|
|
|
1570
1755
|
|
|
1571
1756
|
exports.DabaLang = DabaLang;
|
|
1572
1757
|
exports.PathRouter = PathRouter;
|
|
1758
|
+
exports.inlineSnippet = inlineSnippet;
|
|
1759
|
+
exports.preventFlash = preventFlash;
|
|
1760
|
+
exports.reveal = reveal;
|
|
1573
1761
|
//# sourceMappingURL=dabalang.cjs.map
|
|
1574
1762
|
//# sourceMappingURL=dabalang.cjs.map
|