@ngx-runtime-i18n/core 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.
package/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # @ngx-runtime-i18n
2
+
3
+ Lightweight runtime internationalization core (framework-agnostic).
4
+
5
+ Provides the core ICU-like formatting engine and catalog management.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm i @ngx-runtime-i18n
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ import { formatIcu } from '@ngx-runtime-i18n/core';
17
+
18
+ const catalog = {
19
+ hello: 'Hello {name}!',
20
+ 'cart.items': '{count, plural, =0 {No items} one {1 item} other {# items}} in your cart',
21
+ };
22
+
23
+ formatIcu('en', 'hello', catalog, { name: 'Ashwin' });
24
+ // → "Hello Ashwin!"
25
+ ```
26
+
27
+ This package contains:
28
+
29
+ - `formatIcu(lang, key, catalog, params, onMissingKey?)`
30
+ - Type definitions (`Catalog`, `RuntimeI18nConfig`)
31
+ - Utilities for Angular bindings (`@ngx-runtime-i18n/angular`)
@@ -0,0 +1,110 @@
1
+ function formatIcu(_lang, key, cat, params = {}, onMissingKey) {
2
+ const raw = lookup(key, cat);
3
+ if (raw == null)
4
+ return onMissingKey ? onMissingKey(key) : key;
5
+ let out = String(raw);
6
+ // 1) Resolve {x, plural, ...} with a brace-balanced scanner.
7
+ out = replacePluralBlocks(out, (arg, body) => {
8
+ const n = Number(params[arg] ?? 0);
9
+ const options = parsePluralBody(body);
10
+ if (Number.isFinite(n)) {
11
+ const exact = options[`=${n}`];
12
+ if (exact)
13
+ return exact;
14
+ const one = options['one'];
15
+ if (n === 1 && one)
16
+ return one;
17
+ const other = options['other'] ?? '';
18
+ return other.replace(/#/g, String(n));
19
+ }
20
+ return options['other'] ?? '';
21
+ });
22
+ // 2) Simple {name} interpolation AFTER plural branch selection.
23
+ out = out.replace(/\{(\w+)\}/g, (_m, p1) => params[p1] != null ? String(params[p1]) : `{${p1}}`);
24
+ return out;
25
+ }
26
+ function lookup(path, obj) {
27
+ return path
28
+ .split('.')
29
+ .reduce((o, k) => (o && k in o ? o[k] : undefined), obj);
30
+ }
31
+ /**
32
+ * Replace all `{arg, plural, ...}` blocks in `s` using a brace-balanced scan.
33
+ */
34
+ function replacePluralBlocks(s, render) {
35
+ let i = 0;
36
+ let out = '';
37
+ while (i < s.length) {
38
+ const start = s.indexOf('{', i);
39
+ if (start === -1) {
40
+ out += s.slice(i);
41
+ break;
42
+ }
43
+ out += s.slice(i, start);
44
+ // Try to match the prefix "{arg, plural,"
45
+ const prefixMatch = /\{(\w+),\s*plural,\s*/y;
46
+ prefixMatch.lastIndex = start;
47
+ const m = prefixMatch.exec(s);
48
+ if (!m) {
49
+ // Not a plural block; copy '{' and continue scanning after it.
50
+ out += '{';
51
+ i = start + 1;
52
+ continue;
53
+ }
54
+ const arg = m[1];
55
+ let j = prefixMatch.lastIndex; // position after the matched prefix
56
+ // Find the matching closing '}' for the whole plural block with nesting.
57
+ let depth = 1;
58
+ while (j < s.length && depth > 0) {
59
+ const ch = s.charAt(j++);
60
+ if (ch === '{')
61
+ depth++;
62
+ else if (ch === '}')
63
+ depth--;
64
+ }
65
+ if (depth !== 0) {
66
+ // Unbalanced; fall back to literal copy of the unmatched segment.
67
+ out += s.slice(start, j);
68
+ i = j;
69
+ continue;
70
+ }
71
+ // Body is the contents between prefix end and the final '}'.
72
+ const body = s.slice(prefixMatch.lastIndex, j - 1);
73
+ const rendered = render(arg, body);
74
+ out += rendered;
75
+ i = j; // continue after the closing brace
76
+ }
77
+ return out;
78
+ }
79
+ /** Parse a simple ICU plural clause body: e.g. `one {A} other {B} =0 {C}` */
80
+ function parsePluralBody(body) {
81
+ const map = {};
82
+ // Allow balanced inner {...} in the value (non-greedy over balanced chunks is complex;
83
+ // we approximate by accepting any sequence without unbalanced braces at top level).
84
+ const re = /(one|other|=\d+)\s*\{((?:[^{}]|\{[^{}]*\})*)\}/g;
85
+ let m;
86
+ while ((m = re.exec(body)) !== null) {
87
+ const key = m[1];
88
+ const val = (m[2] ?? '');
89
+ map[key] = val;
90
+ }
91
+ return map;
92
+ }
93
+
94
+ /**
95
+ * @packageDocumentation
96
+ * Core runtime i18n primitives.
97
+ * Keep this surface minimal and stable.
98
+ */
99
+ /**
100
+ * Lightweight ICU-style formatter used internally by the Angular service.
101
+ * Exposed as @experimental for advanced users or custom integrations.
102
+ * @experimental
103
+ */
104
+
105
+ /**
106
+ * Generated bundle index. Do not edit.
107
+ */
108
+
109
+ export { formatIcu };
110
+ //# sourceMappingURL=ngx-runtime-i18n-core.mjs.map
@@ -0,0 +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\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 out = out.replace(/\\{(\\w+)\\}/g, (_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/** Parse a simple ICU plural clause body: e.g. `one {A} other {B} =0 {C}` */\nfunction parsePluralBody(body: string): Record<string, string> {\n const map: Record<string, string> = {};\n // Allow balanced inner {...} in the value (non-greedy over balanced chunks is complex;\n // we approximate by accepting any sequence without unbalanced braces at top level).\n const re = /(one|other|=\\d+)\\s*\\{((?:[^{}]|\\{[^{}]*\\})*)\\}/g;\n let m: RegExpExecArray | null;\n while ((m = re.exec(body)) !== null) {\n const key = m[1] as string;\n const val = (m[2] ?? '') as string;\n map[key] = val;\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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"}
package/index.d.ts ADDED
@@ -0,0 +1,50 @@
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
+ * The set of allowed languages. `setLang()` will guard against values not in this list.
25
+ * Use BCP-47 tags (e.g., "en", "en-GB", "hi").
26
+ */
27
+ supported: string[];
28
+ /**
29
+ * Fetch a catalog at runtime. Must be idempotent and cancellable via AbortSignal.
30
+ * - Runs on the client only (the server should seed catalogs via TransferState).
31
+ * - Return a plain object (parsed JSON).
32
+ */
33
+ fetchCatalog: (lang: string, signal?: AbortSignal) => Promise<Catalog>;
34
+ /**
35
+ * Missing key handler. When omitted, the key itself is returned (useful in dev).
36
+ * Use to log or to inject a visible marker.
37
+ */
38
+ onMissingKey?: (key: string) => string;
39
+ }
40
+
41
+ /**
42
+ * Very small ICU-like formatter with interpolation and plural basics.
43
+ * Used by the Angular service and pipe. Replace with full ICU in a later minor.
44
+ * @experimental
45
+ */
46
+
47
+ declare function formatIcu(_lang: string, key: string, cat: Catalog, params?: Record<string, unknown>, onMissingKey?: (k: string) => string): string;
48
+
49
+ export { formatIcu };
50
+ export type { Catalog, RuntimeI18nConfig };
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@ngx-runtime-i18n/core",
3
+ "version": "1.0.0",
4
+ "description": "Runtime i18n core with ICU-lite formatting",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "keywords": [
8
+ "angular",
9
+ "i18n",
10
+ "internationalization",
11
+ "icu",
12
+ "runtime",
13
+ "ssr",
14
+ "signals"
15
+ ],
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/AshwinSathian/ngx-runtime-i18n.git"
19
+ },
20
+ "license": "MIT",
21
+ "exports": {
22
+ ".": {
23
+ "import": "./fesm2022/ngx-runtime-i18n-core.mjs",
24
+ "types": "./index.d.ts",
25
+ "default": "./fesm2022/ngx-runtime-i18n-core.mjs"
26
+ },
27
+ "./package.json": {
28
+ "default": "./package.json"
29
+ }
30
+ },
31
+ "publishConfig": {
32
+ "access": "public",
33
+ "provenance": false
34
+ },
35
+ "engines": {
36
+ "node": ">=18"
37
+ },
38
+ "module": "fesm2022/ngx-runtime-i18n-core.mjs",
39
+ "typings": "index.d.ts",
40
+ "dependencies": {
41
+ "tslib": "^2.3.0"
42
+ }
43
+ }