@ultimat3/i18n 1.0.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.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +116 -0
  3. package/package.json +35 -0
  4. package/src/catalog.d.ts +28 -0
  5. package/src/catalog.d.ts.map +1 -0
  6. package/src/catalog.js +81 -0
  7. package/src/catalog.js.map +1 -0
  8. package/src/catalog.ts +132 -0
  9. package/src/catalogs/en.json +212 -0
  10. package/src/context.d.ts +64 -0
  11. package/src/context.d.ts.map +1 -0
  12. package/src/context.js +132 -0
  13. package/src/context.js.map +1 -0
  14. package/src/context.ts +195 -0
  15. package/src/define-catalogs.ts +56 -0
  16. package/src/errors.d.ts +22 -0
  17. package/src/errors.d.ts.map +1 -0
  18. package/src/errors.js +52 -0
  19. package/src/errors.js.map +1 -0
  20. package/src/errors.ts +73 -0
  21. package/src/extract.d.ts +67 -0
  22. package/src/extract.d.ts.map +1 -0
  23. package/src/extract.js +106 -0
  24. package/src/extract.js.map +1 -0
  25. package/src/extract.ts +184 -0
  26. package/src/framework.d.ts +12 -0
  27. package/src/framework.d.ts.map +1 -0
  28. package/src/framework.js +16 -0
  29. package/src/framework.js.map +1 -0
  30. package/src/framework.ts +19 -0
  31. package/src/index.d.ts +10 -0
  32. package/src/index.d.ts.map +1 -0
  33. package/src/index.js +10 -0
  34. package/src/index.js.map +1 -0
  35. package/src/index.ts +101 -0
  36. package/src/interpolate.d.ts +36 -0
  37. package/src/interpolate.d.ts.map +1 -0
  38. package/src/interpolate.js +96 -0
  39. package/src/interpolate.js.map +1 -0
  40. package/src/interpolate.ts +103 -0
  41. package/src/locales.d.ts +36 -0
  42. package/src/locales.d.ts.map +1 -0
  43. package/src/locales.js +141 -0
  44. package/src/locales.js.map +1 -0
  45. package/src/locales.ts +166 -0
  46. package/src/translator.d.ts +25 -0
  47. package/src/translator.d.ts.map +1 -0
  48. package/src/translator.js +37 -0
  49. package/src/translator.js.map +1 -0
  50. package/src/translator.ts +87 -0
