langsys-js-typescript 0.2.2 → 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 +82 -41
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +82 -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,8 +489,10 @@ 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;
|
|
495
|
+
this.readyResolve();
|
|
456
496
|
}
|
|
457
497
|
/**
|
|
458
498
|
* React to a user-locale change. Fetches translations if we haven't
|
|
@@ -461,10 +501,12 @@ var Translations = class {
|
|
|
461
501
|
async change(locale, force = false, skipFetch = false) {
|
|
462
502
|
if (!locale) return false;
|
|
463
503
|
if (!this.config.projectid || !this.config.key) return false;
|
|
504
|
+
locale = canonicalizeLocale(locale);
|
|
464
505
|
if (skipFetch) {
|
|
465
506
|
this.debug.log("Using pre-fetched translations for locale", locale);
|
|
466
507
|
this.locale = locale;
|
|
467
508
|
this.lastLoaded[locale] = (/* @__PURE__ */ new Date()).getTime() / 1e3;
|
|
509
|
+
this.readyResolve();
|
|
468
510
|
return true;
|
|
469
511
|
}
|
|
470
512
|
const now = (/* @__PURE__ */ new Date()).getTime() / 1e3;
|
|
@@ -628,11 +670,11 @@ var LangsysAppClass = class {
|
|
|
628
670
|
}
|
|
629
671
|
if (!UserLocaleStore?.subscribe || typeof UserLocaleStore.get !== "function") {
|
|
630
672
|
this.debug.error(
|
|
631
|
-
"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."
|
|
632
674
|
);
|
|
633
675
|
return { status: false, errors: ["Missing UserLocaleStore"] };
|
|
634
676
|
}
|
|
635
|
-
baseLocale = baseLocale
|
|
677
|
+
baseLocale = canonicalizeLocale(baseLocale);
|
|
636
678
|
if (!emulateFailureToLoad) {
|
|
637
679
|
this.config = {
|
|
638
680
|
projectid,
|
|
@@ -671,8 +713,9 @@ var LangsysAppClass = class {
|
|
|
671
713
|
if (initialTranslations2 && initialTranslationsLocale) {
|
|
672
714
|
this.Translations.markLoaded(initialTranslationsLocale);
|
|
673
715
|
}
|
|
674
|
-
this.config.sUserLocale.subscribe((
|
|
716
|
+
this.config.sUserLocale.subscribe((rawLocale) => {
|
|
675
717
|
if (!validateResponse.status) return;
|
|
718
|
+
const locale = canonicalizeLocale(rawLocale);
|
|
676
719
|
this.getLocalesData(locale);
|
|
677
720
|
this.debug.log("SUBSCRIBING TO USER LOCALE STORE");
|
|
678
721
|
this.translationsLoadingPromise = this.Translations.change(locale, false);
|
|
@@ -680,7 +723,7 @@ var LangsysAppClass = class {
|
|
|
680
723
|
return validateResponse;
|
|
681
724
|
}
|
|
682
725
|
async getDialCodes(inLocale) {
|
|
683
|
-
const locale =
|
|
726
|
+
const locale = this.resolveLocale(inLocale);
|
|
684
727
|
if (this.dialCodes.length && this.dialCodesLocale === locale) return this.dialCodes;
|
|
685
728
|
const route = `countries/dial-codes/${locale}`;
|
|
686
729
|
const response = await LangsysAppAPI.get(route);
|
|
@@ -693,7 +736,7 @@ var LangsysAppClass = class {
|
|
|
693
736
|
return this.dialCodes;
|
|
694
737
|
}
|
|
695
738
|
async getCurrencies(inLocale) {
|
|
696
|
-
const locale =
|
|
739
|
+
const locale = this.resolveLocale(inLocale);
|
|
697
740
|
if (this.currencies.length && this.currenciesLocale === locale) return this.currencies;
|
|
698
741
|
const route = `currencies/${locale}`;
|
|
699
742
|
const response = await LangsysAppAPI.get(route);
|
|
@@ -706,7 +749,7 @@ var LangsysAppClass = class {
|
|
|
706
749
|
return this.currencies;
|
|
707
750
|
}
|
|
708
751
|
async getCountries(inLocale) {
|
|
709
|
-
const locale =
|
|
752
|
+
const locale = this.resolveLocale(inLocale);
|
|
710
753
|
if (this.countries.length && this.countriesLocale === locale) return this.countries;
|
|
711
754
|
const route = `countries/${locale}`;
|
|
712
755
|
const response = await LangsysAppAPI.get(route);
|
|
@@ -720,7 +763,7 @@ var LangsysAppClass = class {
|
|
|
720
763
|
}
|
|
721
764
|
async getCountryName(forCountryCode, inLocale) {
|
|
722
765
|
if (!forCountryCode) return "";
|
|
723
|
-
const locale =
|
|
766
|
+
const locale = this.resolveLocale(inLocale);
|
|
724
767
|
if (!this.countries.length || this.countriesLocale !== locale) await this.getCountries(inLocale);
|
|
725
768
|
const country = this.countries.find((c) => c.code.toLowerCase() === forCountryCode.toLowerCase())?.label;
|
|
726
769
|
if (!country) this.debug.warn("getCountryName failed to match", forCountryCode);
|
|
@@ -728,14 +771,14 @@ var LangsysAppClass = class {
|
|
|
728
771
|
}
|
|
729
772
|
async getCurrencyName(forCurrencyCode, inLocale) {
|
|
730
773
|
if (!forCurrencyCode) return "";
|
|
731
|
-
const locale =
|
|
774
|
+
const locale = this.resolveLocale(inLocale);
|
|
732
775
|
if (!this.currencies.length || this.currenciesLocale !== locale) await this.getCurrencies(inLocale);
|
|
733
776
|
const currency = this.currencies.find((c) => c.code?.toLowerCase() === forCurrencyCode.toLowerCase())?.name;
|
|
734
777
|
if (!currency) this.debug.warn("getCurrencyName failed to match", forCurrencyCode);
|
|
735
778
|
return currency || forCurrencyCode;
|
|
736
779
|
}
|
|
737
780
|
async getLocalesFormat(format = "", inLocale) {
|
|
738
|
-
const locale =
|
|
781
|
+
const locale = this.resolveLocale(inLocale);
|
|
739
782
|
const route = `locales/${locale}` + (format ? `/${format}` : "");
|
|
740
783
|
const response = await LangsysAppAPI.get(route);
|
|
741
784
|
if (response.errors || !response.status) {
|
|
@@ -756,7 +799,7 @@ var LangsysAppClass = class {
|
|
|
756
799
|
return await this.getLocalesFormat("flat", inLocale);
|
|
757
800
|
}
|
|
758
801
|
async getLocalesData(inLocale, forceRefresh = false) {
|
|
759
|
-
const locale =
|
|
802
|
+
const locale = this.resolveLocale(inLocale);
|
|
760
803
|
if (this.locales?.[locale]?.length && !forceRefresh) return this.locales[locale];
|
|
761
804
|
this.locales[locale] = await this.getLocalesFormat("data", inLocale);
|
|
762
805
|
return this.locales[locale];
|
|
@@ -766,15 +809,16 @@ var LangsysAppClass = class {
|
|
|
766
809
|
}
|
|
767
810
|
async getLocaleNameWithLookup(forLocale, shortName = false, inLocale) {
|
|
768
811
|
if (!forLocale) return "";
|
|
769
|
-
const locale =
|
|
812
|
+
const locale = this.resolveLocale(inLocale);
|
|
770
813
|
await this.getLocalesData(locale);
|
|
771
814
|
return this.getLocaleName(forLocale, shortName, locale);
|
|
772
815
|
}
|
|
773
816
|
getLocaleName(forLocale, shortName = false, inLocale) {
|
|
774
|
-
const locale =
|
|
817
|
+
const locale = this.resolveLocale(inLocale);
|
|
818
|
+
const target = canonicalizeLocale(forLocale).toLowerCase();
|
|
775
819
|
let name = "";
|
|
776
820
|
this.locales[locale]?.every((loc) => {
|
|
777
|
-
if (loc.code ===
|
|
821
|
+
if (loc.code.toLowerCase() === target) {
|
|
778
822
|
name = shortName ? loc.lang_name : loc.locale_name;
|
|
779
823
|
return false;
|
|
780
824
|
}
|
|
@@ -800,7 +844,7 @@ var LangsysAppClass = class {
|
|
|
800
844
|
const bestMatch = this.findBestLocaleMatch(userLocales, supportedLocales);
|
|
801
845
|
if (bestMatch) return bestMatch;
|
|
802
846
|
}
|
|
803
|
-
return
|
|
847
|
+
return canonicalizeLocale(userLocales[0]);
|
|
804
848
|
}
|
|
805
849
|
getUserLanguagePreferences(acceptLanguageHeader) {
|
|
806
850
|
if (acceptLanguageHeader) {
|
|
@@ -837,30 +881,26 @@ var LangsysAppClass = class {
|
|
|
837
881
|
}
|
|
838
882
|
findBestLocaleMatch(userLocales, supportedLocales) {
|
|
839
883
|
if (!userLocales.length || !supportedLocales.length) return null;
|
|
840
|
-
const
|
|
884
|
+
const supported = supportedLocales.map((l) => canonicalizeLocale(l));
|
|
841
885
|
for (const userLocale of userLocales) {
|
|
842
|
-
const
|
|
843
|
-
if (
|
|
886
|
+
const user = canonicalizeLocale(userLocale);
|
|
887
|
+
if (supported.includes(user)) return user;
|
|
844
888
|
}
|
|
845
889
|
for (const userLocale of userLocales) {
|
|
846
|
-
const
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
890
|
+
const userKey = maximizedLangScript(userLocale);
|
|
891
|
+
if (!userKey) continue;
|
|
892
|
+
const match = supported.find((s) => maximizedLangScript(s) === userKey);
|
|
893
|
+
if (match) return match;
|
|
850
894
|
}
|
|
851
895
|
return null;
|
|
852
896
|
}
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
return
|
|
860
|
-
}
|
|
861
|
-
getLanguageCode(locale) {
|
|
862
|
-
if (!locale || typeof locale !== "string") return "";
|
|
863
|
-
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);
|
|
864
904
|
}
|
|
865
905
|
};
|
|
866
906
|
var LangsysApp = new LangsysAppClass();
|
|
@@ -1493,6 +1533,7 @@ exports.PHRASE_MARKER_ATTR = PHRASE_MARKER_ATTR;
|
|
|
1493
1533
|
exports.Phrase = Phrase;
|
|
1494
1534
|
exports.Translate = Translate;
|
|
1495
1535
|
exports.Translations = Translations;
|
|
1536
|
+
exports.canonicalizeLocale = canonicalizeLocale;
|
|
1496
1537
|
exports.createSignal = createSignal;
|
|
1497
1538
|
exports.currentlyLoadedLocale = currentlyLoadedLocale;
|
|
1498
1539
|
exports.encodeRichText = encodeRichText;
|