@likolabs/i18nmd 0.2.0 → 0.3.1

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/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
- const options = n.style === 'integer' ? { maximumFractionDigits: 0 } : n.style === 'percent' ? { style: 'percent' } : n.style.startsWith('::currency/') ? { style: 'currency', currency: n.style.slice(11) } : {};
37
- push(new Intl.NumberFormat(locale, options).format(v)); continue;
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
- return function i18nmd(token, language = catalog.source, values = {}) {
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. divisions maps each property to [segment, children].
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.2.0",
3
+ "version": "0.3.1",
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
  }