@ngx-runtime-i18n/core 2.0.0 → 2.1.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/README.md CHANGED
@@ -41,16 +41,16 @@ formatIcu('en', 'cart.items', catalog, { count: 2 }); // "2 items"
41
41
 
42
42
  ### `formatIcu(lang, key, catalog, params?, onMissingKey?)`
43
43
 
44
- - **`lang: string`** current language (for plural rules and future features).
45
- - **`key: string`** dotted path into the catalog.
46
- - **`catalog: Catalog`** a nested object of strings/objects.
47
- - **`params?: Record<string, unknown>`** interpolation values.
48
- - **`onMissingKey?: (key: string) => string`** transform for missing keys (defaults to returning the key).
44
+ - **`lang: string`**: current language (for plural rules and future features).
45
+ - **`key: string`**: dotted path into the catalog.
46
+ - **`catalog: Catalog`**: a nested object of strings/objects.
47
+ - **`params?: Record<string, unknown>`**: interpolation values.
48
+ - **`onMissingKey?: (key: string) => string`**: transform for missing keys (defaults to returning the key).
49
49
 
50
50
  ### Types
51
51
 
52
- - **`Catalog`** `Record<string, unknown>` (nested object).
53
- - **`RuntimeI18nConfig`** shape shared with the Angular wrapper for consistency.
52
+ - **`Catalog`**: `Record<string, unknown>` (nested object).
53
+ - **`RuntimeI18nConfig`**: shape shared with the Angular wrapper for consistency.
54
54
 
55
55
  ---
56
56
 
@@ -71,7 +71,7 @@ formatIcu('en', 'cart.items', catalog, { count: 2 }); // "2 items"
71
71
 
72
72
  - Not a full ICU implementation; aims to cover common 80% with a tiny footprint.
73
73
  - If you need Angular binding or SSR helpers, prefer `@ngx-runtime-i18n/angular`.
74
- - Keep your catalogs **flat-ish and predictable** to avoid fragile deep paths.
74
+ - Keep your catalogs flat-ish and predictable to avoid fragile deep paths.
75
75
 
76
76
  ---
77
77
 
@@ -1,27 +1,13 @@
1
1
  // Tokens may include dots or hyphens so nested object keys like "user.name" are practical.
2
2
  const INTERPOLATION_PATTERN = /\{([a-zA-Z_][a-zA-Z0-9_.-]*)\}/g;
