langsys-js-typescript 0.2.3 → 0.3.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 +36 -4
- package/dist/index.d.ts +36 -4
- package/dist/index.js +80 -41
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +80 -42
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
## 0.3.0 - 2026-07-03
|
|
2
|
+
|
|
3
|
+
Full CLDR compliance, matching the backend's CLDR migration. Everything rides on the platform's native `Intl` APIs — no new dependencies, no bundled locale data.
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- **Locale identifiers are now BCP 47 canonical everywhere.** `en-us` → `en-US`, `zh-hant-tw` → `zh-Hant-TW`, `en_US` → `en-US`, legacy aliases resolved (`iw` → `he`). Canonicalization happens at every input boundary (init config, user-locale store emissions, `detectPreferredLocale`, API params), so the canonical form is what goes on the wire and what keys every internal cache. Lowercase locales in existing configs keep working — they're canonicalized on the way in.
|
|
8
|
+
- **`detectPreferredLocale` matching is script-aware** via CLDR likely-subtags (`Intl.Locale.maximize()`). `es-MX` matches a supported `es-ES` or `es`; `zh-TW` matches `zh-Hant` and never `zh-Hans` — cross-script fallbacks are refused instead of guessed. Previously matching truncated to the bare language code, which could hand a Traditional Chinese reader a Simplified catalog.
|
|
9
|
+
- **Simple `{name}` interpolation formats numbers and Dates per the target locale's CLDR rules.** Numbers go through `Intl.NumberFormat` (`1234.5` → `1.234,5` in `de-DE`); Dates through `Intl.DateTimeFormat` medium date style (previously ISO 8601). Pass values as strings to opt out — e.g. IDs that must not get grouping separators.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **Style-less ICU arguments (`{n, number}`, `{d, date}`, `{t, time}`) now format correctly.** The ICU detector required a style argument, so these valid ICU forms fell through to simple interpolation and rendered as literal `{n, number}` text.
|
|
14
|
+
- Locale equality checks (fetch dedup, country/currency/dial-code caches) no longer miss when the same locale arrives with different casing.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- `canonicalizeLocale(locale)` — exported BCP 47 canonicalizer, useful when seeding your own `UserLocaleStore`.
|
|
19
|
+
|
|
1
20
|
## 0.2.1 - 2026-06-11
|
|
2
21
|
|
|
3
22
|
### Changed
|
package/README.md
CHANGED
|
@@ -29,14 +29,14 @@ import { LangsysApp, createSignal, t } from 'langsys-js-typescript';
|
|
|
29
29
|
|
|
30
30
|
// 1. A reactive holder for the user's current locale. Use `createSignal` if you
|
|
31
31
|
// have nothing of your own — any object with subscribe/set/update/get works.
|
|
32
|
-
const userLocale = createSignal('en-
|
|
32
|
+
const userLocale = createSignal('en-US');
|
|
33
33
|
|
|
34
34
|
// 2. Initialize
|
|
35
35
|
const response = await LangsysApp.init({
|
|
36
36
|
projectid: process.env.LANGSYS_PROJECT_ID!,
|
|
37
37
|
key: process.env.LANGSYS_API_KEY!,
|
|
38
38
|
UserLocaleStore: userLocale,
|
|
39
|
-
baseLocale: 'en-
|
|
39
|
+
baseLocale: 'en-US',
|
|
40
40
|
debug: false,
|
|
41
41
|
});
|
|
42
42
|
|
|
@@ -51,7 +51,7 @@ document.querySelector('#greeting')!.textContent =
|
|
|
51
51
|
t('Hello, {name}!', 'Greetings', { name: 'Sarah' });
|
|
52
52
|
|
|
53
53
|
// 5. Change locale — all subscribed consumers re-translate
|
|
54
|
-
userLocale.set('es-
|
|
54
|
+
userLocale.set('es-ES');
|
|
55
55
|
```
|
|
56
56
|
|
|
57
57
|
### How `t()` works
|
|
@@ -68,9 +68,9 @@ t('Hello, {name}!', 'Greetings', {}); // ❌ Property '
|
|
|
68
68
|
t('Hello, {name}!', 'Greetings', { name: 'x', extra: 'y' }); // ❌ Unknown property 'extra'
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
Allowed value types: `string | number | Date | boolean`. Dates
|
|
71
|
+
Allowed value types: `string | number | Date | boolean`. Numbers and Dates format per the active locale's CLDR rules (`1234.5` → `1.234,5` in `de-DE`; Dates use the medium date style). Pass values as strings to opt out — e.g. IDs and codes that must not get grouping separators.
|
|
72
72
|
|
|
73
|
-
>
|
|
73
|
+
> Translations containing ICU MessageFormat syntax — `{count, plural, one {# item} other {# items}}`, `{gender, select, …}`, `{n, number}`, `{d, date}` — are rendered with full CLDR plural rules for the active locale (Arabic's six categories, Russian's four, etc.). Plain `{name}` slots keep working unchanged.
|
|
74
74
|
|
|
75
75
|
#### Categorization disambiguates context
|
|
76
76
|
|
|
@@ -167,9 +167,9 @@ await LangsysApp.init({
|
|
|
167
167
|
projectid: env.LANGSYS_PROJECT_ID,
|
|
168
168
|
key: env.LANGSYS_API_KEY,
|
|
169
169
|
UserLocaleStore: userLocale,
|
|
170
|
-
baseLocale: 'en-
|
|
170
|
+
baseLocale: 'en-US',
|
|
171
171
|
initialTranslations, // fetched on the server
|
|
172
|
-
initialTranslationsLocale: 'es-
|
|
172
|
+
initialTranslationsLocale: 'es-ES',
|
|
173
173
|
ssrTokenStrategy: 'client', // 'client' | 'server' | 'auto'
|
|
174
174
|
});
|
|
175
175
|
```
|
|
@@ -187,7 +187,7 @@ const locale = LangsysApp.detectPreferredLocale();
|
|
|
187
187
|
// SSR — parse Accept-Language header
|
|
188
188
|
const locale = LangsysApp.detectPreferredLocale(req.headers['accept-language']);
|
|
189
189
|
|
|
190
|
-
// Match against your app's supported locales
|
|
190
|
+
// Match against your app's supported locales (script-aware CLDR matching)
|
|
191
191
|
const supported = (await LangsysApp.getLocalesFlat()).map((l) => l.code);
|
|
192
192
|
const locale = LangsysApp.detectPreferredLocale(
|
|
193
193
|
req.headers['accept-language'],
|
|
@@ -195,7 +195,7 @@ const locale = LangsysApp.detectPreferredLocale(
|
|
|
195
195
|
);
|
|
196
196
|
```
|
|
197
197
|
|
|
198
|
-
The matcher tries exact match first (`en-US`), then language-
|
|
198
|
+
The matcher tries an exact match first (`en-US`), then falls back to same-language-and-script matching using CLDR likely-subtags data (`es-MX` matches `es-ES`; `zh-TW` matches `zh-Hant` but never `zh-Hans` — cross-script fallbacks are deliberately refused). All results come back in BCP 47 canonical form (`en-US`, `zh-Hant-TW`). When you pass `supported` and none match, it falls back to the user's top preference (canonicalized); it returns `false` only when no preference can be determined at all.
|
|
199
199
|
|
|
200
200
|
## Localized country / currency / locale lists
|
|
201
201
|
|
|
@@ -204,7 +204,7 @@ const countries = await LangsysApp.getCountries(); // [{ code, label }, ..
|
|
|
204
204
|
const dialCodes = await LangsysApp.getDialCodes(); // [{ country_code, dial_code, name }, ...]
|
|
205
205
|
const currencies = await LangsysApp.getCurrencies(); // [{ code, name, symbol, ... }, ...]
|
|
206
206
|
const locales = await LangsysApp.getLocales(); // { LanguageName: [{ code, name }] }
|
|
207
|
-
const localeName = await LangsysApp.getLocaleNameWithLookup('es-
|
|
207
|
+
const localeName = await LangsysApp.getLocaleNameWithLookup('es-ES', true, 'fr-FR'); // 'espagnol'
|
|
208
208
|
```
|
|
209
209
|
|
|
210
210
|
## The `Signal<T>` primitive
|
package/dist/index.d.mts
CHANGED
|
@@ -358,8 +358,12 @@ declare class LangsysAppClass {
|
|
|
358
358
|
private getUserLanguagePreferences;
|
|
359
359
|
private parseAcceptLanguageHeader;
|
|
360
360
|
private findBestLocaleMatch;
|
|
361
|
-
|
|
362
|
-
|
|
361
|
+
/**
|
|
362
|
+
* Resolve the effective locale for a data-fetching helper: explicit
|
|
363
|
+
* argument, else the user's current locale, else the base locale —
|
|
364
|
+
* always in BCP 47 canonical form so cache keys and API routes agree.
|
|
365
|
+
*/
|
|
366
|
+
private resolveLocale;
|
|
363
367
|
}
|
|
364
368
|
declare const LangsysApp: LangsysAppClass;
|
|
365
369
|
|
|
@@ -648,6 +652,8 @@ declare function md5(inputString: string): string;
|
|
|
648
652
|
*
|
|
649
653
|
* - Matches `{var, plural, …}`, `{var, select, …}`, `{var, selectordinal, …}`,
|
|
650
654
|
* `{var, number, …}`, `{var, date, …}`, `{var, time, …}`.
|
|
655
|
+
* - Also matches the style-less forms `{var, number}`, `{var, date}`,
|
|
656
|
+
* `{var, time}` — valid ICU with the locale-default style.
|
|
651
657
|
* - Doesn't match plain interpolation slots like `{name}` or `{count}`.
|
|
652
658
|
*
|
|
653
659
|
* Exported because the Translation Manager UI needs the same detection logic
|
|
@@ -664,7 +670,11 @@ declare function isICU(template: string): boolean;
|
|
|
664
670
|
* etc.) and gender-select branches.
|
|
665
671
|
* - Simple `{name}` interpolation: cheap regex replacement. Unknown
|
|
666
672
|
* placeholders are left untouched so missing data is visible to the
|
|
667
|
-
* developer rather than silently rendering empty strings.
|
|
673
|
+
* developer rather than silently rendering empty strings. Number and
|
|
674
|
+
* Date values are formatted per the target locale's CLDR rules
|
|
675
|
+
* (`Intl.NumberFormat` / `Intl.DateTimeFormat`) — pass numbers as
|
|
676
|
+
* strings to opt out (e.g. IDs and codes that must not get grouping
|
|
677
|
+
* separators).
|
|
668
678
|
*
|
|
669
679
|
* Malformed ICU falls through to simple interpolation rather than throwing,
|
|
670
680
|
* so the developer sees a broken-but-visible string instead of a runtime crash.
|
|
@@ -676,6 +686,28 @@ declare function isICU(template: string): boolean;
|
|
|
676
686
|
*/
|
|
677
687
|
declare function interpolate(template: string, params: Record<string, unknown>, locale?: string): string;
|
|
678
688
|
|
|
689
|
+
/**
|
|
690
|
+
* Locale-identifier handling, CLDR/BCP 47 flavored. Everything here rides on
|
|
691
|
+
* the platform's native `Intl` — no bundled locale data.
|
|
692
|
+
*
|
|
693
|
+
* The SDK's convention: canonicalize once at every input boundary (init
|
|
694
|
+
* config, user-locale store emissions, detectPreferredLocale, API routes) and
|
|
695
|
+
* treat the canonical string as the identity everywhere downstream — cache
|
|
696
|
+
* keys, equality checks, and the wire format all use it.
|
|
697
|
+
*/
|
|
698
|
+
/**
|
|
699
|
+
* Canonicalize a locale identifier to BCP 47 canonical form (the form CLDR
|
|
700
|
+
* data is keyed by): language lowercase, script Titlecase, region uppercase —
|
|
701
|
+
* `en-us` → `en-US`, `zh-hant-tw` → `zh-Hant-TW`. Underscores are accepted
|
|
702
|
+
* and converted (`en_US` → `en-US`). Legacy aliases are resolved (`iw` → `he`,
|
|
703
|
+
* `in` → `id`).
|
|
704
|
+
*
|
|
705
|
+
* Structurally invalid tags fall back to best-effort manual casing rather
|
|
706
|
+
* than throwing, so a bad config value degrades to a failed catalog lookup
|
|
707
|
+
* instead of a crash.
|
|
708
|
+
*/
|
|
709
|
+
declare function canonicalizeLocale(locale: string): string;
|
|
710
|
+
|
|
679
711
|
interface iProject {
|
|
680
712
|
id: string;
|
|
681
713
|
owner_id: string;
|
|
@@ -696,4 +728,4 @@ interface iProject {
|
|
|
696
728
|
declare const t: TFunction;
|
|
697
729
|
declare const tSignal: Signal<TFunction>;
|
|
698
730
|
|
|
699
|
-
export { type EncodedRichText, type ExtractParamKeys, LangsysApp, LangsysAppAPI, type LocaleSource, Logger, PHRASE_MARKER_ATTR, type ParamPrimitive, type ParamsFor, Phrase, type PhraseOptions, type RichSlot, type Signal, type Subscriber, type TArgs, type TFunction, Translate, type TranslateOptions, type TranslationParams, Translations, type Unsubscriber, type Updater, createSignal, currentlyLoadedLocale, encodeRichText, generateCustomId, getValue, type iCategories, type iContentBlock, type iCountry, type iCountryDialCode, type iCountryList, type iCurrency, type iCurrencyList, type iLangsysConfig, type iLangsysInitConfig, type ResponseObject as iLangsysResponse, type iLanguageName, type iLocaleData, type iLocaleDefault, type iLocaleFlat, type iProject, type iTranslations, interpolate, isContentBlockKnown, isEmpty, isICU, logger, markupTokenValues, md5, persist, reconstitute, registerContentBlock, sTranslations, stripSentinels, t, tSignal, tokenizeElement };
|
|
731
|
+
export { type EncodedRichText, type ExtractParamKeys, LangsysApp, LangsysAppAPI, type LocaleSource, Logger, PHRASE_MARKER_ATTR, type ParamPrimitive, type ParamsFor, Phrase, type PhraseOptions, type RichSlot, type Signal, type Subscriber, type TArgs, type TFunction, Translate, type TranslateOptions, type TranslationParams, Translations, type Unsubscriber, type Updater, canonicalizeLocale, createSignal, currentlyLoadedLocale, encodeRichText, generateCustomId, getValue, type iCategories, type iContentBlock, type iCountry, type iCountryDialCode, type iCountryList, type iCurrency, type iCurrencyList, type iLangsysConfig, type iLangsysInitConfig, type ResponseObject as iLangsysResponse, type iLanguageName, type iLocaleData, type iLocaleDefault, type iLocaleFlat, type iProject, type iTranslations, interpolate, isContentBlockKnown, isEmpty, isICU, logger, markupTokenValues, md5, persist, reconstitute, registerContentBlock, sTranslations, stripSentinels, t, tSignal, tokenizeElement };
|
package/dist/index.d.ts
CHANGED
|
@@ -358,8 +358,12 @@ declare class LangsysAppClass {
|
|
|
358
358
|
private getUserLanguagePreferences;
|
|
359
359
|
private parseAcceptLanguageHeader;
|
|
360
360
|
private findBestLocaleMatch;
|
|
361
|
-
|
|
362
|
-
|
|
361
|
+
/**
|
|
362
|
+
* Resolve the effective locale for a data-fetching helper: explicit
|
|
363
|
+
* argument, else the user's current locale, else the base locale —
|
|
364
|
+
* always in BCP 47 canonical form so cache keys and API routes agree.
|
|
365
|
+
*/
|
|
366
|
+
private resolveLocale;
|
|
363
367
|
}
|
|
364
368
|
declare const LangsysApp: LangsysAppClass;
|
|
365
369
|
|
|
@@ -648,6 +652,8 @@ declare function md5(inputString: string): string;
|
|
|
648
652
|
*
|
|
649
653
|
* - Matches `{var, plural, …}`, `{var, select, …}`, `{var, selectordinal, …}`,
|
|
650
654
|
* `{var, number, …}`, `{var, date, …}`, `{var, time, …}`.
|
|
655
|
+
* - Also matches the style-less forms `{var, number}`, `{var, date}`,
|
|
656
|
+
* `{var, time}` — valid ICU with the locale-default style.
|
|
651
657
|
* - Doesn't match plain interpolation slots like `{name}` or `{count}`.
|
|
652
658
|
*
|
|
653
659
|
* Exported because the Translation Manager UI needs the same detection logic
|
|
@@ -664,7 +670,11 @@ declare function isICU(template: string): boolean;
|
|
|
664
670
|
* etc.) and gender-select branches.
|
|
665
671
|
* - Simple `{name}` interpolation: cheap regex replacement. Unknown
|
|
666
672
|
* placeholders are left untouched so missing data is visible to the
|
|
667
|
-
* developer rather than silently rendering empty strings.
|
|
673
|
+
* developer rather than silently rendering empty strings. Number and
|
|
674
|
+
* Date values are formatted per the target locale's CLDR rules
|
|
675
|
+
* (`Intl.NumberFormat` / `Intl.DateTimeFormat`) — pass numbers as
|
|
676
|
+
* strings to opt out (e.g. IDs and codes that must not get grouping
|
|
677
|
+
* separators).
|
|
668
678
|
*
|
|
669
679
|
* Malformed ICU falls through to simple interpolation rather than throwing,
|
|
670
680
|
* so the developer sees a broken-but-visible string instead of a runtime crash.
|
|
@@ -676,6 +686,28 @@ declare function isICU(template: string): boolean;
|
|
|
676
686
|
*/
|
|
677
687
|
declare function interpolate(template: string, params: Record<string, unknown>, locale?: string): string;
|
|
678
688
|
|
|
689
|
+
/**
|
|
690
|
+
* Locale-identifier handling, CLDR/BCP 47 flavored. Everything here rides on
|
|
691
|
+
* the platform's native `Intl` — no bundled locale data.
|
|
692
|
+
*
|
|
693
|
+
* The SDK's convention: canonicalize once at every input boundary (init
|
|
694
|
+
* config, user-locale store emissions, detectPreferredLocale, API routes) and
|
|
695
|
+
* treat the canonical string as the identity everywhere downstream — cache
|
|
696
|
+
* keys, equality checks, and the wire format all use it.
|
|
697
|
+
*/
|
|
698
|
+
/**
|
|
699
|
+
* Canonicalize a locale identifier to BCP 47 canonical form (the form CLDR
|
|
700
|
+
* data is keyed by): language lowercase, script Titlecase, region uppercase —
|
|
701
|
+
* `en-us` → `en-US`, `zh-hant-tw` → `zh-Hant-TW`. Underscores are accepted
|
|
702
|
+
* and converted (`en_US` → `en-US`). Legacy aliases are resolved (`iw` → `he`,
|
|
703
|
+
* `in` → `id`).
|
|
704
|
+
*
|
|
705
|
+
* Structurally invalid tags fall back to best-effort manual casing rather
|
|
706
|
+
* than throwing, so a bad config value degrades to a failed catalog lookup
|
|
707
|
+
* instead of a crash.
|
|
708
|
+
*/
|
|
709
|
+
declare function canonicalizeLocale(locale: string): string;
|
|
710
|
+
|
|
679
711
|
interface iProject {
|
|
680
712
|
id: string;
|
|
681
713
|
owner_id: string;
|
|
@@ -696,4 +728,4 @@ interface iProject {
|
|
|
696
728
|
declare const t: TFunction;
|
|
697
729
|
declare const tSignal: Signal<TFunction>;
|
|
698
730
|
|
|
699
|
-
export { type EncodedRichText, type ExtractParamKeys, LangsysApp, LangsysAppAPI, type LocaleSource, Logger, PHRASE_MARKER_ATTR, type ParamPrimitive, type ParamsFor, Phrase, type PhraseOptions, type RichSlot, type Signal, type Subscriber, type TArgs, type TFunction, Translate, type TranslateOptions, type TranslationParams, Translations, type Unsubscriber, type Updater, createSignal, currentlyLoadedLocale, encodeRichText, generateCustomId, getValue, type iCategories, type iContentBlock, type iCountry, type iCountryDialCode, type iCountryList, type iCurrency, type iCurrencyList, type iLangsysConfig, type iLangsysInitConfig, type ResponseObject as iLangsysResponse, type iLanguageName, type iLocaleData, type iLocaleDefault, type iLocaleFlat, type iProject, type iTranslations, interpolate, isContentBlockKnown, isEmpty, isICU, logger, markupTokenValues, md5, persist, reconstitute, registerContentBlock, sTranslations, stripSentinels, t, tSignal, tokenizeElement };
|
|
731
|
+
export { type EncodedRichText, type ExtractParamKeys, LangsysApp, LangsysAppAPI, type LocaleSource, Logger, PHRASE_MARKER_ATTR, type ParamPrimitive, type ParamsFor, Phrase, type PhraseOptions, type RichSlot, type Signal, type Subscriber, type TArgs, type TFunction, Translate, type TranslateOptions, type TranslationParams, Translations, type Unsubscriber, type Updater, canonicalizeLocale, createSignal, currentlyLoadedLocale, encodeRichText, generateCustomId, getValue, type iCategories, type iContentBlock, type iCountry, type iCountryDialCode, type iCountryList, type iCurrency, type iCurrencyList, type iLangsysConfig, type iLangsysInitConfig, type ResponseObject as iLangsysResponse, type iLanguageName, type iLocaleData, type iLocaleDefault, type iLocaleFlat, type iProject, type iTranslations, interpolate, isContentBlockKnown, isEmpty, isICU, logger, markupTokenValues, md5, persist, reconstitute, registerContentBlock, sTranslations, stripSentinels, t, tSignal, tokenizeElement };
|
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();
|
|
@@ -1495,6 +1533,7 @@ exports.PHRASE_MARKER_ATTR = PHRASE_MARKER_ATTR;
|
|
|
1495
1533
|
exports.Phrase = Phrase;
|
|
1496
1534
|
exports.Translate = Translate;
|
|
1497
1535
|
exports.Translations = Translations;
|
|
1536
|
+
exports.canonicalizeLocale = canonicalizeLocale;
|
|
1498
1537
|
exports.createSignal = createSignal;
|
|
1499
1538
|
exports.currentlyLoadedLocale = currentlyLoadedLocale;
|
|
1500
1539
|
exports.encodeRichText = encodeRichText;
|