@ngx-runtime-i18n/core 2.1.0 → 2.2.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
@@ -2,7 +2,7 @@
2
2
 
3
3
  Framework‑agnostic primitives for runtime internationalisation:
4
4
 
5
- - Tiny, dependency‑free **ICU‑lite** formatter (interpolation + basic `plural`).
5
+ - Tiny, dependency‑free **ICU‑lite** formatter (interpolation, `plural`, `select`, `selectordinal`).
6
6
  - Shared types used by the Angular wrapper.
7
7
 
8
8
  This package is designed to be used directly or via `@ngx-runtime-i18n/angular`.
@@ -39,18 +39,33 @@ formatIcu('en', 'cart.items', catalog, { count: 2 }); // "2 items"
39
39
 
40
40
  ## API
41
41
 
42
- ### `formatIcu(lang, key, catalog, params?, onMissingKey?)`
42
+ ### `formatIcu(lang, key, catalog, params?, onMissingKey?, pluralResolver?)`
43
43
 
44
44
  - **`lang: string`**: current language (for plural rules and future features).
45
45
  - **`key: string`**: dotted path into the catalog.
46
46
  - **`catalog: Catalog`**: a nested object of strings/objects.
47
- - **`params?: Record<string, unknown>`**: interpolation values.
47
+ - **`params?: Record<string, unknown> | null`**: interpolation values (`null`/`undefined` are both treated as no params).
48
48
  - **`onMissingKey?: (key: string) => string`**: transform for missing keys (defaults to returning the key).
49
+ - **`pluralResolver?: PluralResolver`**: resolves the CLDR plural category for `plural`/`selectordinal` blocks. Without one, the built-in fallback treats `Math.abs(n) === 1` as `one` and everything else as `other` — English-shaped, but sign-agnostic (`-1` is also `one`).
49
50
 
50
51
  ### Types
51
52
 
52
53
  - **`Catalog`**: `Record<string, unknown>` (nested object).
53
54
  - **`RuntimeI18nConfig`**: shape shared with the Angular wrapper for consistency.
55
+ - **`I18nSchema`**: an empty, module-augmentable interface. Augment it in your app (see below) to unlock typed keys and params:
56
+ ```ts
57
+ // src/i18n.d.ts
58
+ import type en from '../public/i18n/en.json';
59
+ declare module '@ngx-runtime-i18n/core' {
60
+ interface I18nSchema { translations: typeof en; }
61
+ }
62
+ ```
63
+ - **`TranslationKey`**: union of dot-notation key paths derived from `I18nSchema['translations']`, or `string` when no schema is augmented.
64
+ - **`TranslationParams<K>`**: the interpolation params required for key `K`, derived from parsing the catalog string for `K` at the type level. Falls back to `Record<string, unknown>` when no schema is augmented.
65
+ - **`DeepKeys<T>`** / **`ExtractParams<S>`**: the type-level machinery behind `TranslationKey`/`TranslationParams` — exported for advanced use (e.g. building your own typed helpers). `ExtractParams` balances nested braces inside `plural`/`select`/`selectordinal` clause bodies, so params are extracted correctly even when a clause contains its own `{...}` (e.g. `{count, plural, other {# items}}` still yields `{ count: number }`, not a param named after the whole clause body).
66
+ - **`ActiveCatalogType`**: the augmented catalog's shape (`I18nSchema['translations']`), or `Record<string, unknown>` when unaugmented.
67
+ - **`PluralCategory`**: `'zero' | 'one' | 'two' | 'few' | 'many' | 'other'` (CLDR TR35 categories).
68
+ - **`PluralResolver`**: `(count: number, locale: string) => PluralCategory` — see `pluralResolver` above.
54
69
 
55
70
  ---
56
71
 
@@ -80,14 +95,15 @@ formatIcu('en', 'cart.items', catalog, { count: 2 }); // "2 items"
80
95
  ### Supported
81
96
 
82
97
  - Basic `{param}` interpolation (tokens may include dots and hyphens for nested data).
