@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 +8 -8
- package/fesm2022/ngx-runtime-i18n-core.mjs +52 -44
- package/fesm2022/ngx-runtime-i18n-core.mjs.map +1 -1
- package/package.json +5 -5
- package/types/ngx-runtime-i18n-core.d.ts +129 -0
- package/index.d.ts +0 -55
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
|
|
45
|
-
- **`key: string
|
|
46
|
-
- **`catalog: Catalog
|
|
47
|
-
- **`params?: Record<string, unknown
|
|
48
|
-
- **`onMissingKey?: (key: string) => string
|
|
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
|
|
53
|
-
- **`RuntimeI18nConfig
|
|
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
|
|
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 =
|
|
10
|
-
|
|
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) =>
|
|
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
|
|
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
|
|
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
|
|
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
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
*
|
|
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 &&
|
|
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
|
-
|
|
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.
|
|
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": "./
|
|
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":
|
|
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": "
|
|
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 };
|