@likolabs/i18nmd 0.2.0 → 0.3.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/CHANGELOG.md +15 -0
- package/README.haw.md +421 -0
- package/README.md +43 -6
- package/bin/i18nmd.mjs +31 -5
- package/lib/catalog.mjs +13 -8
- package/lib/compiler.mjs +156 -36
- package/lib/interop.mjs +4 -4
- package/lib/llm.mjs +1 -1
- package/lib/lock.mjs +18 -8
- package/lib/messages.mjs +12 -5
- package/lib/plural-rules.json +1 -0
- package/lib/runtime.mjs +24 -6
- package/package.json +8 -3
package/lib/runtime.mjs
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/** Intl.NumberFormat options for a {n, number, style} style. */
|
|
2
|
+
export function numberOptions(style = '') {
|
|
3
|
+
return style === 'integer' ? { maximumFractionDigits: 0 } : style === 'percent' ? { style: 'percent' } : style.startsWith('::currency/') ? { style: 'currency', currency: style.slice(11) } : {};
|
|
4
|
+
}
|
|
5
|
+
|
|
1
6
|
/**
|
|
2
7
|
* Format a compiled catalog using the host's Intl implementation.
|
|
3
8
|
* Messages without tags return strings. Messages with rich-text tags call the
|
|
@@ -6,8 +11,11 @@
|
|
|
6
11
|
* onError receives problems such as an unknown token or a missing value. By
|
|
7
12
|
* default they are logged and the message degrades instead of crashing the UI;
|
|
8
13
|
* pass `error => { throw error; }` to fail fast in tests.
|
|
14
|
+
* formatters supplies the project's own argument types: { length: (value,
|
|
15
|
+
* language) => string } formats {len, length}. It is read on each call, so
|
|
16
|
+
* formatters may be added to the object later.
|
|
9
17
|
*/
|
|
10
|
-
export function createI18n(catalog, { onError = error => console.error(`i18nmd: ${error.message}`) } = {}) {
|
|
18
|
+
export function createI18n(catalog, { onError = error => console.error(`i18nmd: ${error.message}`), formatters = {} } = {}) {
|
|
11
19
|
function value(values, name) {
|
|
12
20
|
if (!Object.hasOwn(values, name)) { onError(new Error(`Missing value for {${name}}.`)); return undefined; }
|
|
13
21
|
return values[name];
|
|
@@ -33,8 +41,13 @@ export function createI18n(catalog, { onError = error => console.error(`i18nmd:
|
|
|
33
41
|
if (v == null) { onError(new Error(`Missing value for {${n.name}}.`)); push(`{${n.name}}`); continue; }
|
|
34
42
|
if (['number', 'plural', 'selectordinal'].includes(n.type) && (typeof v !== 'number' || !Number.isFinite(v))) { onError(new Error(`{${n.name}} must be a finite number.`)); push(String(v)); continue; }
|
|
35
43
|
if (n.type === 'number') {
|
|
36
|
-
|
|
37
|
-
|
|
44
|
+
push(new Intl.NumberFormat(locale, numberOptions(n.style)).format(v)); continue;
|
|
45
|
+
}
|
|
46
|
+
if (n.type === 'format') {
|
|
47
|
+
const format = Object.hasOwn(formatters, n.format) ? formatters[n.format] : undefined;
|
|
48
|
+
if (typeof format !== 'function') { onError(new Error(`No formatter for {${n.name}, ${n.format}}.`)); push(String(v)); continue; }
|
|
49
|
+
try { push(String(format(v, locale))); } catch (error) { onError(error); push(String(v)); }
|
|
50
|
+
continue;
|
|
38
51
|
}
|
|
39
52
|
if (n.type === 'date' || n.type === 'time') {
|
|
40
53
|
const date = v instanceof Date ? v : typeof v === 'number' ? new Date(v) : null;
|
|
@@ -48,7 +61,7 @@ export function createI18n(catalog, { onError = error => console.error(`i18nmd:
|
|
|
48
61
|
}
|
|
49
62
|
return parts.length === 0 ? '' : parts.length === 1 && typeof parts[0] === 'string' ? parts[0] : parts;
|
|
50
63
|
}
|
|
51
|
-
|
|
64
|
+
function i18nmd(token, language = catalog.source, values = {}) {
|
|
52
65
|
if (!Object.hasOwn(catalog.messages, token)) { onError(new Error(`Unknown translation token: ${token}`)); return token; }
|
|
53
66
|
let requested = language;
|
|
54
67
|
try { requested = Intl.getCanonicalLocales(language)[0]; } catch { /* Custom language names use source formatting. */ }
|
|
@@ -60,7 +73,10 @@ export function createI18n(catalog, { onError = error => console.error(`i18nmd:
|
|
|
60
73
|
try { Intl.getCanonicalLocales(formattingLocale); } catch { formattingLocale = catalog.source; }
|
|
61
74
|
try { Intl.getCanonicalLocales(formattingLocale); } catch { formattingLocale = 'en'; }
|
|
62
75
|
return render(entry[languageUsed], formattingLocale, values || {});
|
|
63
|
-
}
|
|
76
|
+
}
|
|
77
|
+
/** Whether the catalog has token, for text written after the last compile. */
|
|
78
|
+
i18nmd.has = token => Object.hasOwn(catalog.messages, token);
|
|
79
|
+
return i18nmd;
|
|
64
80
|
}
|
|
65
81
|
|
|
66
82
|
/** The catalog language that best matches a requested one: exact, then base language. */
|
|
@@ -138,7 +154,8 @@ export function createLanguage(languages, source, { load, divisions = [], storag
|
|
|
138
154
|
* The generated i18nmd function over a language's store. Divisions are
|
|
139
155
|
* properties: i18nmd.support("contact_us") is the token support.contact_us.
|
|
140
156
|
* i18nmd.in("fr") translates into a given (loaded) language instead, for
|
|
141
|
-
* servers, emails and tests.
|
|
157
|
+
* servers, emails and tests. i18nmd.has(token) (or i18nmd.support.has("contact_us"))
|
|
158
|
+
* says whether a token is among the messages loaded. divisions maps each property to [segment, children].
|
|
142
159
|
*/
|
|
143
160
|
export function createTranslator(language, divisions = {}, options = {}) {
|
|
144
161
|
const { store } = language;
|
|
@@ -151,6 +168,7 @@ export function createTranslator(language, divisions = {}, options = {}) {
|
|
|
151
168
|
} });
|
|
152
169
|
const build = (prefix, tree, fixed) => {
|
|
153
170
|
const scoped = (token, values) => translate(prefix + token, fixed ?? language.getLanguage(), values);
|
|
171
|
+
Object.defineProperty(scoped, 'has', { value: token => translate.has(prefix + token) });
|
|
154
172
|
for (const [property, [segment, children]] of Object.entries(tree)) {
|
|
155
173
|
Object.defineProperty(scoped, property, { value: build(`${prefix}${segment}.`, children, fixed), enumerable: true });
|
|
156
174
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@likolabs/i18nmd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Interface strings in Markdown, one file per language. A compiler checks every language and generates typed code your app imports.",
|
|
5
5
|
"author": "Liko Labs (https://likolabs.com)",
|
|
6
6
|
"type": "module",
|
|
@@ -19,13 +19,15 @@
|
|
|
19
19
|
"lib",
|
|
20
20
|
"scripts/python_catalog.py",
|
|
21
21
|
"README.md",
|
|
22
|
+
"README.haw.md",
|
|
22
23
|
"LICENSE",
|
|
23
24
|
"PROMPT.md",
|
|
24
25
|
"CHANGELOG.md"
|
|
25
26
|
],
|
|
26
27
|
"scripts": {
|
|
27
28
|
"test": "node --test tests/*.test.mjs",
|
|
28
|
-
"check": "npm test"
|
|
29
|
+
"check": "npm test",
|
|
30
|
+
"plural-rules": "node scripts/plural-rules.mjs"
|
|
29
31
|
},
|
|
30
32
|
"engines": {
|
|
31
33
|
"node": ">=22"
|
|
@@ -55,5 +57,8 @@
|
|
|
55
57
|
"typescript",
|
|
56
58
|
"llm"
|
|
57
59
|
],
|
|
58
|
-
"license": "MIT"
|
|
60
|
+
"license": "MIT",
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"cldr-core": "48.2.0"
|
|
63
|
+
}
|
|
59
64
|
}
|