83
- - `plural` blocks with `one`, `other`, and `=n` selectors plus `#` replacement.
84
- - Nested placeholders inside plural option bodies (balanced braces are retained).
98
+ - `plural` blocks with `one`, `other`, `=n` selectors, `#` replacement, and an optional leading `offset:N` (subtracted from the count for category selection and `#`, but not for `=n` exact-match comparisons, per ICU).
99
+ - `select` blocks with arbitrary named options, falling back to `other` when a value doesn't match.
100
+ - `selectordinal` blocks, using the same `=n` exact-match syntax as `plural`. Without a custom plural resolver, both `plural` and `selectordinal` fall back to a plain `one`-if-equal-to-1/`other` rule; pass a `pluralResolver` for full CLDR (cardinal or ordinal) categories.
101
+ - Nesting `plural`, `select`, and `selectordinal` blocks inside each other, including the same keyword nested inside itself, to arbitrary depth.
85
102
 
86
103
  ### Not supported
87
104
 
88
- - `select` or other ICU argument types beyond `plural`.
89
- - Full ICU-style escaping, quoting, or nested plural/select grammar.
90
- - Plural blocks inside other plural blocks (depth beyond one level is skipped).
105
+ - ICU-style escaping or quoting (`''`, `'{'`) quotes and braces pass through as written.
106
+ - Argument types beyond `plural`, `select`, and `selectordinal` (ICU's `number`, `date`, `time`, or custom formatters).
91
107
  - Escaping braces beyond the literals above; unmatched braces must not resemble valid tokens.
92
108
 
93
109
  ---
@@ -4,12 +4,13 @@ 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
+ const p = params ?? {};
7
8
  let out = String(raw);
8
9
  // 1) Resolve {x, plural, ...}, {x, select, ...}, {x, selectordinal, ...} with a brace-balanced scanner.
9
- out = replaceMessageBlocks(out, _lang, params, pluralResolver);
10
+ out = replaceMessageBlocks(out, _lang, p, pluralResolver);
10
11
  // 2) Simple {name} interpolation AFTER message block selection.
11
12
  INTERPOLATION_PATTERN.lastIndex = 0;
12
- out = out.replace(INTERPOLATION_PATTERN, (_m, p1) => params[p1] != null ? String(params[p1]) : `{${p1}}`);
13
+ out = out.replace(INTERPOLATION_PATTERN, (_m, p1) => p[p1] != null ? String(p[p1]) : `{${p1}}`);
13
14
  return out;
14
15
  }
