@dabalabs/lang 0.0.7-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.
@@ -30,6 +30,17 @@ interface DabaLangOptions {
30
30
  * project's languages directly, anchored to the trigger — for sites
31
31
  * that want the languages visible in one click, no overlay. */
32
32
  view?: "modal" | "dropdown";
33
+ /**
34
+ * Translate each page into every configured language in the background,
35
+ * not just the one the visitor is reading.
36
+ *
37
+ * The gateway caches by content hash, so this makes the first visitor to
38
+ * a page pay for it once and everyone after — in any language — get it
39
+ * instantly. Off by default: it is extra requests per page view, and
40
+ * that should be a site owner's decision rather than something the
41
+ * widget starts doing on its own.
42
+ */
43
+ prewarm?: boolean;
33
44
  onError?: (error: DabaLangError) => void;
34
45
  onLanguageChange?: (langCode: string) => void;
35
46
  }
@@ -89,6 +100,7 @@ declare class DabaLang {
89
100
  private metadata;
90
101
  private pathRouter;
91
102
  private navWatcher;
103
+ private prewarmer;
92
104
  /** Tail of the re-translation chain; see retranslateCurrentPage. */
93
105
  private retranslating;
94
106
  /** Currently active language (null = original). Tracked here rather
@@ -104,6 +116,8 @@ declare class DabaLang {
104
116
  * A no-op in the source language — there is nothing to apply — and on
105
117
  * a site-translated page, where the page already *is* the translation.
106
118
  */
119
+ /** Warms every configured language except the one on screen. */
120
+ private startPrewarm;
107
121
  private retranslateCurrentPage;
108
122
  /** True when the site serves this language's content itself (CMS,
109
123
  * localized build): a pre-translated ("developer") language. Combined
@@ -180,4 +194,19 @@ declare class PathRouter {
180
194
  private matchPrefix;
181
195
  }
182
196
 
183
- export { DabaLang, type DabaLangError, type DabaLangOptions, type LanguageConfig, PathRouter, type ProjectMetadata, type TranslationResult };
197
+ /**
198
+ * Call as early as possible — ideally an inline `<script>` in `<head>`,
199
+ * before the body parses. Safe to call more than once.
200
+ *
201
+ * Text is hidden with `visibility`, not `display`: layout still happens,
202
+ * so nothing reflows when it is revealed and the page does not jump.
203
+ */
204
+ declare function preventFlash(projectId: string): void;
205
+ /** Reveals the page. Idempotent, and safe to call before preventFlash. */
206
+ declare function reveal(): void;
207
+ /** The inline snippet, for sites that want this without loading the widget
208
+ * first. Kept in one place so the published copy cannot drift from the
209
+ * implementation above. */
210
+ declare function inlineSnippet(projectId: string): string;
211
+
212
+ export { DabaLang, type DabaLangError, type DabaLangOptions, type LanguageConfig, PathRouter, type ProjectMetadata, type TranslationResult, inlineSnippet, preventFlash, reveal };
@@ -30,6 +30,17 @@ interface DabaLangOptions {
30
30
  * project's languages directly, anchored to the trigger — for sites
31
31
  * that want the languages visible in one click, no overlay. */
32
32
  view?: "modal" | "dropdown";
33
+ /**
34
+ * Translate each page into every configured language in the background,
35
+ * not just the one the visitor is reading.
36
+ *
37
+ * The gateway caches by content hash, so this makes the first visitor to
38
+ * a page pay for it once and everyone after — in any language — get it
39
+ * instantly. Off by default: it is extra requests per page view, and
40
+ * that should be a site owner's decision rather than something the
41
+ * widget starts doing on its own.
42
+ */
43
+ prewarm?: boolean;
33
44
  onError?: (error: DabaLangError) => void;
34
45
  onLanguageChange?: (langCode: string) => void;
35
46
  }
@@ -89,6 +100,7 @@ declare class DabaLang {
89
100
  private metadata;
90
101
  private pathRouter;
91
102
  private navWatcher;
103
+ private prewarmer;
92
104
  /** Tail of the re-translation chain; see retranslateCurrentPage. */
93
105
  private retranslating;
94
106
  /** Currently active language (null = original). Tracked here rather
@@ -104,6 +116,8 @@ declare class DabaLang {
104
116
  * A no-op in the source language — there is nothing to apply — and on
105
117
  * a site-translated page, where the page already *is* the translation.
106
118
  */
119
+ /** Warms every configured language except the one on screen. */
120
+ private startPrewarm;
107
121
  private retranslateCurrentPage;
108
122
  /** True when the site serves this language's content itself (CMS,
109
123
  * localized build): a pre-translated ("developer") language. Combined
@@ -180,4 +194,19 @@ declare class PathRouter {
180
194
  private matchPrefix;
181
195
  }
182
196
 
183
- export { DabaLang, type DabaLangError, type DabaLangOptions, type LanguageConfig, PathRouter, type ProjectMetadata, type TranslationResult };
197
+ /**
198
+ * Call as early as possible — ideally an inline `<script>` in `<head>`,
199
+ * before the body parses. Safe to call more than once.
200
+ *
201
+ * Text is hidden with `visibility`, not `display`: layout still happens,
202
+ * so nothing reflows when it is revealed and the page does not jump.
203
+ */
204
+ declare function preventFlash(projectId: string): void;
205
+ /** Reveals the page. Idempotent, and safe to call before preventFlash. */
206
+ declare function reveal(): void;
207
+ /** The inline snippet, for sites that want this without loading the widget
208
+ * first. Kept in one place so the published copy cannot drift from the
209
+ * implementation above. */
210
+ declare function inlineSnippet(projectId: string): string;
211
+
212
+ export { DabaLang, type DabaLangError, type DabaLangOptions, type LanguageConfig, PathRouter, type ProjectMetadata, type TranslationResult, inlineSnippet, preventFlash, reveal };
package/dist/dabalang.js CHANGED
@@ -313,6 +313,41 @@ var TranslationApplier = class {
313
313
  }
314
314
  return painted;
315
315
  }
316
+ /** True when every string on this page is already translated for
317
+ * `targetLang`, so warming it would be a request that changes nothing. */
318
+ isFullyCached(targetLang) {
319
+ const cached = this.fetchedLanguages.get(targetLang) ?? readCachedTranslations(this.projectId, targetLang) ?? void 0;
320
+ if (!cached) return false;
321
+ return this.sourceTexts().every((t) => cached.has(t));
322
+ }
323
+ /**
324
+ * Translates this page into `targetLang` WITHOUT touching the DOM.
325
+ *
326
+ * Used to warm languages the visitor is not reading. The gateway caches
327
+ * by content hash, so the first visitor to a page pays for it and every
328
+ * later visitor — in any language — gets it instantly. Painting here
329
+ * would replace the text the visitor is currently reading with a
330
+ * language they did not ask for, so this is deliberately fetch-only and
331
+ * shares nothing with applyLanguage beyond the cache it fills.
332
+ */
333
+ async warmLanguage(targetLang) {
334
+ const existing = this.fetchedLanguages.get(targetLang) ?? readCachedTranslations(this.projectId, targetLang) ?? /* @__PURE__ */ new Map();
335
+ const missing = this.sourceTexts().filter((t) => !existing.has(t));
336
+ if (missing.length === 0) {
337
+ this.fetchedLanguages.set(targetLang, existing);
338
+ return;
339
+ }
340
+ const results = await batchTranslate(
341
+ this.gatewayUrl,
342
+ this.projectId,
343
+ this.apiKey,
344
+ targetLang,
345
+ missing
346
+ );
347
+ for (const r of results) existing.set(r.sourceText, r.translatedText);
348
+ this.fetchedLanguages.set(targetLang, existing);
349
+ writeCachedTranslations(this.projectId, targetLang, existing);
350
+ }
316
351
  async applyLanguage(targetLang) {
317
352
  let cached = this.fetchedLanguages.get(targetLang);
318
353
  if (!cached) {
@@ -664,6 +699,93 @@ function writePreferredLanguage(projectId, langCode) {
664
699
  }
665
700
  }
666
701
 
702
+ // src/prevent-flash.ts
703
+ var STYLE_ID = "dabalang-prevent-flash";
704
+ var READY_ATTR = "data-dabalang-ready";
705
+ var PREF_PREFIX = "dabalang:lang:";
706
+ var MAX_HIDE_MS = 1200;
707
+ function preventFlash(projectId) {
708
+ if (typeof document === "undefined") return;
709
+ let preferred = null;
710
+ try {
711
+ preferred = window.localStorage.getItem(PREF_PREFIX + projectId);
712
+ } catch {
713
+ return;
714
+ }
715
+ if (!preferred) return;
716
+ if (document.getElementById(STYLE_ID)) return;
717
+ const style = document.createElement("style");
718
+ style.id = STYLE_ID;
719
+ 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; }`;
720
+ (document.head || document.documentElement).appendChild(style);
721
+ window.setTimeout(reveal, MAX_HIDE_MS);
722
+ }
723
+ function reveal() {
724
+ if (typeof document === "undefined") return;
725
+ document.documentElement.setAttribute(READY_ATTR, "");
726
+ }
727
+ function inlineSnippet(projectId) {
728
+ 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>`;
729
+ }
730
+
731
+ // src/prewarm.ts
732
+ var GAP_MS = 600;
733
+ function whenIdle(fn) {
734
+ const ric = window.requestIdleCallback;
735
+ if (typeof ric === "function") {
736
+ ric(fn, { timeout: 3e3 });
737
+ } else {
738
+ window.setTimeout(fn, 1200);
739
+ }
740
+ }
741
+ function warmOrder(targetLanguages, activeLang) {
742
+ return targetLanguages.filter((code) => code !== activeLang);
743
+ }
744
+ var Prewarmer = class {
745
+ constructor(target) {
746
+ this.target = target;
747
+ this.running = false;
748
+ this.cancelled = false;
749
+ }
750
+ /**
751
+ * Warms `languages` one at a time. Calling again while a run is in
752
+ * flight cancels the old one first, so a visitor clicking quickly
753
+ * through pages warms the page they landed on rather than queueing up
754
+ * every page they passed through.
755
+ */
756
+ start(languages) {
757
+ this.cancel();
758
+ const queue = languages.filter((lang) => !this.target.isFullyCached(lang));
759
+ if (queue.length === 0) return;
760
+ this.cancelled = false;
761
+ whenIdle(() => {
762
+ if (this.cancelled) return;
763
+ void this.run(queue);
764
+ });
765
+ }
766
+ async run(queue) {
767
+ if (this.running) return;
768
+ this.running = true;
769
+ try {
770
+ for (const lang of queue) {
771
+ if (this.cancelled) return;
772
+ try {
773
+ await this.target.warm(lang);
774
+ } catch {
775
+ }
776
+ if (this.cancelled) return;
777
+ await new Promise((r) => window.setTimeout(r, GAP_MS));
778
+ }
779
+ } finally {
780
+ this.running = false;
781
+ }
782
+ }
783
+ /** Stops after the language currently in flight. */
784
+ cancel() {
785
+ this.cancelled = true;
786
+ }
787
+ };
788
+
667
789
  // src/ui/flag-svgs.ts
668
790
  var FLAG_IMAGES = {
669
791
  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==",
@@ -715,7 +837,7 @@ var FLAG_IMAGES = {
715
837
  };
716
838
 
717
839
  // src/ui/language-switcher.ts
718
- var STYLE_ID = "dabalang-switcher-styles";
840
+ var STYLE_ID2 = "dabalang-switcher-styles";
719
841
  var LANGUAGE_LABELS = {
720
842
  en: "English",
721
843
  es: "Espa\xF1ol",
@@ -809,9 +931,9 @@ var ALL_WORLD_LANGUAGES = [
809
931
  { code: "is", label: "\xCDslenska", flag: "is" }
810
932
  ];
811
933
  function injectStyles() {
812
- if (document.getElementById(STYLE_ID)) return;
934
+ if (document.getElementById(STYLE_ID2)) return;
813
935
  const style = document.createElement("style");
814
- style.id = STYLE_ID;
936
+ style.id = STYLE_ID2;
815
937
  style.textContent = `
816
938
  .dabalang-trigger {
817
939
  appearance: none;
@@ -1425,6 +1547,7 @@ var DabaLang = class {
1425
1547
  this.metadata = null;
1426
1548
  this.pathRouter = null;
1427
1549
  this.navWatcher = null;
1550
+ this.prewarmer = null;
1428
1551
  /** Tail of the re-translation chain; see retranslateCurrentPage. */
1429
1552
  this.retranslating = Promise.resolve();
1430
1553
  /** Currently active language (null = original). Tracked here rather
@@ -1440,9 +1563,11 @@ var DabaLang = class {
1440
1563
  this.ready = this.init();
1441
1564
  }
1442
1565
  async init() {
1566
+ preventFlash(this.options.projectId);
1443
1567
  try {
1444
1568
  this.metadata = await fetchProjectMetadata(this.gatewayUrl, this.options.projectId, this.options.apiKey);
1445
1569
  } catch (err) {
1570
+ reveal();
1446
1571
  this.handleError(err);
1447
1572
  return;
1448
1573
  }
@@ -1494,6 +1619,8 @@ var DabaLang = class {
1494
1619
  }
1495
1620
  }
1496
1621
  }
1622
+ reveal();
1623
+ this.startPrewarm();
1497
1624
  }
1498
1625
  /**
1499
1626
  * Re-applies the active language to content that arrived after init.
@@ -1501,6 +1628,16 @@ var DabaLang = class {
1501
1628
  * A no-op in the source language — there is nothing to apply — and on
1502
1629
  * a site-translated page, where the page already *is* the translation.
1503
1630
  */
1631
+ /** Warms every configured language except the one on screen. */
1632
+ startPrewarm() {
1633
+ if (!this.options.prewarm || !this.applier) return;
1634
+ const applier = this.applier;
1635
+ this.prewarmer ?? (this.prewarmer = new Prewarmer({
1636
+ warm: (lang) => applier.warmLanguage(lang),
1637
+ isFullyCached: (lang) => applier.isFullyCached(lang)
1638
+ }));
1639
+ this.prewarmer.start(warmOrder(this.readyLanguages(), this.activeLang));
1640
+ }
1504
1641
  async retranslateCurrentPage() {
1505
1642
  if (!this.applier || this.activeLang === null) return;
1506
1643
  if (this.isSiteTranslated(this.activeLang)) return;
@@ -1512,6 +1649,7 @@ var DabaLang = class {
1512
1649
  } catch (err) {
1513
1650
  this.handleError(err);
1514
1651
  }
1652
+ this.startPrewarm();
1515
1653
  });
1516
1654
  await this.retranslating;
1517
1655
  }
@@ -1562,6 +1700,7 @@ var DabaLang = class {
1562
1700
  this.activeLang = langCode;
1563
1701
  this.switcher.setActive(langCode);
1564
1702
  writePreferredLanguage(this.options.projectId, langCode);
1703
+ this.startPrewarm();
1565
1704
  this.options.onLanguageChange?.(langCode ?? this.metadata?.sourceLang ?? "");
1566
1705
  } catch (err) {
1567
1706
  this.handleError(err);
@@ -1604,6 +1743,7 @@ var DabaLang = class {
1604
1743
  }
1605
1744
  destroy() {
1606
1745
  this.navWatcher?.stop();
1746
+ this.prewarmer?.cancel();
1607
1747
  this.navWatcher = null;
1608
1748
  this.editor?.detach();
1609
1749
  this.switcher?.destroy();
@@ -1611,6 +1751,6 @@ var DabaLang = class {
1611
1751
  }
1612
1752
  };
1613
1753
 
1614
- export { DabaLang, PathRouter };
1754
+ export { DabaLang, PathRouter, inlineSnippet, preventFlash, reveal };
1615
1755
  //# sourceMappingURL=dabalang.js.map
1616
1756
  //# sourceMappingURL=dabalang.js.map