3
- function formatIcu(_lang, key, cat, params = {}, onMissingKey) {
3
+ function formatIcu(_lang, key, cat, params = {}, onMissingKey, pluralResolver) {
4
4
  const raw = lookup(key, cat);
5
5
  if (raw == null)
6
6
  return onMissingKey ? onMissingKey(key) : key;
7
7
  let out = String(raw);
8
- // 1) Resolve {x, plural, ...} with a brace-balanced scanner.
9
- out = replacePluralBlocks(out, (arg, body) => {
10
- const n = Number(params[arg] ?? 0);
11
- const options = parsePluralBody(body);
12
- if (Number.isFinite(n)) {
13
- const exact = options[`=${n}`];
14
- if (exact)
15
- return exact;
16
- const one = options['one'];
17
- if (n === 1 && one)
18
- return one;
19
- const other = options['other'] ?? '';
20
- return other.replace(/#/g, String(n));
21
- }
22
- return options['other'] ?? '';
23
- });
24
- // 2) Simple {name} interpolation AFTER plural branch selection.
8
+ // 1) Resolve {x, plural, ...}, {x, select, ...}, {x, selectordinal, ...} with a brace-balanced scanner.
9
+ out = replaceMessageBlocks(out, _lang, params, pluralResolver);
10
+ // 2) Simple {name} interpolation AFTER message block selection.
25
11
  INTERPOLATION_PATTERN.lastIndex = 0;
26
12
  out = out.replace(INTERPOLATION_PATTERN, (_m, p1) => params[p1] != null ? String(params[p1]) : `{${p1}}`);
27
13
  return out;
@@ -29,12 +15,14 @@ function formatIcu(_lang, key, cat, params = {}, onMissingKey) {
29
15
  function lookup(path, obj) {
30
16
  return path
31
17
  .split('.')
32
- .reduce((o, k) => (o && k in o ? o[k] : undefined), obj);
18
+ .reduce((o, k) => o && typeof o === 'object' && Object.prototype.hasOwnProperty.call(o, k)
19
+ ? o[k]
20
+ : undefined, obj);
33
21
  }
34
22
  /**
35
- * Replace all `{arg, plural, ...}` blocks in `s` using a brace-balanced scan.
23
+ * Replace all `{arg, plural|select|selectordinal, ...}` blocks in `s` using a brace-balanced scan.
36
24
  */
37
- function replacePluralBlocks(s, render) {
25
+ function replaceMessageBlocks(s, lang, params, pluralResolver) {
38
26
  let i = 0;
39
27
  let out = '';
40
28
  while (i < s.length) {
@@ -44,19 +32,20 @@ function replacePluralBlocks(s, render) {
44
32
  break;
45
33
  }
46
34
  out += s.slice(i, start);
47
- // Try to match the prefix "{arg, plural,"
48
- const prefixMatch = /\{(\w+),\s*plural,\s*/y;
35
+ // Try to match the prefix "{arg, plural|select|selectordinal,"
36
+ const prefixMatch = /\{(\w+),\s*(plural|select|selectordinal),\s*/y;
49
37
  prefixMatch.lastIndex = start;
50
38
  const m = prefixMatch.exec(s);
51
39
  if (!m) {
52
- // Not a plural block; copy '{' and continue scanning after it.
40
+ // Not a message block; copy '{' and continue scanning after it.
53
41
  out += '{';
54
42
  i = start + 1;
55
43
  continue;
56
44
  }
57
45
  const arg = m[1];
46
+ const keyword = m[2];
58
47
  let j = prefixMatch.lastIndex; // position after the matched prefix
59
- // Find the matching closing '}' for the whole plural block with nesting.
48
+ // Find the matching closing '}' for the whole block with nesting.
60
49
  let depth = 1;
61
50
  while (j < s.length && depth > 0) {
62
51
  const ch = s.charAt(j++);
@@ -73,42 +62,62 @@ function replacePluralBlocks(s, render) {
73
62
  }
74
63
  // Body is the contents between prefix end and the final '}'.
75
64
  const body = s.slice(prefixMatch.lastIndex, j - 1);
76
- const rendered = render(arg, body);
77
- out += rendered;
78
- i = j; // continue after the closing brace
65
+ const options = parsePluralBody(body);
66
+ if (keyword === 'plural' || keyword === 'selectordinal') {
67
+ const n = Number(params[arg] ?? 0);
68
+ if (Number.isFinite(n)) {
69
+ const exact = options[`=${n}`];
70
+ if (exact != null) {
71
+ out += replaceHash(replaceMessageBlocks(exact, lang, params, pluralResolver), n);
72
+ i = j;
73
+ continue;
74
+ }
75
+ const category = pluralResolver
76
+ ? pluralResolver(n, lang)
77
+ : n === 1 ? 'one' : 'other';
78
+ const match = options[category] ?? options['other'] ?? '';
79
+ out += replaceHash(replaceMessageBlocks(match, lang, params, pluralResolver), n);
80
+ }
81
+ else {
82
+ out += replaceHash(replaceMessageBlocks(options['other'] ?? '', lang, params, pluralResolver), Number(params[arg]));
83
+ }
84
+ }
85
+ else {
86
+ // select: look up param value directly, then resolve any nested keyword blocks
87
+ const val = String(params[arg] ?? 'other');
88
+ out += replaceMessageBlocks(options[val] ?? options['other'] ?? '', lang, params, pluralResolver);
89
+ }
90
+ i = j;
79
91
  }
80
92
  return out;
81
93
  }
94
+ function replaceHash(s, n) {
95
+ return s.replace(/#/g, String(n));
96
+ }
82
97
  /**
83
- * Parse a simple ICU plural clause body: e.g. `one {A} other {B} =0 {C}`.
84
- * Only literal selectors and balanced brace bodies are supported; nested plural/select forms are intentionally skipped.
98
+ * Parse a simple ICU plural/select clause body: e.g. `one {A} other {B} =0 {C}`.
99
+ * Supports balanced brace bodies and nested forms.
85
100
  */
86
101
  function parsePluralBody(body) {
87
102
  const map = {};
88
103
  let i = 0;
89
104
  while (i < body.length) {
90
105
  // Skip whitespace between selectors.
91
- while (i < body.length && /\s/.test(body.charAt(i))) {
106
+ while (i < body.length && /\s/.test(body.charAt(i)))
92
107
  i++;
93
- }
94
- if (i >= body.length) {
108
+ if (i >= body.length)
95
109
  break;
96
- }
97
110
  const keyStart = i;
98
- while (i < body.length && !/\s|\{/.test(body.charAt(i))) {
111
+ while (i < body.length && !/[\s{]/.test(body.charAt(i)))
99
112
  i++;
100
- }
101
- if (keyStart === i) {
113
+ if (keyStart === i)
102
114
  break;
103
- }
104
115
  const key = body.slice(keyStart, i);
105
116
  // Skip whitespace before the opening brace.
106
- while (i < body.length && /\s/.test(body.charAt(i))) {
117
+ while (i < body.length && /\s/.test(body.charAt(i)))
107
118
  i++;
108
- }
109
- if (body.charAt(i) !== '{') {
119
+ if (body.charAt(i) !== '{')
110
120
  break;
111
- }
112
121
  i++; // Consume '{'
113
122
  const valueStart = i;
114
123
  let depth = 1;
@@ -124,8 +133,7 @@ function parsePluralBody(body) {
124
133
  // Unbalanced braces (malformed plural); bail out to avoid infinite loops.
125
134
  break;
126
135
  }
127
- const valueEnd = i - 1;
128
- map[key] = body.slice(valueStart, valueEnd);
136
+ map[key] = body.slice(valueStart, i - 1);
129
137
  }
130
138
  return map;
131
139
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ngx-runtime-i18n-core.mjs","sources":["../../../../libs/runtime-i18n/src/lib/icu.ts","../../../../libs/runtime-i18n/src/index.ts","../../../../libs/runtime-i18n/src/ngx-runtime-i18n-core.ts"],"sourcesContent":["/**\n * Very small ICU-like formatter with interpolation and plural basics.\n * Used by the Angular service and pipe. Replace with full ICU in a later minor.\n * @experimental\n */\nimport type { Catalog } from './types';\n\n// Tokens may include dots or hyphens so nested object keys like \"user.name\" are practical.\nconst INTERPOLATION_PATTERN = /\\{([a-zA-Z_][a-zA-Z0-9_.-]*)\\}/g;\n\nexport function formatIcu(\n _lang: string,\n key: string,\n cat: Catalog,\n params: Record<string, unknown> = {},\n onMissingKey?: (k: string) => string\n): string {\n const raw = lookup(key, cat);\n if (raw == null) return onMissingKey ? onMissingKey(key) : key;\n\n let out = String(raw);\n\n // 1) Resolve {x, plural, ...} with a brace-balanced scanner.\n out = replacePluralBlocks(out, (arg, body) => {\n const n = Number(params[arg] ?? 0);\n const options = parsePluralBody(body);\n if (Number.isFinite(n)) {\n const exact = options[`=${n}`];\n if (exact) return exact;\n const one = options['one'];\n if (n === 1 && one) return one;\n const other = options['other'] ?? '';\n return other.replace(/#/g, String(n));\n }\n return options['other'] ?? '';\n });\n\n // 2) Simple {name} interpolation AFTER plural branch selection.\n INTERPOLATION_PATTERN.lastIndex = 0;\n out = out.replace(INTERPOLATION_PATTERN, (_m: string, p1: string) =>\n params[p1] != null ? String(params[p1]) : `{${p1}}`\n );\n\n return out;\n}\n\nfunction lookup(path: string, obj: any): any {\n return path\n .split('.')\n .reduce((o: any, k: string) => (o && k in o ? o[k] : undefined), obj);\n}\n\n/**\n * Replace all `{arg, plural, ...}` blocks in `s` using a brace-balanced scan.\n */\nfunction replacePluralBlocks(\n s: string,\n render: (arg: string, body: string) => string\n): string {\n let i = 0;\n let out = '';\n\n while (i < s.length) {\n const start = s.indexOf('{', i);\n if (start === -1) {\n out += s.slice(i);\n break;\n }\n out += s.slice(i, start);\n\n // Try to match the prefix \"{arg, plural,\"\n const prefixMatch = /\\{(\\w+),\\s*plural,\\s*/y;\n prefixMatch.lastIndex = start;\n const m = prefixMatch.exec(s);\n if (!m) {\n // Not a plural block; copy '{' and continue scanning after it.\n out += '{';\n i = start + 1;\n continue;\n }\n\n const arg = m[1];\n let j = prefixMatch.lastIndex; // position after the matched prefix\n\n // Find the matching closing '}' for the whole plural block with nesting.\n let depth = 1;\n while (j < s.length && depth > 0) {\n const ch = s.charAt(j++);\n if (ch === '{') depth++;\n else if (ch === '}') depth--;\n }\n\n if (depth !== 0) {\n // Unbalanced; fall back to literal copy of the unmatched segment.\n out += s.slice(start, j);\n i = j;\n continue;\n }\n\n // Body is the contents between prefix end and the final '}'.\n const body = s.slice(prefixMatch.lastIndex, j - 1);\n const rendered = render(arg, body);\n out += rendered;\n i = j; // continue after the closing brace\n }\n\n return out;\n}\n\n/**\n * Parse a simple ICU plural clause body: e.g. `one {A} other {B} =0 {C}`.\n * Only literal selectors and balanced brace bodies are supported; nested plural/select forms are intentionally skipped.\n */\nfunction parsePluralBody(body: string): Record<string, string> {\n const map: Record<string, string> = {};\n let i = 0;\n\n while (i < body.length) {\n // Skip whitespace between selectors.\n while (i < body.length && /\\s/.test(body.charAt(i))) {\n i++;\n }\n if (i >= body.length) {\n break;\n }\n\n const keyStart = i;\n while (i < body.length && !/\\s|\\{/.test(body.charAt(i))) {\n i++;\n }\n if (keyStart === i) {\n break;\n }\n const key = body.slice(keyStart, i);\n\n // Skip whitespace before the opening brace.\n while (i < body.length && /\\s/.test(body.charAt(i))) {\n i++;\n }\n if (body.charAt(i) !== '{') {\n break;\n }\n i++; // Consume '{'\n\n const valueStart = i;\n let depth = 1;\n // Consume until the matching closing brace; supports nested braces for placeholders.\n while (i < body.length && depth > 0) {\n const ch = body.charAt(i++);\n if (ch === '{') depth++;\n else if (ch === '}') depth--;\n }\n if (depth !== 0) {\n // Unbalanced braces (malformed plural); bail out to avoid infinite loops.\n break;\n }\n\n const valueEnd = i - 1;\n map[key] = body.slice(valueStart, valueEnd);\n }\n\n return map;\n}\n","/**\n * @packageDocumentation\n * Core runtime i18n primitives.\n * Keep this surface minimal and stable.\n */\n\nexport type { Catalog, RuntimeI18nConfig } from './lib/types';\n\n/**\n * Lightweight ICU-style formatter used internally by the Angular service.\n * Exposed as @experimental for advanced users or custom integrations.\n * @experimental\n */\nexport { formatIcu } from './lib/icu';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ngx-runtime-i18n-core.mjs","sources":["../../../../libs/runtime-i18n/src/lib/icu.ts","../../../../libs/runtime-i18n/src/index.ts","../../../../libs/runtime-i18n/src/ngx-runtime-i18n-core.ts"],"sourcesContent":["/**\n * ICU-style formatter with interpolation, plural, select, and selectordinal support.\n * Used by the Angular service and pipe.\n * @experimental\n */\nimport type { Catalog, PluralCategory, PluralResolver } from './types';\n\n// Tokens may include dots or hyphens so nested object keys like \"user.name\" are practical.\nconst INTERPOLATION_PATTERN = /\\{([a-zA-Z_][a-zA-Z0-9_.-]*)\\}/g;\n\nexport function formatIcu(\n _lang: string,\n key: string,\n cat: Catalog,\n params: Record<string, unknown> = {},\n onMissingKey?: (k: string) => string,\n pluralResolver?: PluralResolver\n): string {\n const raw = lookup(key, cat);\n if (raw == null) return onMissingKey ? onMissingKey(key) : key;\n\n let out = String(raw);\n\n // 1) Resolve {x, plural, ...}, {x, select, ...}, {x, selectordinal, ...} with a brace-balanced scanner.\n out = replaceMessageBlocks(out, _lang, params, pluralResolver);\n\n // 2) Simple {name} interpolation AFTER message block selection.\n INTERPOLATION_PATTERN.lastIndex = 0;\n out = out.replace(INTERPOLATION_PATTERN, (_m: string, p1: string) =>\n params[p1] != null ? String(params[p1]) : `{${p1}}`\n );\n\n return out;\n}\n\nfunction lookup(path: string, obj: unknown): unknown {\n return path\n .split('.')\n .reduce((o: unknown, k: string) =>\n o && typeof o === 'object' && Object.prototype.hasOwnProperty.call(o, k)\n ? (o as Record<string, unknown>)[k]\n : undefined,\n obj);\n}\n\n/**\n * Replace all `{arg, plural|select|selectordinal, ...}` blocks in `s` using a brace-balanced scan.\n */\nfunction replaceMessageBlocks(\n s: string,\n lang: string,\n params: Record<string, unknown>,\n pluralResolver?: PluralResolver\n): string {\n let i = 0;\n let out = '';\n\n while (i < s.length) {\n const start = s.indexOf('{', i);\n if (start === -1) { out += s.slice(i); break; }\n out += s.slice(i, start);\n\n // Try to match the prefix \"{arg, plural|select|selectordinal,\"\n const prefixMatch = /\\{(\\w+),\\s*(plural|select|selectordinal),\\s*/y;\n prefixMatch.lastIndex = start;\n const m = prefixMatch.exec(s);\n if (!m) {\n // Not a message block; copy '{' and continue scanning after it.\n out += '{';\n i = start + 1;\n continue;\n }\n\n const arg = m[1];\n const keyword = m[2] as 'plural' | 'select' | 'selectordinal';\n let j = prefixMatch.lastIndex; // position after the matched prefix\n\n // Find the matching closing '}' for the whole block with nesting.\n let depth = 1;\n while (j < s.length && depth > 0) {\n const ch = s.charAt(j++);\n if (ch === '{') depth++;\n else if (ch === '}') depth--;\n }\n\n if (depth !== 0) {\n // Unbalanced; fall back to literal copy of the unmatched segment.\n out += s.slice(start, j);\n i = j;\n continue;\n }\n\n // Body is the contents between prefix end and the final '}'.\n const body = s.slice(prefixMatch.lastIndex, j - 1);\n const options = parsePluralBody(body);\n\n if (keyword === 'plural' || keyword === 'selectordinal') {\n const n = Number(params[arg] ?? 0);\n if (Number.isFinite(n)) {\n const exact = options[`=${n}`];\n if (exact != null) {\n out += replaceHash(replaceMessageBlocks(exact, lang, params, pluralResolver), n);\n i = j;\n continue;\n }\n\n const category: PluralCategory = pluralResolver\n ? pluralResolver(n, lang)\n : n === 1 ? 'one' : 'other';\n\n const match = options[category] ?? options['other'] ?? '';\n out += replaceHash(replaceMessageBlocks(match, lang, params, pluralResolver), n);\n } else {\n out += replaceHash(replaceMessageBlocks(options['other'] ?? '', lang, params, pluralResolver), Number(params[arg]));\n }\n } else {\n // select: look up param value directly, then resolve any nested keyword blocks\n const val = String(params[arg] ?? 'other');\n out += replaceMessageBlocks(options[val] ?? options['other'] ?? '', lang, params, pluralResolver);\n }\n\n i = j;\n }\n\n return out;\n}\n\nfunction replaceHash(s: string, n: number): string {\n return s.replace(/#/g, String(n));\n}\n\n/**\n * Parse a simple ICU plural/select clause body: e.g. `one {A} other {B} =0 {C}`.\n * Supports balanced brace bodies and nested forms.\n */\nfunction parsePluralBody(body: string): Record<string, string> {\n const map: Record<string, string> = {};\n let i = 0;\n\n while (i < body.length) {\n // Skip whitespace between selectors.\n while (i < body.length && /\\s/.test(body.charAt(i))) i++;\n if (i >= body.length) break;\n\n const keyStart = i;\n while (i < body.length && !/[\\s{]/.test(body.charAt(i))) i++;\n if (keyStart === i) break;\n const key = body.slice(keyStart, i);\n\n // Skip whitespace before the opening brace.\n while (i < body.length && /\\s/.test(body.charAt(i))) i++;\n if (body.charAt(i) !== '{') break;\n i++; // Consume '{'\n\n const valueStart = i;\n let depth = 1;\n // Consume until the matching closing brace; supports nested braces for placeholders.\n while (i < body.length && depth > 0) {\n const ch = body.charAt(i++);\n if (ch === '{') depth++;\n else if (ch === '}') depth--;\n }\n if (depth !== 0) {\n // Unbalanced braces (malformed plural); bail out to avoid infinite loops.\n break;\n }\n\n map[key] = body.slice(valueStart, i - 1);\n }\n\n return map;\n}\n","/**\n * @packageDocumentation\n * Core runtime i18n primitives.\n * Keep this surface minimal and stable.\n */\n\nexport type {\n Catalog,\n RuntimeI18nConfig,\n I18nSchema,\n TranslationKey,\n TranslationParams,\n DeepKeys,\n ExtractParams,\n ActiveCatalogType,\n PluralCategory,\n PluralResolver,\n} from './lib/types';\n\n/**\n * Lightweight ICU-style formatter used internally by the Angular service.\n * Exposed as @experimental for advanced users or custom integrations.\n * @experimental\n */\nexport { formatIcu } from './lib/icu';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ngx-runtime-i18n/core",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Runtime i18n core with ICU-lite formatting",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -21,7 +21,7 @@
21
21
  "exports": {
22
22
  ".": {
23
23
  "import": "./fesm2022/ngx-runtime-i18n-core.mjs",
24
- "types": "./index.d.ts",
24
+ "types": "./types/ngx-runtime-i18n-core.d.ts",
25
25
  "default": "./fesm2022/ngx-runtime-i18n-core.mjs"
26
26
  },
27
27
  "./package.json": {
@@ -30,14 +30,14 @@
30
30
  },
31
31
  "publishConfig": {
32
32
  "access": "public",
33
- "provenance": false
33
+ "provenance": true
34
34
  },
35
35
  "engines": {
36
36
  "node": ">=18"
37
37
  },
38
38
  "module": "fesm2022/ngx-runtime-i18n-core.mjs",
39
- "typings": "index.d.ts",
39
+ "typings": "types/ngx-runtime-i18n-core.d.ts",
40
40
  "dependencies": {
41
41
  "tslib": "^2.3.0"
42
42
  }
43
- }
43
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * A translation catalog is a nested object where keys are dot-addressable.
3
+ * Example:
4
+ * {
5
+ * "hello": { "user": "Hello, {name}!" },
6
+ * "cart": { "items": "{count, plural, one {1 item} other {# items}}" }
7
+ * }
8
+ * Keys may contain ICU message syntax.
9
+ * @publicApi
10
+ */
11
+ type Catalog = Record<string, unknown>;
12
+ /**
13
+ * Runtime i18n configuration shared across CSR and SSR.
14
+ * Provide via {@link @ngx-runtime-i18n/angular!provideRuntimeI18n | provideRuntimeI18n()}.
15
+ * @publicApi
16
+ */
17
+ interface RuntimeI18nConfig {
18
+ /**
19
+ * The language to render when no user preference is known.
20
+ * SSR should override per request (e.g., from URL/cookie).
21
+ */
22
+ defaultLang: string;
23
+ /**
24
+ * Optional ordered list of fallback languages (per key).
25
+ * Missing keys resolve using: active → fallbacks → defaultLang.
26
+ */
27
+ fallbacks?: string[];
28
+ /**
29
+ * The set of allowed languages. `setLang()` will guard against values not in this list.
30
+ * Use BCP-47 tags (e.g., "en", "en-GB", "hi").
31
+ */
32
+ supported: string[];
33
+ /**
34
+ * Fetch a catalog at runtime. Must be idempotent and cancellable via AbortSignal.
35
+ * - Runs on the client only (the server should seed catalogs via TransferState).
36
+ * - Return a plain object (parsed JSON).
37
+ * - `scope` is set when loading a route-scoped catalog registered via `withI18nScope()`
38
+ * (see `@ngx-runtime-i18n/angular`); build the scoped URL yourself, e.g.
39
+ * `scope ? `/i18n/${scope}/${lang}.json` : `/i18n/${lang}.json``.
40
+ */
41
+ fetchCatalog: (lang: string, signal?: AbortSignal, scope?: string) => Promise<Catalog>;
42
+ /**
43
+ * Missing key handler. When omitted, the key itself is returned (useful in dev).
44
+ * Use to log or to inject a visible marker.
45
+ */
46
+ onMissingKey?: (key: string) => string;
47
+ }
48
+ /**
49
+ * Module-augmentation hook for typed translation keys.
50
+ * Augment this interface in your app to enable typed t() and pipe.
51
+ *
52
+ * @example
53
+ * // src/i18n.d.ts
54
+ * import type en from '../public/i18n/en.json';
55
+ * declare module '@ngx-runtime-i18n/core' {
56
+ * interface I18nSchema { translations: typeof en; }
57
+ * }
58
+ * @publicApi
59
+ */
60
+ interface I18nSchema {
61
+ }
62
+ /** @internal */
63
+ type ActiveCatalogType = I18nSchema extends {
64
+ translations: infer T;
65
+ } ? T : Record<string, unknown>;
66
+ /** Depth guard for recursive type — prevents TypeScript slowdown on large catalogs. @internal */
67
+ type Prev = [never, 0, 1, 2, 3, 4, ...0[]];
68
+ /**
69
+ * Produces dot-notation key paths for T up to Depth levels deep.
70
+ * Depth cap prevents compile-time slowdown at 2000+ keys (i18next issue #1914).
71
+ * @publicApi
72
+ */
73
+ type DeepKeys<T, Depth extends number = 4> = [
74
+ Depth
75
+ ] extends [never] ? never : T extends Record<string, unknown> ? {
76
+ [K in keyof T & string]: K | (T[K] extends Record<string, unknown> ? `${K}.${DeepKeys<T[K], Prev[Depth]>}` : never);
77
+ }[keyof T & string] : never;
78
+ /**
79
+ * The union of valid translation keys, or `string` when no schema is provided (backward compat).
80
+ * @publicApi
81
+ */
82
+ type TranslationKey = I18nSchema extends {
83
+ translations: infer T;
84
+ } ? T extends Record<string, unknown> ? DeepKeys<T> : string : string;
85
+ /**
86
+ * Extracts interpolation param names from an ICU message string literal.
87
+ * Works for simple {name} tokens and ICU keyword blocks {count, plural, ...}.
88
+ * @publicApi
89
+ */
90
+ type ExtractParams<S extends string> = S extends `${string}{${infer Token}}${infer Rest}` ? Token extends `${infer Arg},${infer Keyword},${string}` ? Keyword extends 'plural' | 'select' | 'selectordinal' ? {
91
+ [K in Arg]: number;
92
+ } & ExtractParams<Rest> : {
93
+ [K in Token]: string | number;
94
+ } & ExtractParams<Rest> : {
95
+ [K in Token]: string | number;
96
+ } & ExtractParams<Rest> : Record<never, never>;
97
+ /** @internal */
98
+ type ResolveValue<T, P extends string> = P extends `${infer K}.${infer Rest}` ? K extends keyof T ? ResolveValue<T[K], Rest> : never : P extends keyof T ? T[P] : never;
99
+ /**
100
+ * Resolves the interpolation params type for a given key K.
101
+ * Falls back to Record<string, unknown> when no schema or when value is not a string literal.
102
+ * @publicApi
103
+ */
104
+ type TranslationParams<K extends TranslationKey> = I18nSchema extends {
105
+ translations: infer Cat;
106
+ } ? Cat extends Record<string, unknown> ? K extends string ? ResolveValue<Cat, K> extends string ? ExtractParams<ResolveValue<Cat, K>> : Record<string, unknown> : Record<string, unknown> : Record<string, unknown> : Record<string, unknown>;
107
+ /**
108
+ * CLDR plural categories per Unicode TR35.
109
+ * @publicApi
110
+ */
111
+ type PluralCategory = 'zero' | 'one' | 'two' | 'few' | 'many' | 'other';
112
+ /**
113
+ * Resolves the CLDR plural category for a count in a given locale.
114
+ * Provide via Angular's getLocalePluralCase() or a custom implementation.
115
+ * Falls back to English-biased one/other logic when not provided.
116
+ * @publicApi
117
+ */
118
+ type PluralResolver = (count: number, locale: string) => PluralCategory;
119
+
120
+ /**
121
+ * ICU-style formatter with interpolation, plural, select, and selectordinal support.
122
+ * Used by the Angular service and pipe.
123
+ * @experimental
124
+ */
125
+
126
+ declare function formatIcu(_lang: string, key: string, cat: Catalog, params?: Record<string, unknown>, onMissingKey?: (k: string) => string, pluralResolver?: PluralResolver): string;
127
+
128
+ export { formatIcu };
129
+ export type { ActiveCatalogType, Catalog, DeepKeys, ExtractParams, I18nSchema, PluralCategory, PluralResolver, RuntimeI18nConfig, TranslationKey, TranslationParams };
package/index.d.ts DELETED
@@ -1,55 +0,0 @@
1
- /**
2
- * A translation catalog is a nested object where keys are dot-addressable.
3
- * Example:
4
- * {
5
- * "hello": { "user": "Hello, {name}!" },
6
- * "cart": { "items": "{count, plural, one {1 item} other {# items}}" }
7
- * }
8
- * Keys may contain ICU message syntax.
9
- * @publicApi
10
- */
11
- type Catalog = Record<string, unknown>;
12
- /**
13
- * Runtime i18n configuration shared across CSR and SSR.
14
- * Provide via {@link @ngx-runtime-i18n/angular!provideRuntimeI18n | provideRuntimeI18n()}.
15
- * @publicApi
16
- */
17
- interface RuntimeI18nConfig {
18
- /**
19
- * The language to render when no user preference is known.
20
- * SSR should override per request (e.g., from URL/cookie).
21
- */
22
- defaultLang: string;
23
- /**
24
- * Optional ordered list of fallback languages (per key).
25
- * Missing keys resolve using: active → fallbacks → defaultLang.
26
- */
27
- fallbacks?: string[];
28
- /**
29
- * The set of allowed languages. `setLang()` will guard against values not in this list.
30
- * Use BCP-47 tags (e.g., "en", "en-GB", "hi").
31
- */
32
- supported: string[];
33
- /**
34
- * Fetch a catalog at runtime. Must be idempotent and cancellable via AbortSignal.
35
- * - Runs on the client only (the server should seed catalogs via TransferState).
36
- * - Return a plain object (parsed JSON).
37
- */
38
- fetchCatalog: (lang: string, signal?: AbortSignal) => Promise<Catalog>;
39
- /**
40
- * Missing key handler. When omitted, the key itself is returned (useful in dev).
41
- * Use to log or to inject a visible marker.
42
- */
43
- onMissingKey?: (key: string) => string;
44
- }
45
-
46
- /**
47
- * Very small ICU-like formatter with interpolation and plural basics.
48
- * Used by the Angular service and pipe. Replace with full ICU in a later minor.
49
- * @experimental
50
- */
51
-
52
- declare function formatIcu(_lang: string, key: string, cat: Catalog, params?: Record<string, unknown>, onMissingKey?: (k: string) => string): string;
53
-
54
- export { formatIcu };
55
- export type { Catalog, RuntimeI18nConfig };