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/CHANGELOG.md +19 -0
- package/README.md +10 -10
- package/dist/index.d.mts +46 -4
- package/dist/index.d.ts +46 -4
- package/dist/index.js +107 -49
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +107 -50
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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
|
|
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)
|
|
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-
|
|
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
|
|
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((
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 ===
|
|
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
|
|
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
|
|
884
|
+
const supported = supportedLocales.map((l) => canonicalizeLocale(l));
|
|
843
885
|
for (const userLocale of userLocales) {
|
|
844
|
-
const
|
|
845
|
-
if (
|
|
886
|
+
const user = canonicalizeLocale(userLocale);
|
|
887
|
+
if (supported.includes(user)) return user;
|
|
846
888
|
}
|
|
847
889
|
for (const userLocale of userLocales) {
|
|
848
|
-
const
|
|
849
|
-
|
|
850
|
-
|
|
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
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
return
|
|
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
|
-
|
|
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;
|