@@ -0,0 +1,96 @@
1
+ /**
2
+ * `{var}` substitution and CLDR plural-form selection.
3
+ * Pure string work: it never reads a catalog and never resolves a locale.
4
+ */
5
+ /** CLDR plural categories, in the order we probe catalog suffixes. */
6
+ export const PLURAL_CATEGORIES = [
7
+ 'zero',
8
+ 'one',
9
+ 'two',
10
+ 'few',
11
+ 'many',
12
+ 'other',
13
+ ];
14
+ const PLACEHOLDER = /\{\{|\}\}|\{([^{}\s]+)\}/g;
15
+ /**
16
+ * Replace `{name}` with `vars.name`. `{{` and `}}` escape a literal brace.
17
+ * An unknown variable renders loudly as `⟦name⟧` for the same reason a missing key
18
+ * does: a silently blank slot ships to production, a bracketed one does not.
19
+ */
20
+ export function interpolate(template, vars) {
21
+ if (!template.includes('{'))
22
+ return template;
23
+ return template.replace(PLACEHOLDER, (match, name) => {
24
+ if (match === '{{')
25
+ return '{';
26
+ if (match === '}}')
27
+ return '}';
28
+ if (name === undefined)
29
+ return match;
30
+ const value = vars?.[name];
31
+ if (value === undefined)
32
+ return `⟦${name}⟧`;
33
+ return String(value);
34
+ });
35
+ }
36
+ /** Variable names a template expects — used by the extractor to flag arity drift. */
37
+ export function placeholdersOf(template) {
38
+ const names = new Set();
39
+ for (const match of template.matchAll(PLACEHOLDER)) {
40
+ const name = match[1];
41
+ if (name !== undefined)
42
+ names.add(name);
43
+ }
44
+ return [...names];
45
+ }
46
+ /**
47
+ * Pick the plural category for `count` in `locale` via `Intl.PluralRules` — CLDR
48
+ * categories, never an English `n === 1` check. `pl` has one/few/many/other, `ar`
49
+ * adds zero/two: a two-branch translator is wrong in both.
50
+ */
51
+ export function pluralCategory(count, locale) {
52
+ return pluralRulesFor(locale).select(count);
53
+ }
54
+ /**
55
+ * Candidate keys for a plural lookup, most specific first.
56
+ *
57
+ * `items` + count 3 in `pl` → `items_many`, `items_plural`, `items_other`, `items`.
58
+ * `items` + count 1 in `pl` → `items_one`, `items`.
59
+ * `_plural` is the two-form authoring shortcut from the house style; the `_<category>`
60
+ * suffixes are what a 3+ form locale needs.
61
+ */
62
+ export function pluralKeyCandidates(key, count, locale) {
63
+ const category = pluralCategory(count, locale);
64
+ if (category === 'one')
65
+ return [`${key}_one`, key];
66
+ return [`${key}_${category}`, `${key}_plural`, `${key}_other`, key];
67
+ }
68
+ /** First candidate that exists, per the caller's `has` predicate; falls back to `key`. */
69
+ export function selectPluralKey(key, count, locale, has) {
70
+ for (const candidate of pluralKeyCandidates(key, count, locale)) {
71
+ if (has(candidate))
72
+ return candidate;
73
+ }
74
+ return key;
75
+ }
76
+ /** All suffixed variants of `key` a locale could legitimately define. */
77
+ export function pluralVariantsOf(key) {
78
+ return [`${key}_plural`, ...PLURAL_CATEGORIES.map((category) => `${key}_${category}`)];
79
+ }
80
+ const rulesCache = new Map();
81
+ function pluralRulesFor(locale) {
82
+ const cached = rulesCache.get(locale);
83
+ if (cached !== undefined)
84
+ return cached;
85
+ // An unknown tag would throw inside Intl; degrade to `en` rather than break a render.
86
+ let rules;
87
+ try {
88
+ rules = new Intl.PluralRules(locale);
89
+ }
90
+ catch {
91
+ rules = new Intl.PluralRules('en');
92
+ }
93
+ rulesCache.set(locale, rules);
94
+ return rules;
95
+ }
96
+ //# sourceMappingURL=interpolate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interpolate.js","sourceRoot":"","sources":["interpolate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,103 @@
1
+ /**
2
+ * `{var}` substitution and CLDR plural-form selection.
3
+ * Pure string work: it never reads a catalog and never resolves a locale.
4
+ */
5
+
6
+ export type InterpolationValue = string | number | boolean;
7
+ export type InterpolationVars = Record<string, InterpolationValue>;
8
+
9
+ /** CLDR plural categories, in the order we probe catalog suffixes. */
10
+ export const PLURAL_CATEGORIES: readonly Intl.LDMLPluralRule[] = [
11
+ 'zero',
12
+ 'one',
13
+ 'two',
14
+ 'few',
15
+ 'many',
16
+ 'other',
17
+ ];
18
+
19
+ const PLACEHOLDER = /\{\{|\}\}|\{([^{}\s]+)\}/g;
20
+
21
+ /**
22
+ * Replace `{name}` with `vars.name`. `{{` and `}}` escape a literal brace.
23
+ * An unknown variable renders loudly as `⟦name⟧` for the same reason a missing key
24
+ * does: a silently blank slot ships to production, a bracketed one does not.
25
+ */
26
+ export function interpolate(template: string, vars?: InterpolationVars): string {
27
+ if (!template.includes('{')) return template;
28
+ return template.replace(PLACEHOLDER, (match, name: string | undefined) => {
29
+ if (match === '{{') return '{';
30
+ if (match === '}}') return '}';
31
+ if (name === undefined) return match;
32
+ const value = vars?.[name];
33
+ if (value === undefined) return `⟦${name}⟧`;
34
+ return String(value);
35
+ });
36
+ }
37
+
38
+ /** Variable names a template expects — used by the extractor to flag arity drift. */
39
+ export function placeholdersOf(template: string): string[] {
40
+ const names = new Set<string>();
41
+ for (const match of template.matchAll(PLACEHOLDER)) {
42
+ const name = match[1];
43
+ if (name !== undefined) names.add(name);
44
+ }
45
+ return [...names];
46
+ }
47
+
48
+ /**
49
+ * Pick the plural category for `count` in `locale` via `Intl.PluralRules` — CLDR
50
+ * categories, never an English `n === 1` check. `pl` has one/few/many/other, `ar`
51
+ * adds zero/two: a two-branch translator is wrong in both.
52
+ */
53
+ export function pluralCategory(count: number, locale: string): Intl.LDMLPluralRule {
54
+ return pluralRulesFor(locale).select(count);
55
+ }
56
+
57
+ /**
58
+ * Candidate keys for a plural lookup, most specific first.
59
+ *
60
+ * `items` + count 3 in `pl` → `items_many`, `items_plural`, `items_other`, `items`.
61
+ * `items` + count 1 in `pl` → `items_one`, `items`.
62
+ * `_plural` is the two-form authoring shortcut from the house style; the `_<category>`
63
+ * suffixes are what a 3+ form locale needs.
64
+ */
65
+ export function pluralKeyCandidates(key: string, count: number, locale: string): string[] {
66
+ const category = pluralCategory(count, locale);
67
+ if (category === 'one') return [`${key}_one`, key];
68
+ return [`${key}_${category}`, `${key}_plural`, `${key}_other`, key];
69
+ }
70
+
71
+ /** First candidate that exists, per the caller's `has` predicate; falls back to `key`. */
72
+ export function selectPluralKey(
73
+ key: string,
74
+ count: number,
75
+ locale: string,
76
+ has: (candidate: string) => boolean,
77
+ ): string {
78
+ for (const candidate of pluralKeyCandidates(key, count, locale)) {
79
+ if (has(candidate)) return candidate;
80
+ }
81
+ return key;
82
+ }
83
+
84
+ /** All suffixed variants of `key` a locale could legitimately define. */
85
+ export function pluralVariantsOf(key: string): string[] {
86
+ return [`${key}_plural`, ...PLURAL_CATEGORIES.map((category) => `${key}_${category}`)];
87
+ }
88
+
89
+ const rulesCache = new Map<string, Intl.PluralRules>();
90
+
91
+ function pluralRulesFor(locale: string): Intl.PluralRules {
92
+ const cached = rulesCache.get(locale);
93
+ if (cached !== undefined) return cached;
94
+ // An unknown tag would throw inside Intl; degrade to `en` rather than break a render.
95
+ let rules: Intl.PluralRules;
96
+ try {
97
+ rules = new Intl.PluralRules(locale);
98
+ } catch {
99
+ rules = new Intl.PluralRules('en');
100
+ }
101
+ rulesCache.set(locale, rules);
102
+ return rules;
103
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Locale resolution only: the supported set, the default, tag normalization and
3
+ * HTTP `Accept-Language` negotiation. Knows nothing about catalogs or interpolation.
4
+ */
5
+ /** A normalized BCP-47 tag — primary subtag, lowercase (`pt`), or `lang-script` (`zh-hant`). */
6
+ export type Locale = string;
7
+ export declare const DEFAULT_LOCALE: Locale;
8
+ /** Locales the framework's own catalogs may ship for. Apps narrow or extend this set. */
9
+ export declare const SUPPORTED_LOCALES: readonly Locale[];
10
+ export type Direction = 'ltr' | 'rtl';
11
+ /**
12
+ * Strip region, lowercase, fall back to the default.
13
+ * `pt-BR` → `pt`; `ZH-Hant-TW` → `zh-hant` when that tag is supported, else `zh`.
14
+ */
15
+ export declare function normalizeLocale(tag?: string | null, supported?: readonly Locale[], fallback?: Locale): Locale;
16
+ /** Same as `normalizeLocale` but loud: use where an unknown locale is a caller bug. */
17
+ export declare function assertSupportedLocale(tag: string, supported?: readonly Locale[]): Locale;
18
+ export declare function isSupportedLocale(tag: string, supported?: readonly Locale[]): boolean;
19
+ export interface LanguageRange {
20
+ tag: string;
21
+ quality: number;
22
+ }
23
+ /**
24
+ * Parse `Accept-Language: en-GB,en;q=0.9,fr-CH;q=0.8,*;q=0.5` into ranges sorted by
25
+ * quality descending, ties resolved by header order (stable sort).
26
+ */
27
+ export declare function parseAcceptLanguage(header?: string | null): LanguageRange[];
28
+ /**
29
+ * Pick the best supported locale for an `Accept-Language` header.
30
+ * `*` matches the fallback rather than the first supported locale — a wildcard is
31
+ * "no preference", not "any language you have".
32
+ */
33
+ export declare function negotiateLocale(acceptLanguage?: string | null, supported?: readonly Locale[], fallback?: Locale): Locale;
34
+ export declare function isRtl(locale: Locale): boolean;
35
+ export declare function directionOf(locale: Locale): Direction;
36
+ //# sourceMappingURL=locales.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["locales.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,gGAAgG;AAChG,MAAM,MAAM,MAAM,GAAG,MAAM,CAAC;AAE5B,eAAO,MAAM,cAAc,EAAE,MAAa,CAAC;AAE3C,yFAAyF;AACzF,eAAO,MAAM,iBAAiB,EAAE,SAAS,MAAM,EA+B9C,CAAC;AAuBF,MAAM,MAAM,SAAS,GAAG,KAAK,GAAG,KAAK,CAAC;AAEtC;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,EACnB,SAAS,GAAE,SAAS,MAAM,EAAsB,EAChD,QAAQ,GAAE,MAAuB,GAChC,MAAM,CAeR;AAED,uFAAuF;AACvF,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,MAAM,EACX,SAAS,GAAE,SAAS,MAAM,EAAsB,GAC/C,MAAM,CAIR;AAED,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,MAAM,EACX,SAAS,GAAE,SAAS,MAAM,EAAsB,GAC/C,OAAO,CAET;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,aAAa,EAAE,CAkB3E;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,EAC9B,SAAS,GAAE,SAAS,MAAM,EAAsB,EAChD,QAAQ,GAAE,MAAuB,GAChC,MAAM,CAOR;AAED,wBAAgB,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAG7C;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAErD"}
package/src/locales.js ADDED
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Locale resolution only: the supported set, the default, tag normalization and
3
+ * HTTP `Accept-Language` negotiation. Knows nothing about catalogs or interpolation.
4
+ */
5
+ import { localeUnsupported } from './errors';
6
+ export const DEFAULT_LOCALE = 'en';
7
+ /** Locales the framework's own catalogs may ship for. Apps narrow or extend this set. */
8
+ export const SUPPORTED_LOCALES = [
9
+ 'en',
10
+ 'es',
11
+ 'pt',
12
+ 'fr',
13
+ 'de',
14
+ 'it',
15
+ 'nl',
16
+ 'pl',
17
+ 'ru',
18
+ 'uk',
19
+ 'tr',
20
+ 'cs',
21
+ 'sv',
22
+ 'da',
23
+ 'nb',
24
+ 'fi',
25
+ 'el',
26
+ 'ro',
27
+ 'hu',
28
+ 'ar',
29
+ 'he',
30
+ 'fa',
31
+ 'ur',
32
+ 'hi',
33
+ 'id',
34
+ 'vi',
35
+ 'th',
36
+ 'ja',
37
+ 'ko',
38
+ 'zh',
39
+ ];
40
+ /**
41
+ * Right-to-left scripts, by primary subtag. A static CLDR-derived set rather than
42
+ * `Intl.Locale.prototype.getTextInfo` so direction is deterministic across runtimes.
43
+ */
44
+ const RTL_LOCALES = new Set([
45
+ 'ar',
46
+ 'arc',
47
+ 'ckb',
48
+ 'dv',
49
+ 'fa',
50
+ 'he',
51
+ 'ks',
52
+ 'ku',
53
+ 'nqo',
54
+ 'ps',
55
+ 'sd',
56
+ 'ug',
57
+ 'ur',
58
+ 'yi',
59
+ ]);
60
+ /**
61
+ * Strip region, lowercase, fall back to the default.
62
+ * `pt-BR` → `pt`; `ZH-Hant-TW` → `zh-hant` when that tag is supported, else `zh`.
63
+ */
64
+ export function normalizeLocale(tag, supported = SUPPORTED_LOCALES, fallback = DEFAULT_LOCALE) {
65
+ if (!tag)
66
+ return fallback;
67
+ const lower = tag.trim().toLowerCase().replace(/_/g, '-');
68
+ if (lower.length === 0 || lower === '*')
69
+ return fallback;
70
+ if (supported.includes(lower))
71
+ return lower;
72
+ const segments = lower.split('-');
73
+ const primary = segments[0] ?? '';
74
+ // `lang-script` is a distinct written form (zh-hant vs zh-hans) — try it before dropping to `lang`.
75
+ const script = segments[1];
76
+ if (script !== undefined && script.length === 4) {
77
+ const withScript = `${primary}-${script}`;
78
+ if (supported.includes(withScript))
79
+ return withScript;
80
+ }
81
+ return supported.includes(primary) ? primary : fallback;
82
+ }
83
+ /** Same as `normalizeLocale` but loud: use where an unknown locale is a caller bug. */
84
+ export function assertSupportedLocale(tag, supported = SUPPORTED_LOCALES) {
85
+ const normalized = normalizeLocale(tag, supported, '');
86
+ if (normalized === '')
87
+ throw localeUnsupported(tag, supported);
88
+ return normalized;
89
+ }
90
+ export function isSupportedLocale(tag, supported = SUPPORTED_LOCALES) {
91
+ return normalizeLocale(tag, supported, '') !== '';
92
+ }
93
+ /**
94
+ * Parse `Accept-Language: en-GB,en;q=0.9,fr-CH;q=0.8,*;q=0.5` into ranges sorted by
95
+ * quality descending, ties resolved by header order (stable sort).
96
+ */
97
+ export function parseAcceptLanguage(header) {
98
+ if (!header)
99
+ return [];
100
+ const ranges = [];
101
+ for (const part of header.split(',')) {
102
+ const [rawTag, ...params] = part.trim().split(';');
103
+ const tag = rawTag?.trim();
104
+ if (!tag)
105
+ continue;
106
+ let quality = 1;
107
+ for (const param of params) {
108
+ const match = /^\s*q\s*=\s*([0-9.]+)\s*$/i.exec(param);
109
+ if (match?.[1] !== undefined) {
110
+ const parsed = Number.parseFloat(match[1]);
111
+ quality = Number.isFinite(parsed) ? Math.min(Math.max(parsed, 0), 1) : 0;
112
+ }
113
+ }
114
+ if (quality > 0)
115
+ ranges.push({ tag: tag.toLowerCase(), quality });
116
+ }
117
+ return ranges.sort((a, b) => b.quality - a.quality);
118
+ }
119
+ /**
120
+ * Pick the best supported locale for an `Accept-Language` header.
121
+ * `*` matches the fallback rather than the first supported locale — a wildcard is
122
+ * "no preference", not "any language you have".
123
+ */
124
+ export function negotiateLocale(acceptLanguage, supported = SUPPORTED_LOCALES, fallback = DEFAULT_LOCALE) {
125
+ for (const { tag } of parseAcceptLanguage(acceptLanguage)) {
126
+ if (tag === '*')
127
+ return fallback;
128
+ const match = normalizeLocale(tag, supported, '');
129
+ if (match !== '')
130
+ return match;
131
+ }
132
+ return fallback;
133
+ }
134
+ export function isRtl(locale) {
135
+ const primary = locale.split('-')[0]?.toLowerCase() ?? '';
136
+ return RTL_LOCALES.has(primary);
137
+ }
138
+ export function directionOf(locale) {
139
+ return isRtl(locale) ? 'rtl' : 'ltr';
140
+ }
141
+ //# sourceMappingURL=locales.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"locales.js","sourceRoot":"","sources":["locales.ts"],"names":[],"mappings":"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"}
package/src/locales.ts ADDED
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Locale resolution only: the supported set, the default, tag normalization and
3
+ * HTTP `Accept-Language` negotiation. Knows nothing about catalogs or interpolation.
4
+ */
5
+
6
+ import { localeUnsupported } from './errors';
7
+
8
+ /** A normalized BCP-47 tag — primary subtag, lowercase (`pt`), or `lang-script` (`zh-hant`). */
9
+ export type Locale = string;
10
+
11
+ export const DEFAULT_LOCALE: Locale = 'en';
12
+
13
+ /** Locales the framework's own catalogs may ship for. Apps narrow or extend this set. */
14
+ export const SUPPORTED_LOCALES: readonly Locale[] = [
15
+ 'en',
16
+ 'es',
17
+ 'pt',
18
+ 'fr',
19
+ 'de',
20
+ 'it',
21
+ 'nl',
22
+ 'pl',
23
+ 'ru',
24
+ 'uk',
25
+ 'tr',
26
+ 'cs',
27
+ 'sv',
28
+ 'da',
29
+ 'nb',
30
+ 'fi',
31
+ 'el',
32
+ 'ro',
33
+ 'hu',
34
+ 'ar',
35
+ 'he',
36
+ 'fa',
37
+ 'ur',
38
+ 'hi',
39
+ 'id',
40
+ 'vi',
41
+ 'th',
42
+ 'ja',
43
+ 'ko',
44
+ 'zh',
45
+ ];
46
+
47
+ /**
48
+ * Right-to-left scripts, by primary subtag. A static CLDR-derived set rather than
49
+ * `Intl.Locale.prototype.getTextInfo` so direction is deterministic across runtimes.
50
+ */
51
+ const RTL_LOCALES: ReadonlySet<string> = new Set([
52
+ 'ar',
53
+ 'arc',
54
+ 'ckb',
55
+ 'dv',
56
+ 'fa',
57
+ 'he',
58
+ 'ks',
59
+ 'ku',
60
+ 'nqo',
61
+ 'ps',
62
+ 'sd',
63
+ 'ug',
64
+ 'ur',
65
+ 'yi',
66
+ ]);
67
+
68
+ export type Direction = 'ltr' | 'rtl';
69
+
70
+ /**
71
+ * Strip region, lowercase, fall back to the default.
72
+ * `pt-BR` → `pt`; `ZH-Hant-TW` → `zh-hant` when that tag is supported, else `zh`.
73
+ */
74
+ export function normalizeLocale(
75
+ tag?: string | null,
76
+ supported: readonly Locale[] = SUPPORTED_LOCALES,
77
+ fallback: Locale = DEFAULT_LOCALE,
78
+ ): Locale {
79
+ if (!tag) return fallback;
80
+ const lower = tag.trim().toLowerCase().replace(/_/g, '-');
81
+ if (lower.length === 0 || lower === '*') return fallback;
82
+ if (supported.includes(lower)) return lower;
83
+
84
+ const segments = lower.split('-');
85
+ const primary = segments[0] ?? '';
86
+ // `lang-script` is a distinct written form (zh-hant vs zh-hans) — try it before dropping to `lang`.
87
+ const script = segments[1];
88
+ if (script !== undefined && script.length === 4) {
89
+ const withScript = `${primary}-${script}`;
90
+ if (supported.includes(withScript)) return withScript;
91
+ }
92
+ return supported.includes(primary) ? primary : fallback;
93
+ }
94
+
95
+ /** Same as `normalizeLocale` but loud: use where an unknown locale is a caller bug. */
96
+ export function assertSupportedLocale(
97
+ tag: string,
98
+ supported: readonly Locale[] = SUPPORTED_LOCALES,
99
+ ): Locale {
100
+ const normalized = normalizeLocale(tag, supported, '');
101
+ if (normalized === '') throw localeUnsupported(tag, supported);
102
+ return normalized;
103
+ }
104
+
105
+ export function isSupportedLocale(
106
+ tag: string,
107
+ supported: readonly Locale[] = SUPPORTED_LOCALES,
108
+ ): boolean {
109
+ return normalizeLocale(tag, supported, '') !== '';
110
+ }
111
+
112
+ export interface LanguageRange {
113
+ tag: string;
114
+ quality: number;
115
+ }
116
+
117
+ /**
118
+ * Parse `Accept-Language: en-GB,en;q=0.9,fr-CH;q=0.8,*;q=0.5` into ranges sorted by
119
+ * quality descending, ties resolved by header order (stable sort).
120
+ */
121
+ export function parseAcceptLanguage(header?: string | null): LanguageRange[] {
122
+ if (!header) return [];
123
+ const ranges: LanguageRange[] = [];
124
+ for (const part of header.split(',')) {
125
+ const [rawTag, ...params] = part.trim().split(';');
126
+ const tag = rawTag?.trim();
127
+ if (!tag) continue;
128
+ let quality = 1;
129
+ for (const param of params) {
130
+ const match = /^\s*q\s*=\s*([0-9.]+)\s*$/i.exec(param);
131
+ if (match?.[1] !== undefined) {
132
+ const parsed = Number.parseFloat(match[1]);
133
+ quality = Number.isFinite(parsed) ? Math.min(Math.max(parsed, 0), 1) : 0;
134
+ }
135
+ }
136
+ if (quality > 0) ranges.push({ tag: tag.toLowerCase(), quality });
137
+ }
138
+ return ranges.sort((a, b) => b.quality - a.quality);
139
+ }
140
+
141
+ /**
142
+ * Pick the best supported locale for an `Accept-Language` header.
143
+ * `*` matches the fallback rather than the first supported locale — a wildcard is
144
+ * "no preference", not "any language you have".
145
+ */
146
+ export function negotiateLocale(
147
+ acceptLanguage?: string | null,
148
+ supported: readonly Locale[] = SUPPORTED_LOCALES,
149
+ fallback: Locale = DEFAULT_LOCALE,
150
+ ): Locale {
151
+ for (const { tag } of parseAcceptLanguage(acceptLanguage)) {
152
+ if (tag === '*') return fallback;
153
+ const match = normalizeLocale(tag, supported, '');
154
+ if (match !== '') return match;
155
+ }
156
+ return fallback;
157
+ }
158
+
159
+ export function isRtl(locale: Locale): boolean {
160
+ const primary = locale.split('-')[0]?.toLowerCase() ?? '';
161
+ return RTL_LOCALES.has(primary);
162
+ }
163
+
164
+ export function directionOf(locale: Locale): Direction {
165
+ return isRtl(locale) ? 'rtl' : 'ltr';
166
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Lookup + interpolation delegation, nothing else. Never throws, never falls back to
3
+ * another locale, never returns the key bare: a miss renders `⟦key⟧` so gaps are
4
+ * visible in dev and in review screenshots instead of invisible in production.
5
+ */
6
+ import type { Catalog } from './catalog';
7
+ import { type InterpolationVars } from './interpolate';
8
+ import { type Locale } from './locales';
9
+ export interface TranslateVars extends InterpolationVars {
10
+ /** Present ⇒ plural selection runs against the locale's CLDR categories. */
11
+ count?: number;
12
+ }
13
+ export interface Translator {
14
+ (key: string, vars?: TranslateVars): string;
15
+ /** Whether the key (or any of its plural variants) can be rendered. */
16
+ has(key: string): boolean;
17
+ /** The raw template, for tooling that needs the placeholders. */
18
+ raw(key: string): string | undefined;
19
+ keys(): string[];
20
+ readonly locale: Locale;
21
+ }
22
+ export declare function createTranslator(catalog: Catalog, locale?: Locale): Translator;
23
+ /** True when a rendered string is a loud miss — assert on this in tests, never on `''`. */
24
+ export declare function isMiss(rendered: string): boolean;
25
+ //# sourceMappingURL=translator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"translator.d.ts","sourceRoot":"","sources":["translator.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAE,KAAK,iBAAiB,EAAgC,MAAM,eAAe,CAAC;AACrF,OAAO,EAAkB,KAAK,MAAM,EAAE,MAAM,WAAW,CAAC;AAExD,MAAM,WAAW,aAAc,SAAQ,iBAAiB;IACtD,4EAA4E;IAC5E,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,UAAU;IACzB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,aAAa,GAAG,MAAM,CAAC;IAC5C,uEAAuE;IACvE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAC1B,iEAAiE;IACjE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IACrC,IAAI,IAAI,MAAM,EAAE,CAAC;IACjB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,GAAE,MAAuB,GAAG,UAAU,CA2B9F;AAED,2FAA2F;AAC3F,wBAAgB,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAEhD"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Lookup + interpolation delegation, nothing else. Never throws, never falls back to
3
+ * another locale, never returns the key bare: a miss renders `⟦key⟧` so gaps are
4
+ * visible in dev and in review screenshots instead of invisible in production.
5
+ */
6
+ import { interpolate, selectPluralKey } from './interpolate';
7
+ import { DEFAULT_LOCALE } from './locales';
8
+ export function createTranslator(catalog, locale = DEFAULT_LOCALE) {
9
+ const hasExact = (key) => Object.hasOwn(catalog, key);
10
+ const resolveKey = (key, vars) => {
11
+ const count = vars?.count;
12
+ if (typeof count === 'number' && Number.isFinite(count)) {
13
+ return selectPluralKey(key, count, locale, hasExact);
14
+ }
15
+ return key;
16
+ };
17
+ const translate = (key, vars) => {
18
+ const template = catalog[resolveKey(key, vars)];
19
+ if (template === undefined)
20
+ return `⟦${key}⟧`;
21
+ return vars === undefined ? template : interpolate(template, vars);
22
+ };
23
+ return Object.assign(translate, {
24
+ has: (key) => hasExact(key) ||
25
+ hasExact(`${key}_other`) ||
26
+ hasExact(`${key}_plural`) ||
27
+ hasExact(`${key}_one`),
28
+ raw: (key) => catalog[key],
29
+ keys: () => Object.keys(catalog).sort(),
30
+ locale,
31
+ });
32
+ }
33
+ /** True when a rendered string is a loud miss — assert on this in tests, never on `''`. */
34
+ export function isMiss(rendered) {
35
+ return rendered.startsWith('⟦') && rendered.endsWith('⟧');
36
+ }
37
+ //# sourceMappingURL=translator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"translator.js","sourceRoot":"","sources":["translator.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAA0B,WAAW,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrF,OAAO,EAAE,cAAc,EAAe,MAAM,WAAW,CAAC;AAiBxD,MAAM,UAAU,gBAAgB,CAAC,OAAgB,EAAE,MAAM,GAAW,cAAc;IAChF,MAAM,QAAQ,GAAG,CAAC,GAAW,EAAW,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IAEvE,MAAM,UAAU,GAAG,CAAC,GAAW,EAAE,IAAoB,EAAU,EAAE;QAC/D,MAAM,KAAK,GAAG,IAAI,EAAE,KAAK,CAAC;QAC1B,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;YACxD,OAAO,eAAe,CAAC,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC;QACvD,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC,CAAC;IAEF,MAAM,SAAS,GAAG,CAAC,GAAW,EAAE,IAAoB,EAAU,EAAE;QAC9D,MAAM,QAAQ,GAAG,OAAO,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC;QAChD,IAAI,QAAQ,KAAK,SAAS;YAAE,OAAO,IAAI,GAAG,GAAG,CAAC;QAC9C,OAAO,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IACrE,CAAC,CAAC;IAEF,OAAO,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE;QAC9B,GAAG,EAAE,CAAC,GAAW,EAAW,EAAE,CAC5B,QAAQ,CAAC,GAAG,CAAC;YACb,QAAQ,CAAC,GAAG,GAAG,QAAQ,CAAC;YACxB,QAAQ,CAAC,GAAG,GAAG,SAAS,CAAC;YACzB,QAAQ,CAAC,GAAG,GAAG,MAAM,CAAC;QACxB,GAAG,EAAE,CAAC,GAAW,EAAsB,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC;QACtD,IAAI,EAAE,GAAa,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE;QACjD,MAAM;KACP,CAAC,CAAC;AACL,CAAC;AAED,2FAA2F;AAC3F,MAAM,UAAU,MAAM,CAAC,QAAgB;IACrC,OAAO,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;AAC5D,CAAC"}