15
16
  function lookup(path, obj) {
@@ -62,24 +63,25 @@ function replaceMessageBlocks(s, lang, params, pluralResolver) {
62
63
  }
63
64
  // Body is the contents between prefix end and the final '}'.
64
65
  const body = s.slice(prefixMatch.lastIndex, j - 1);
65
- const options = parsePluralBody(body);
66
+ const { offset, map: options } = parsePluralBody(body);
66
67
  if (keyword === 'plural' || keyword === 'selectordinal') {
67
68
  const n = Number(params[arg] ?? 0);
68
69
  if (Number.isFinite(n)) {
69
70
  const exact = options[`=${n}`];
70
71
  if (exact != null) {
71
- out += replaceHash(replaceMessageBlocks(exact, lang, params, pluralResolver), n);
72
+ out += replaceHash(replaceMessageBlocks(exact, lang, params, pluralResolver), n - offset);
72
73
  i = j;
73
74
  continue;
74
75
  }
76
+ const shifted = n - offset;
75
77
  const category = pluralResolver
76
- ? pluralResolver(n, lang)
77
- : n === 1 ? 'one' : 'other';
78
+ ? pluralResolver(shifted, lang)
79
+ : Math.abs(shifted) === 1 ? 'one' : 'other';
78
80
  const match = options[category] ?? options['other'] ?? '';
79
- out += replaceHash(replaceMessageBlocks(match, lang, params, pluralResolver), n);
81
+ out += replaceHash(replaceMessageBlocks(match, lang, params, pluralResolver), shifted);
80
82
  }
81
83
  else {
82
- out += replaceHash(replaceMessageBlocks(options['other'] ?? '', lang, params, pluralResolver), Number(params[arg]));
84
+ out += replaceHash(replaceMessageBlocks(options['other'] ?? '', lang, params, pluralResolver), 0);
83
85
  }
84
86
  }
85
87
  else {
@@ -96,11 +98,16 @@ function replaceHash(s, n) {
96
98
  }
97
99
  /**
98
100
  * Parse a simple ICU plural/select clause body: e.g. `one {A} other {B} =0 {C}`.
99
- * Supports balanced brace bodies and nested forms.
101
+ * Supports balanced brace bodies, nested forms, and a leading `offset:N` token.
102
+ * The returned map has no prototype, so a selector value matching an
103
+ * `Object.prototype` member name (e.g. "constructor") can never resolve to an
104
+ * inherited function instead of `undefined`.
100
105
  */
101
106
  function parsePluralBody(body) {
102
- const map = {};
103
- let i = 0;
107
+ const map = Object.create(null);
108
+ const offsetMatch = /^\s*offset:\s*(\d+)\s*/.exec(body);
109
+ const offset = offsetMatch ? Number(offsetMatch[1]) : 0;
110
+ let i = offsetMatch ? offsetMatch[0].length : 0;
104
111
  while (i < body.length) {
105
112
  // Skip whitespace between selectors.
106
113
  while (i < body.length && /\s/.test(body.charAt(i)))
@@ -135,7 +142,7 @@ function parsePluralBody(body) {
135
142
  }
136
143
  map[key] = body.slice(valueStart, i - 1);
137
144
  }
138
- return map;
145
+ return { offset, map };
139
146
  }
140
147
 
141
148
  /**
@@ -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 * 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":"AAOA;AACA,MAAM,qBAAqB,GAAG,iCAAiC;AAEzD,SAAU,SAAS,CACvB,KAAa,EACb,GAAW,EACX,GAAY,EACZ,MAAA,GAAkC,EAAE,EACpC,YAAoC,EACpC,cAA+B,EAAA;IAE/B,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC;IAC5B,IAAI,GAAG,IAAI,IAAI;AAAE,QAAA,OAAO,YAAY,GAAG,YAAY,CAAC,GAAG,CAAC,GAAG,GAAG;AAE9D,IAAA,IAAI,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC;;IAGrB,GAAG,GAAG,oBAAoB,CAAC,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,cAAc,CAAC;;AAG9D,IAAA,qBAAqB,CAAC,SAAS,GAAG,CAAC;AACnC,IAAA,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,qBAAqB,EAAE,CAAC,EAAU,EAAE,EAAU,KAC9D,MAAM,CAAC,EAAE,CAAC,IAAI,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,CAAA,CAAA,CAAG,CACpD;AAED,IAAA,OAAO,GAAG;AACZ;AAEA,SAAS,MAAM,CAAC,IAAY,EAAE,GAAY,EAAA;AACxC,IAAA,OAAO;SACJ,KAAK,CAAC,GAAG;SACT,MAAM,CAAC,CAAC,CAAU,EAAE,CAAS,KAC5B,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;AACrE,UAAG,CAA6B,CAAC,CAAC;AAClC,UAAE,SAAS,EACb,GAAG,CAAC;AACV;AAEA;;AAEG;AACH,SAAS,oBAAoB,CAC3B,CAAS,EACT,IAAY,EACZ,MAA+B,EAC/B,cAA+B,EAAA;IAE/B,IAAI,CAAC,GAAG,CAAC;IACT,IAAI,GAAG,GAAG,EAAE;AAEZ,IAAA,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE;QACnB,MAAM,KAAK,GAAG,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;AAC/B,QAAA,IAAI,KAAK,KAAK,CAAC,CAAC,EAAE;AAAE,YAAA,GAAG,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;YAAE;QAAO;QAC9C,GAAG,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC;;QAGxB,MAAM,WAAW,GAAG,+CAA+C;AACnE,QAAA,WAAW,CAAC,SAAS,GAAG,KAAK;QAC7B,MAAM,CAAC,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC;QAC7B,IAAI,CAAC,CAAC,EAAE;;YAEN,GAAG,IAAI,GAAG;AACV,YAAA,CAAC,GAAG,KAAK,GAAG,CAAC;YACb;QACF;AAEA,QAAA,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;AAChB,QAAA,MAAM,OAAO,GAAG,CAAC,CAAC,CAAC,CAA0C;AAC7D,QAAA,IAAI,CAAC,GAAG,WAAW,CAAC,SAAS,CAAC;;QAG9B,IAAI,KAAK,GAAG,CAAC;QACb,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,IAAI,KAAK,GAAG,CAAC,EAAE;YAChC,MAAM,EAAE,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC;YACxB,IAAI,EAAE,KAAK,GAAG;AAAE,gBAAA,KAAK,EAAE;iBAClB,IAAI,EAAE,KAAK,GAAG;AAAE,gBAAA,KAAK,EAAE;QAC9B;AAEA,QAAA,IAAI,KAAK,KAAK,CAAC,EAAE;;YAEf,GAAG,IAAI,CAAC,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC;YACxB,CAAC,GAAG,CAAC;YACL;QACF;;AAGA,QAAA,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,WAAW,CAAC,SAAS,EAAE,CAAC,GAAG,CAAC,CAAC;AAClD,QAAA,MAAM,OAAO,GAAG,eAAe,CAAC,IAAI,CAAC;QAErC,IAAI,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,eAAe,EAAE;YACvD,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AAClC,YAAA,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE;gBACtB,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA,CAAE,CAAC;AAC9B,gBAAA,IAAI,KAAK,IAAI,IAAI,EAAE;AACjB,oBAAA,GAAG,IAAI,WAAW,CAAC,oBAAoB,CAAC,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,CAAC,EAAE,CAAC,CAAC;oBAChF,CAAC,GAAG,CAAC;oBACL;gBACF;gBAEA,MAAM,QAAQ,GAAmB;AAC/B,sBAAE,cAAc,CAAC,CAAC,EAAE,IAAI;AACxB,sBAAE,CAAC,KAAK,CAAC,GAAG,KAAK,GAAG,OAAO;AAE7B,gBAAA,MAAM,KAAK,GAAG,OAAO,CAAC,QAAQ,CAAC,IAAI,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE;AACzD,gBAAA,GAAG,IAAI,WAAW,CAAC,oBAAoB,CAAC,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,CAAC,EAAE,CAAC,CAAC;YAClF;iBAAO;gBACL,GAAG,IAAI,WAAW,CAAC,oBAAoB,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;YACrH;QACF;aAAO;;YAEL,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC;YAC1C,GAAG,IAAI,oBAAoB,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,CAAC;QACnG;QAEA,CAAC,GAAG,CAAC;IACP;AAEA,IAAA,OAAO,GAAG;AACZ;AAEA,SAAS,WAAW,CAAC,CAAS,EAAE,CAAS,EAAA;IACvC,OAAO,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;AACnC;AAEA;;;AAGG;AACH,SAAS,eAAe,CAAC,IAAY,EAAA;IACnC,MAAM,GAAG,GAA2B,EAAE;IACtC,IAAI,CAAC,GAAG,CAAC;AAET,IAAA,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE;;AAEtB,QAAA,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;AAAE,YAAA,CAAC,EAAE;AACxD,QAAA,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM;YAAE;QAEtB,MAAM,QAAQ,GAAG,CAAC;AAClB,QAAA,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;AAAE,YAAA,CAAC,EAAE;QAC5D,IAAI,QAAQ,KAAK,CAAC;YAAE;QACpB,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC;;AAGnC,QAAA,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;AAAE,YAAA,CAAC,EAAE;AACxD,QAAA,IAAI,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG;YAAE;QAC5B,CAAC,EAAE,CAAC;QAEJ,MAAM,UAAU,GAAG,CAAC;QACpB,IAAI,KAAK,GAAG,CAAC;;QAEb,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,IAAI,KAAK,GAAG,CAAC,EAAE;YACnC,MAAM,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC;YAC3B,IAAI,EAAE,KAAK,GAAG;AAAE,gBAAA,KAAK,EAAE;iBAClB,IAAI,EAAE,KAAK,GAAG;AAAE,gBAAA,KAAK,EAAE;QAC9B;AACA,QAAA,IAAI,KAAK,KAAK,CAAC,EAAE;;YAEf;QACF;AAEA,QAAA,GAAG,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,EAAE,CAAC,GAAG,CAAC,CAAC;IAC1C;AAEA,IAAA,OAAO,GAAG;AACZ;;AC3KA;;;;AAIG;AAeH;;;;AAIG;;ACvBH;;AAEG;;;;"}
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> | null = {},\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 const p = params ?? {};\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, p, 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 p[p1] != null ? String(p[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 { offset, map: 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 - offset);\n i = j;\n continue;\n }\n\n const shifted = n - offset;\n const category: PluralCategory = pluralResolver\n ? pluralResolver(shifted, lang)\n : Math.abs(shifted) === 1 ? 'one' : 'other';\n\n const match = options[category] ?? options['other'] ?? '';\n out += replaceHash(replaceMessageBlocks(match, lang, params, pluralResolver), shifted);\n } else {\n out += replaceHash(replaceMessageBlocks(options['other'] ?? '', lang, params, pluralResolver), 0);\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, nested forms, and a leading `offset:N` token.\n * The returned map has no prototype, so a selector value matching an\n * `Object.prototype` member name (e.g. \"constructor\") can never resolve to an\n * inherited function instead of `undefined`.\n */\nfunction parsePluralBody(body: string): { offset: number; map: Record<string, string> } {\n const map: Record<string, string> = Object.create(null);\n const offsetMatch = /^\\s*offset:\\s*(\\d+)\\s*/.exec(body);\n const offset = offsetMatch ? Number(offsetMatch[1]) : 0;\n let i = offsetMatch ? offsetMatch[0].length : 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 { offset, 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.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Runtime i18n core with ICU-lite formatting",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -82,18 +82,34 @@ type DeepKeys<T, Depth extends number = 4> = [
82
82
  type TranslationKey = I18nSchema extends {
83
83
  translations: infer T;
84
84
  } ? T extends Record<string, unknown> ? DeepKeys<T> : string : string;
85
+ /**
86
+ * Splits `S` at the `}` that balances the block's opening `{` (already consumed
87
+ * by the caller), so nested ICU clause braces don't truncate the match early.
88
+ * Returns `[Token, Rest]` once `depth` closes back to 0, else `never`.
89
+ * @internal
90
+ */
91
+ type SplitBalanced<S extends string, Depth extends unknown[] = [], Token extends string = ''> = S extends `${infer Ch}${infer Rest}` ? Ch extends '{' ? SplitBalanced<Rest, [...Depth, 0], `${Token}${Ch}`> : Ch extends '}' ? Depth extends [unknown, ...infer DepthRest] ? SplitBalanced<Rest, DepthRest, `${Token}${Ch}`> : [Token, Rest] : SplitBalanced<Rest, Depth, `${Token}${Ch}`> : never;
92
+ /** Strips leading whitespace, mirroring the runtime scanner's `\s*` after each comma. @internal */
93
+ type TrimLeft<S extends string> = S extends ` ${infer Rest}` ? TrimLeft<Rest> : S;
85
94
  /**
86
95
  * Extracts interpolation param names from an ICU message string literal.
87
- * Works for simple {name} tokens and ICU keyword blocks {count, plural, ...}.
96
+ * Works for simple {name} tokens and ICU keyword blocks {count, plural, ...},
97
+ * balancing nested braces inside plural/select clause bodies (e.g. `{# items}`)
98
+ * instead of stopping at the first `}`, and tolerating arbitrary whitespace
99
+ * after the comma the same way the runtime scanner's `\s*` does.
88
100
  * @publicApi
89
101
  */
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' ? {
102
+ type ExtractParams<S extends string> = S extends `${string}{${infer AfterBrace}` ? SplitBalanced<AfterBrace> extends [infer Token extends string, infer Rest extends string] ? Token extends `${infer Arg},${infer AfterComma}` ? TrimLeft<AfterComma> extends `${infer Keyword},${string}` ? Keyword extends 'plural' | 'selectordinal' ? {
91
103
  [K in Arg]: number;
104
+ } & ExtractParams<Rest> : Keyword extends 'select' ? {
105
+ [K in Arg]: string | number;
106
+ } & ExtractParams<Rest> : {
107
+ [K in Token]: string | number;
92
108
  } & ExtractParams<Rest> : {
93
109
  [K in Token]: string | number;
94
110
  } & ExtractParams<Rest> : {
95
111
  [K in Token]: string | number;
96
- } & ExtractParams<Rest> : Record<never, never>;
112
+ } & ExtractParams<Rest> : Record<never, never> : Record<never, never>;
97
113
  /** @internal */
98
114
  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
115
  /**
@@ -123,7 +139,7 @@ type PluralResolver = (count: number, locale: string) => PluralCategory;
123
139
  * @experimental
124
140
  */
125
141
 
126
- declare function formatIcu(_lang: string, key: string, cat: Catalog, params?: Record<string, unknown>, onMissingKey?: (k: string) => string, pluralResolver?: PluralResolver): string;
142
+ declare function formatIcu(_lang: string, key: string, cat: Catalog, params?: Record<string, unknown> | null, onMissingKey?: (k: string) => string, pluralResolver?: PluralResolver): string;
127
143
 
128
144
  export { formatIcu };
129
145
  export type { ActiveCatalogType, Catalog, DeepKeys, ExtractParams, I18nSchema, PluralCategory, PluralResolver, RuntimeI18nConfig, TranslationKey, TranslationParams };