@ultimat3/i18n 2.0.0 → 3.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/CLAUDE.md +8 -0
- package/package.json +2 -2
- package/src/catalog.ts +9 -2
- package/src/translator.ts +8 -2
package/CLAUDE.md
CHANGED
|
@@ -29,6 +29,14 @@ Imported by every package that renders a string.
|
|
|
29
29
|
for. Same reach `catalog.ts` shuts off by nesting into null-prototype nodes; never reintroduce
|
|
30
30
|
either. The `interpolate` fast path must test **both** braces: `}}` un-escapes with no `{` in
|
|
31
31
|
sight, and a `{`-only check gave one escape two meanings.
|
|
32
|
+
- **A catalog is read through `Object.hasOwn`, never a raw index, and every catalog this package
|
|
33
|
+
builds is `Object.create(null)`** — `flattenCatalog`, `mergeCatalogs` and `nestCatalog` all are.
|
|
34
|
+
On a `{}` catalog `catalog['valueOf']` resolved to the INHERITED function, so `t('valueOf')`
|
|
35
|
+
threw inside `interpolate`, `t('constructor')` returned a function through a signature typed
|
|
36
|
+
`string`, and `isMiss(t('__proto__'))` threw on an object — all three reachable wherever a key
|
|
37
|
+
travels as data (`t(row.labelKey)`). Both halves are load-bearing: the guard in `translator.ts`
|
|
38
|
+
makes it true of a catalog this package did not build, the null prototype makes `__proto__` an
|
|
39
|
+
ordinary key instead of one the setter silently swallows. Never reintroduce either.
|
|
32
40
|
- Plural selection is `Intl.PluralRules`. Never `count === 1`. Variants are underscore suffixes on
|
|
33
41
|
the leaf — a CLDR category (`_zero _one _two _few _many _other`), or `n` / `n_plural` as the
|
|
34
42
|
two-form shortcut; pair `n_one` with `n_other`, never with `n_plural`. Never a nested
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/i18n",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Dependency-free translator, catalog flattening, locale negotiation and loud missing-key rendering",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -31,6 +31,6 @@
|
|
|
31
31
|
"test": "bun test"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@ultimat3/core": "
|
|
34
|
+
"@ultimat3/core": "3.0.0"
|
|
35
35
|
}
|
|
36
36
|
}
|
package/src/catalog.ts
CHANGED
|
@@ -16,9 +16,14 @@ const KEY_SEGMENT = /^[A-Za-z0-9_-]+$/;
|
|
|
16
16
|
/**
|
|
17
17
|
* Depth-first flatten. Throws `X_CATALOG_INVALID` on a non-string leaf (arrays and
|
|
18
18
|
* numbers are the two mistakes translators actually make) or a duplicate flat key.
|
|
19
|
+
*
|
|
20
|
+
* `Object.create(null)`, for the reason `nestCatalog` states below: a catalog is untrusted input
|
|
21
|
+
* read off disk. On a `{}` literal `out['__proto__'] = 'Hello'` hits the `Object.prototype` SETTER
|
|
22
|
+
* and the key vanishes silently, and every other member of `Object.prototype` reads back as
|
|
23
|
+
* present when it is not.
|
|
19
24
|
*/
|
|
20
25
|
export function flattenCatalog(source: NestedCatalog, prefix = ''): Catalog {
|
|
21
|
-
const flat
|
|
26
|
+
const flat = Object.create(null) as Record<string, string>;
|
|
22
27
|
walk(source, prefix, flat);
|
|
23
28
|
return flat;
|
|
24
29
|
}
|
|
@@ -89,7 +94,9 @@ export function nestCatalog(catalog: Catalog): NestedCatalog {
|
|
|
89
94
|
* app can override `errors.notFound.title` without forking the framework catalog.
|
|
90
95
|
*/
|
|
91
96
|
export function mergeCatalogs(...catalogs: readonly Catalog[]): Catalog {
|
|
92
|
-
|
|
97
|
+
// Null-prototyped like every catalog this package builds — a merge must not hand back an
|
|
98
|
+
// object on which `catalog['toString']` reads as a function.
|
|
99
|
+
const merged = Object.create(null) as Record<string, string>;
|
|
93
100
|
for (const catalog of catalogs) {
|
|
94
101
|
for (const key of Object.keys(catalog)) {
|
|
95
102
|
const value = catalog[key];
|
package/src/translator.ts
CHANGED
|
@@ -64,7 +64,13 @@ export function createTranslator(catalog: Catalog, locale: Locale = DEFAULT_LOCA
|
|
|
64
64
|
};
|
|
65
65
|
|
|
66
66
|
const translate = (key: string, vars?: TranslateVars): string => {
|
|
67
|
-
|
|
67
|
+
// Through `hasExact`, never a raw index: a key travels as data (`t(row.labelKey)`), and on a
|
|
68
|
+
// `{}`-prototyped catalog `catalog['valueOf']` resolves to the INHERITED function instead of
|
|
69
|
+
// reading as absent — so `interpolate` threw on a non-string, `t('constructor')` returned a
|
|
70
|
+
// function through a signature typed `string`, and `isMiss(t('__proto__'))` threw on an object.
|
|
71
|
+
// Catalogs are null-prototyped now; this guard is what makes that true of any catalog.
|
|
72
|
+
const resolved = resolveKey(key, vars);
|
|
73
|
+
const template = hasExact(resolved) ? catalog[resolved] : undefined;
|
|
68
74
|
if (template === undefined) return `⟦${key}⟧`;
|
|
69
75
|
return vars === undefined ? template : interpolate(template, vars);
|
|
70
76
|
};
|
|
@@ -75,7 +81,7 @@ export function createTranslator(catalog: Catalog, locale: Locale = DEFAULT_LOCA
|
|
|
75
81
|
hasExact(`${key}_other`) ||
|
|
76
82
|
hasExact(`${key}_plural`) ||
|
|
77
83
|
hasExact(`${key}_one`),
|
|
78
|
-
raw: (key: string): string | undefined => catalog[key],
|
|
84
|
+
raw: (key: string): string | undefined => (hasExact(key) ? catalog[key] : undefined),
|
|
79
85
|
keys: (): string[] => Object.keys(catalog).sort(),
|
|
80
86
|
locale,
|
|
81
87
|
});
|