langsys-js-typescript 0.2.3 → 0.4.0

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/index.js CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  var intlMessageformat = require('intl-messageformat');
4
4
 
5
+ // src/locale.ts
6
+ function canonicalizeLocale(locale) {
7
+ if (!locale || typeof locale !== "string") return locale;
8
+ const cleaned = locale.trim().replace(/_/g, "-");
9
+ try {
10
+ const [canonical] = Intl.getCanonicalLocales(cleaned);
11
+ return canonical ?? cleaned;
12
+ } catch {
13
+ return cleaned.split("-").map((part, i) => {
14
+ if (i === 0) return part.toLowerCase();
15
+ if (part.length === 4) return part[0].toUpperCase() + part.slice(1).toLowerCase();
16
+ if (part.length === 2 || /^\d{3}$/.test(part)) return part.toUpperCase();
17
+ return part.toLowerCase();
18
+ }).join("-");
19
+ }
20
+ }
21
+ function maximizedLangScript(locale) {
22
+ try {
23
+ const max = new Intl.Locale(canonicalizeLocale(locale)).maximize();
24
+ return `${max.language}-${max.script ?? ""}`;
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
5
30
  // src/signal.ts
6
31
  function createSignal(initial) {
7
32
  let value = initial;
@@ -93,7 +118,7 @@ var LangsysAppAPIClass = class {
93
118
  return await this.get("authorize-project/[projectid]");
94
119
  }
95
120
  async getTranslations(locale) {
96
- return await this.get("translations", { project_id: this.config.projectid, locale });
121
+ return await this.get("translations", { project_id: this.config.projectid, locale: canonicalizeLocale(locale) });
97
122
  }
98
123
  /**
99
124
  * Register phrases and/or content blocks via the current unified endpoint.
@@ -239,24 +264,37 @@ var config = {
239
264
  function isICU(template) {
240
265
  return ICU_PATTERN.test(template);
241
266
  }
242
- var ICU_PATTERN = /\{[^{}]+,\s*(plural|select|selectordinal|number|date|time)\s*,/;
267
+ var ICU_PATTERN = /\{[^{}]+,\s*(plural|select|selectordinal|number|date|time)\s*[,}]/;
243
268
  function interpolate(template, params, locale) {
244
269
  if (isICU(template)) {
245
270
  try {
246
- return new intlMessageformat.IntlMessageFormat(template, locale ?? "en").format(params);
271
+ return new intlMessageformat.IntlMessageFormat(template, locale || "en").format(params);
247
272
  } catch {
248
- return simpleInterpolate(template, params);
273
+ return simpleInterpolate(template, params, locale);
249
274
  }
250
275
  }
251
- return simpleInterpolate(template, params);
276
+ return simpleInterpolate(template, params, locale);
252
277
  }
253
- function simpleInterpolate(template, params) {
278
+ function simpleInterpolate(template, params, locale) {
254
279
  return template.replace(/\{([^{},]+)\}/g, (match, rawKey) => {
255
280
  const key = rawKey.trim();
256
281
  if (!(key in params)) return match;
257
282
  const value = params[key];
258
283
  if (value === void 0 || value === null) return match;
259
- if (value instanceof Date) return value.toISOString();
284
+ if (value instanceof Date) {
285
+ try {
286
+ return new Intl.DateTimeFormat(locale || "en", { dateStyle: "medium" }).format(value);
287
+ } catch {
288
+ return value.toISOString();
289
+ }
290
+ }
291
+ if (typeof value === "number" || typeof value === "bigint") {
292
+ try {
293
+ return new Intl.NumberFormat(locale || "en").format(value);
294
+ } catch {
295
+ return String(value);
296
+ }
297
+ }
260
298
  return String(value);
261
299
  });
262
300
  }
@@ -282,7 +320,7 @@ var Translations = class {
282
320
  });
283
321
  this.config = config2;
284
322
  this.debug = new Logger(!!config2.debug);
285
- this.locale = config2.baseLocale || "";
323
+ this.locale = canonicalizeLocale(config2.baseLocale || "");
286
324
  this.tSignal = createSignal(this.buildTFn());
287
325
  sTranslations.subscribe(() => this.tSignal.set(this.buildTFn()));
288
326
  currentlyLoadedLocale.subscribe(() => this.tSignal.set(this.buildTFn()));
@@ -423,7 +461,7 @@ var Translations = class {
423
461
  this.config = config2;
424
462
  if (!this.config.projectid || !this.config.key) return;
425
463
  if (this.config.debug) this.debug.debugEnabled = this.config.debug;
426
- if (config2.baseLocale) this.locale = config2.baseLocale;
464
+ if (config2.baseLocale) this.locale = canonicalizeLocale(config2.baseLocale);
427
465
  this.debug.log("TRANSLATION SETUP INITIATED", this.config);
428
466
  if (typeof window !== "undefined") {
429
467
  if (this.isFirstClientRun && this.missingTokens.length > 0) {
@@ -451,6 +489,7 @@ var Translations = class {
451
489
  */
452
490
  markLoaded(locale) {
453
491
  if (!locale) return;
492
+ locale = canonicalizeLocale(locale);
454
493
  this.locale = locale;
455
494
  this.lastLoaded[locale] = (/* @__PURE__ */ new Date()).getTime() / 1e3;
456
495
  this.readyResolve();
@@ -462,6 +501,7 @@ var Translations = class {
462
501
  async change(locale, force = false, skipFetch = false) {
463
502
  if (!locale) return false;
464
503
  if (!this.config.projectid || !this.config.key) return false;
504
+ locale = canonicalizeLocale(locale);
465
505
  if (skipFetch) {
466
506
  this.debug.log("Using pre-fetched translations for locale", locale);
467
507
  this.locale = locale;
@@ -630,11 +670,11 @@ var LangsysAppClass = class {
630
670
  }
631
671
  if (!UserLocaleStore?.subscribe || typeof UserLocaleStore.get !== "function") {
632
672
  this.debug.error(
633
- "LangsysApp.init missing UserLocaleStore \u2014 pass any object satisfying LocaleSource (get + subscribe). createSignal('en-us') works if you have nothing of your own."
673
+ "LangsysApp.init missing UserLocaleStore \u2014 pass any object satisfying LocaleSource (get + subscribe). createSignal('en-US') works if you have nothing of your own."
634
674
  );
635
675
  return { status: false, errors: ["Missing UserLocaleStore"] };
636
676
  }
637
- baseLocale = baseLocale.toLowerCase();
677
+ baseLocale = canonicalizeLocale(baseLocale);
638
678
  if (!emulateFailureToLoad) {
639
679
  this.config = {
640
680
  projectid,
@@ -673,8 +713,9 @@ var LangsysAppClass = class {
673
713
  if (initialTranslations2 && initialTranslationsLocale) {
674
714
  this.Translations.markLoaded(initialTranslationsLocale);
675
715
  }
676
- this.config.sUserLocale.subscribe((locale) => {
716
+ this.config.sUserLocale.subscribe((rawLocale) => {
677
717
  if (!validateResponse.status) return;
718
+ const locale = canonicalizeLocale(rawLocale);
678
719
  this.getLocalesData(locale);
679
720
  this.debug.log("SUBSCRIBING TO USER LOCALE STORE");
680
721
  this.translationsLoadingPromise = this.Translations.change(locale, false);
@@ -682,7 +723,7 @@ var LangsysAppClass = class {
682
723
  return validateResponse;
683
724
  }
684
725
  async getDialCodes(inLocale) {
685
- const locale = inLocale || this.config.sUserLocale.get() || this.config.baseLocale;
726
+ const locale = this.resolveLocale(inLocale);
686
727
  if (this.dialCodes.length && this.dialCodesLocale === locale) return this.dialCodes;
687
728
  const route = `countries/dial-codes/${locale}`;
688
729
  const response = await LangsysAppAPI.get(route);
@@ -695,7 +736,7 @@ var LangsysAppClass = class {
695
736
  return this.dialCodes;
696
737
  }
697
738
  async getCurrencies(inLocale) {
698
- const locale = inLocale || this.config.sUserLocale.get() || this.config.baseLocale;
739
+ const locale = this.resolveLocale(inLocale);
699
740
  if (this.currencies.length && this.currenciesLocale === locale) return this.currencies;
700
741
  const route = `currencies/${locale}`;
701
742
  const response = await LangsysAppAPI.get(route);
@@ -708,7 +749,7 @@ var LangsysAppClass = class {
708
749
  return this.currencies;
709
750
  }
710
751
  async getCountries(inLocale) {
711
- const locale = inLocale || this.config.sUserLocale.get() || this.config.baseLocale;
752
+ const locale = this.resolveLocale(inLocale);
712
753
  if (this.countries.length && this.countriesLocale === locale) return this.countries;
713
754
  const route = `countries/${locale}`;
714
755
  const response = await LangsysAppAPI.get(route);
@@ -722,7 +763,7 @@ var LangsysAppClass = class {
722
763
  }
723
764
  async getCountryName(forCountryCode, inLocale) {
724
765
  if (!forCountryCode) return "";
725
- const locale = inLocale || this.config.sUserLocale.get() || this.config.baseLocale;
766
+ const locale = this.resolveLocale(inLocale);
726
767
  if (!this.countries.length || this.countriesLocale !== locale) await this.getCountries(inLocale);
727
768
  const country = this.countries.find((c) => c.code.toLowerCase() === forCountryCode.toLowerCase())?.label;
728
769
  if (!country) this.debug.warn("getCountryName failed to match", forCountryCode);
@@ -730,14 +771,14 @@ var LangsysAppClass = class {
730
771
  }
731
772
  async getCurrencyName(forCurrencyCode, inLocale) {
732
773
  if (!forCurrencyCode) return "";
733
- const locale = inLocale || this.config.sUserLocale.get() || this.config.baseLocale;
774
+ const locale = this.resolveLocale(inLocale);
734
775
  if (!this.currencies.length || this.currenciesLocale !== locale) await this.getCurrencies(inLocale);
735
776
  const currency = this.currencies.find((c) => c.code?.toLowerCase() === forCurrencyCode.toLowerCase())?.name;
736
777
  if (!currency) this.debug.warn("getCurrencyName failed to match", forCurrencyCode);
737
778
  return currency || forCurrencyCode;
738
779
  }
739
780
  async getLocalesFormat(format = "", inLocale) {
740
- const locale = inLocale || this.config.sUserLocale.get() || this.config.baseLocale;
781
+ const locale = this.resolveLocale(inLocale);
741
782
  const route = `locales/${locale}` + (format ? `/${format}` : "");
742
783
  const response = await LangsysAppAPI.get(route);
743
784
  if (response.errors || !response.status) {
@@ -758,7 +799,7 @@ var LangsysAppClass = class {
758
799
  return await this.getLocalesFormat("flat", inLocale);
759
800
  }
760
801
  async getLocalesData(inLocale, forceRefresh = false) {
761
- const locale = inLocale || this.config.sUserLocale.get() || this.config.baseLocale;
802
+ const locale = this.resolveLocale(inLocale);
762
803
  if (this.locales?.[locale]?.length && !forceRefresh) return this.locales[locale];
763
804
  this.locales[locale] = await this.getLocalesFormat("data", inLocale);
764
805
  return this.locales[locale];
@@ -768,15 +809,16 @@ var LangsysAppClass = class {
768
809
  }
769
810
  async getLocaleNameWithLookup(forLocale, shortName = false, inLocale) {
770
811
  if (!forLocale) return "";
771
- const locale = inLocale || this.config.sUserLocale.get() || this.config.baseLocale;
812
+ const locale = this.resolveLocale(inLocale);
772
813
  await this.getLocalesData(locale);
773
814
  return this.getLocaleName(forLocale, shortName, locale);
774
815
  }
775
816
  getLocaleName(forLocale, shortName = false, inLocale) {
776
- const locale = inLocale || this.config.sUserLocale.get() || this.config.baseLocale;
817
+ const locale = this.resolveLocale(inLocale);
818
+ const target = canonicalizeLocale(forLocale).toLowerCase();
777
819
  let name = "";
778
820
  this.locales[locale]?.every((loc) => {
779
- if (loc.code === forLocale) {
821
+ if (loc.code.toLowerCase() === target) {
780
822
  name = shortName ? loc.lang_name : loc.locale_name;
781
823
  return false;
782
824
  }
@@ -802,7 +844,7 @@ var LangsysAppClass = class {
802
844
  const bestMatch = this.findBestLocaleMatch(userLocales, supportedLocales);
803
845
  if (bestMatch) return bestMatch;
804
846
  }
805
- return this.normalizeLocale(userLocales[0]);
847
+ return canonicalizeLocale(userLocales[0]);
806
848
  }
807
849
  getUserLanguagePreferences(acceptLanguageHeader) {
808
850
  if (acceptLanguageHeader) {
@@ -839,30 +881,26 @@ var LangsysAppClass = class {
839
881
  }
840
882
  findBestLocaleMatch(userLocales, supportedLocales) {
841
883
  if (!userLocales.length || !supportedLocales.length) return null;
842
- const normalizedSupported = supportedLocales.map((l) => this.normalizeLocale(l));
884
+ const supported = supportedLocales.map((l) => canonicalizeLocale(l));
843
885
  for (const userLocale of userLocales) {
844
- const normalizedUser = this.normalizeLocale(userLocale);
845
- if (normalizedSupported.includes(normalizedUser)) return normalizedUser;
886
+ const user = canonicalizeLocale(userLocale);
887
+ if (supported.includes(user)) return user;
846
888
  }
847
889
  for (const userLocale of userLocales) {
848
- const userLang = this.getLanguageCode(userLocale);
849
- for (const supported of normalizedSupported) {
850
- if (this.getLanguageCode(supported) === userLang) return supported;
851
- }
890
+ const userKey = maximizedLangScript(userLocale);
891
+ if (!userKey) continue;
892
+ const match = supported.find((s) => maximizedLangScript(s) === userKey);
893
+ if (match) return match;
852
894
  }
853
895
  return null;
854
896
  }
855
- normalizeLocale(locale) {
856
- if (!locale || typeof locale !== "string") return locale;
857
- const parts = locale.split(/[-_]/);
858
- if (parts.length === 1) return parts[0].toLowerCase();
859
- const language = parts[0].toLowerCase();
860
- const region = parts[1].toUpperCase();
861
- return `${language}-${region}`;
862
- }
863
- getLanguageCode(locale) {
864
- if (!locale || typeof locale !== "string") return "";
865
- return locale.split(/[-_]/)[0].toLowerCase();
897
+ /**
898
+ * Resolve the effective locale for a data-fetching helper: explicit
899
+ * argument, else the user's current locale, else the base locale —
900
+ * always in BCP 47 canonical form so cache keys and API routes agree.
901
+ */
902
+ resolveLocale(inLocale) {
903
+ return canonicalizeLocale(inLocale || this.config.sUserLocale.get() || this.config.baseLocale);
866
904
  }
867
905
  };
868
906
  var LangsysApp = new LangsysAppClass();
@@ -1215,6 +1253,14 @@ var Translate = class {
1215
1253
  if (locale && this.parseComplete) this.translateUpdate(locale);
1216
1254
  });
1217
1255
  }
1256
+ /** Update interpolation params (e.g. a changed count) and re-render. Mirrors `Phrase.setParams`. */
1257
+ setParams(params) {
1258
+ this.options.params = params;
1259
+ if (!this.parseComplete) return;
1260
+ this.lastTranslatedLocale = "";
1261
+ const locale = currentlyLoadedLocale.get();
1262
+ if (locale) this.translateUpdate(locale);
1263
+ }
1218
1264
  /** Stop reacting to locale changes. Safe to call multiple times. */
1219
1265
  destroy() {
1220
1266
  if (this.unsubscribeLocale) {
@@ -1227,7 +1273,7 @@ var Translate = class {
1227
1273
  if (this.lastTranslatedLocale === currentLocale) return;
1228
1274
  const { category = "" } = this.options;
1229
1275
  if (this.tokens.length === 1) {
1230
- this.element.innerText = LangsysApp.Translations.t(this.tokens[0], category);
1276
+ this.element.innerText = this.applyParams(LangsysApp.Translations.t(this.tokens[0], category));
1231
1277
  } else {
1232
1278
  this.translate(Array.from(this.element.childNodes));
1233
1279
  this.lastTranslatedLocale = currentLocale;
@@ -1245,7 +1291,7 @@ var Translate = class {
1245
1291
  this.tokens = tokens;
1246
1292
  const { category = "" } = this.options;
1247
1293
  if (this.tokens.length === 1) {
1248
- this.element.innerText = LangsysApp.Translations.t(this.tokens[0], category);
1294
+ this.element.innerText = this.applyParams(LangsysApp.Translations.t(this.tokens[0], category));
1249
1295
  } else {
1250
1296
  const contentBlock = {
1251
1297
  custom_id: "",
@@ -1281,7 +1327,7 @@ var Translate = class {
1281
1327
  }
1282
1328
  translate(nodes) {
1283
1329
  const currentLocale = currentlyLoadedLocale.get();
1284
- if (currentLocale === config.baseLocale && (this.lastTranslatedLocale === "" || this.lastTranslatedLocale === config.baseLocale)) {
1330
+ if (currentLocale === config.baseLocale && (this.lastTranslatedLocale === "" || this.lastTranslatedLocale === config.baseLocale) && isEmpty(this.options.params)) {
1285
1331
  return;
1286
1332
  }
1287
1333
  nodes.forEach((node) => {
@@ -1304,9 +1350,10 @@ var Translate = class {
1304
1350
  if (!contentToken) return;
1305
1351
  const translation = this.getTranslation(contentToken);
1306
1352
  if (translation && contentToken && node.originalNodeValue) {
1307
- node.nodeValue = node.originalNodeValue.replace(contentToken, translation);
1353
+ const resolved = this.applyParams(translation);
1354
+ node.nodeValue = node.originalNodeValue.replace(contentToken, () => resolved);
1308
1355
  } else if (node.originalNodeValue) {
1309
- node.nodeValue = node.originalNodeValue;
1356
+ node.nodeValue = this.applyParams(node.originalNodeValue);
1310
1357
  }
1311
1358
  });
1312
1359
  }
@@ -1315,6 +1362,16 @@ var Translate = class {
1315
1362
  const { category = "" } = this.options;
1316
1363
  return LangsysApp.Translations.lookupContent(category, this.custom_id, token);
1317
1364
  }
1365
+ /**
1366
+ * Interpolate `{name}`-style params into a resolved text, matching `t()`:
1367
+ * unknown keys fall through untouched, and the untranslated fallback is
1368
+ * interpolated too. No-op when no params were supplied.
1369
+ */
1370
+ applyParams(text) {
1371
+ const { params } = this.options;
1372
+ if (isEmpty(params)) return text;
1373
+ return interpolate(text, params, currentlyLoadedLocale.get());
1374
+ }
1318
1375
  translateAttributes(element) {
1319
1376
  const tagName = element.tagName.toLowerCase();
1320
1377
  if (!element.originalAttributes) element.originalAttributes = {};
@@ -1341,7 +1398,7 @@ var Translate = class {
1341
1398
  const originalText = optionEl.originalAttributes["textContent"].trim();
1342
1399
  if (originalText) {
1343
1400
  const translation = this.getTranslation(originalText);
1344
- option.textContent = translation || optionEl.originalAttributes["textContent"];
1401
+ option.textContent = this.applyParams(translation || optionEl.originalAttributes["textContent"]);
1345
1402
  }
1346
1403
  });
1347
1404
  }
@@ -1355,8 +1412,8 @@ var Translate = class {
1355
1412
  const originalValue = element.originalAttributes[attr].trim();
1356
1413
  if (!originalValue) return;
1357
1414
  const translation = this.getTranslation(originalValue);
1358
- if (translation) element.setAttribute(attr, translation);
1359
- else element.setAttribute(attr, element.originalAttributes[attr]);
1415
+ if (translation) element.setAttribute(attr, this.applyParams(translation));
1416
+ else element.setAttribute(attr, this.applyParams(element.originalAttributes[attr]));
1360
1417
  }
1361
1418
  };
1362
1419
 
@@ -1495,6 +1552,7 @@ exports.PHRASE_MARKER_ATTR = PHRASE_MARKER_ATTR;
1495
1552
  exports.Phrase = Phrase;
1496
1553
  exports.Translate = Translate;
1497
1554
  exports.Translations = Translations;
1555
+ exports.canonicalizeLocale = canonicalizeLocale;
1498
1556
  exports.createSignal = createSignal;
1499
1557
  exports.currentlyLoadedLocale = currentlyLoadedLocale;
1500
1558
  exports.encodeRichText = encodeRichText;