@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/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.1
4
+
5
+ - **Fixed:** the compiler loaded its plural rules through `node:module`, so bundling it for the browser (as the i18n.md demo does) failed. The rules are now a plain JavaScript module.
6
+
7
+ ## 0.3.0
8
+
9
+ - **Fixed: Python plurals now match JavaScript.** The Python module used a table of whole numbers, so decimals always took `other` (French `1.5` is `one`) and large numbers could pick the wrong form (French `1000000` is `many`). It now evaluates CLDR's plural rules, the data behind `Intl.PluralRules`, with the same operands and rounding, and rounds numbers half away from zero as `Intl` does. Python numbers are read as doubles, like JavaScript's. A new cross-runtime test renders plurals, ordinals and every number style in 20 languages over 160 values and requires identical output.
10
+ - **Currencies in Python:** `{n, number, ::currency/EUR}` now writes the currency as `Intl` does.
11
+ - **App formatters:** `{len, length}` passes a value to a function the app supplies, for text ICU can't write, such as fractional inches. Declare the names in `i18nmd.lock.json` (`"formatters": ["length"]`), then `registerFormatter` (generated JavaScript), `createI18n(catalog, { formatters })`, or `register_formatter` (Python).
12
+ - **`has(token)`:** `i18nmd.has(token)`, `i18nmd.<division>.has(token)` and Python's `has(token)` say whether a token is compiled. A division can no longer be called `has`.
13
+ - **Fixed:** `import --out translations/<division>` wrote its own lock inside the division with unprefixed tokens, so `status` from the root never saw its translations. It now records them in the tree's lock.
14
+ - **`import --merge`** adds source-language messages to an existing source file without touching other languages or the lock. The README describes how to translate sentences stored in a database this way.
15
+
16
+ ## 0.2.1
17
+
18
+ - **Python numbers follow each language:** grouping and decimal marks, Indian-style grouping, minus signs and percent signs come from `Intl` at compile time, so `{n, number}` and `#` match the JavaScript runtime.
19
+ - **README in ʻōlelo Hawaiʻi:** [README.haw.md](README.haw.md).
20
+ - **Fixed:** a translation edited before its source text changed was counted as updated for the new text, so the change went unnoticed. Now an edit only counts once `sync` (or `status`) has seen the source change; if both changed in between, the translation is marked stale for review. Existing lock files keep working.
21
+
3
22
  ## 0.2.0
4
23
 
5
24
  - **Static websites.** `extract` reads `.html` pages: it marks each sentence, the title and description, image text and form labels with a `data-i18n` attribute and leaves the English in place, and takes `/* i18n */` strings from inline scripts. `render <site> --out dist --url …` writes a copy of every page in every language (`/haw/…`) with `lang`, `hreflang` links, `og:url`, adjusted relative links, and a language switcher wherever a page has `<nav data-i18n-languages>`. No JavaScript needed.
package/README.haw.md ADDED
@@ -0,0 +1,421 @@
1
+ # i18n.md
2
+
3
+ [English](README.md) · **ʻŌlelo Hawaiʻi**
4
+
5
+ **[E hoʻāʻo i ka hōʻike ola ma i18n.md](https://i18n.md)** · Hana ʻia e **[Liko Labs](https://likolabs.com)** ma Hilo, Hawaiʻi
6
+
7
+ E mālama i nā kikokikona o kāu polokalamu (app) a me kāu kahua pūnaewele ma Markdown: hoʻokahi waihona no kēlā me kēia ʻōlelo, i hiki i nā mea unuhi, nā mea nānā, a me nā LLM ke heluhelu a hoʻoponopono pololei. Nānā ka polokalamu hōʻuluʻulu (compiler) i nā ʻōlelo a pau, a hana i ke code i hoʻopaʻa ʻia nā ʻano (typed code) e hoʻohana ʻia e kāu polokalamu.
8
+
9
+ Ua hana ʻo [Liko Labs](https://likolabs.com) iā i18nmd a hāʻawi manuahi aku me ka manaʻolana e maʻalahi ka hoʻolako ʻana i ka ʻōlelo Hawaiʻi no kēlā me kēia ʻoihana a me nā hui kaiāulu ma Hawaiʻi. ʻEkolu wale nō kauoha no ke kahua pūnaewele ma ka ʻōlelo Pelekānia a me ka ʻōlelo Hawaiʻi, ʻo `extract`, `--add haw` a me `render` (e nānā i [Nā kahua pūnaewele HTML](#nā-kahua-pūnaewele-html)), a noho kēlā me kēia māmalaʻōlelo Hawaiʻi i loko o kekahi waihona maʻalahi e hiki ai i ka mea mākaukau ke nānā a hoʻoponopono. No ke kōkua i ka lawe ʻana mai i ka ʻōlelo Hawaiʻi i kāu kahua pūnaewele a i ʻole polokalamu, e kelepona aku iā [Liko Labs](https://likolabs.com).
10
+
11
+ ````md
12
+ # Français
13
+
14
+ ## cart_items
15
+
16
+ Context: Item count in the cart header.
17
+
18
+ ```icu
19
+ {count, plural, one {# article} other {# articles}}
20
+ ```
21
+ ````
22
+
23
+ ```tsx
24
+ <p>{i18nmd('cart_items', { count })}</p>
25
+ ```
26
+
27
+ ʻO nā waihona ke kumu o ka ʻoiaʻiʻo. ʻIke ʻia nā loli i loko o nā pull request, a hiki i kekahi ke hāʻawi i kekahi waihona i ke kanaka a i ʻole ka LLM e unuhi. ʻO nā mea a pau a i18nmd e hana ai, he loli ia i ia mau waihona, a i ʻole he code i hana ʻia mai ia mau waihona.
28
+
29
+ - [Hoʻokomo](#hoʻokomo)
30
+ - [Hoʻomaka wikiwiki](#hoʻomaka-wikiwiki)
31
+ - [Nā kahua pūnaewele HTML](#nā-kahua-pūnaewele-html)
32
+ - [Nā waihona ʻōlelo](#nā-waihona-ʻōlelo)
33
+ - [Nā māhele](#nā-māhele)
34
+ - [Ka hoʻohana ʻana i ke code i hana ʻia](#ka-hoʻohana-ʻana-i-ke-code-i-hana-ʻia)
35
+ - [Ka huki ʻana i nā kikokikona mai kāu code](#ka-huki-ʻana-i-nā-kikokikona-mai-kāu-code)
36
+ - [Ka unuhi ʻana me ka LLM](#ka-unuhi-ʻana-me-ka-llm)
37
+ - [Ka mālama ʻana i nā unuhina i ke au](#ka-mālama-ʻana-i-nā-unuhina-i-ke-au)
38
+ - [Python](#python)
39
+ - [Nā ʻano waihona ʻē aʻe](#nā-ʻano-waihona-ʻē-aʻe)
40
+ - [Nā kauoha](#nā-kauoha)
41
+ - [Nā palena o kēia manawa](#nā-palena-o-kēia-manawa)
42
+
43
+ ## Hoʻokomo
44
+
45
+ Pono ʻo i18nmd iā Node.js 22 a i ʻole ka mana hou aku.
46
+
47
+ ```sh
48
+ npm install --save-dev @likolabs/i18nmd
49
+ npx i18nmd --version
50
+ ```
51
+
52
+ ## Hoʻomaka wikiwiki
53
+
54
+ **1. E huki i nā kikokikona** mai kekahi polokalamu React a i ʻole JSX/TSX. Me `--in-place`, kākau hou ʻo i18nmd i kāu mau waihona code, no laila e commit mua a nānā i nā loli.
55
+
56
+ ```sh
57
+ npx i18nmd extract src --in-place
58
+ ```
59
+
60
+ Pae kāu mau kikokikona i `translations/i18n-en.md` (e hoʻohana iā `--source it` inā ma ka ʻōlelo ʻĪkālia kāu polokalamu), a kāhea kāu code i nā unuhina ma kahi o ka kikokikona:
61
+
62
+ ```tsx
63
+ // before
64
+ <p>Welcome back, {user.name}!</p>
65
+ // after
66
+ import { i18nmd } from '../i18n/i18n';
67
+ <p>{i18nmd('welcome_back', { name: user.name })}</p>
68
+ ```
69
+
70
+ Hōʻike ka mea huki (extractor) i nā mea ʻaʻole hiki iā ia ke hoʻololi, e like me ke kikokikona i kūkulu ʻia ma ke code a i ʻole nā helu e pono ai ke ʻano lehulehu (plural). Aʻo ʻo [ka paipai huki](PROMPT.md) (extraction prompt) i kekahi coding agent pehea e hoʻopau ai i ka hana.
71
+
72
+ **2. E hoʻohui i nā ʻōlelo.** E noi i kekahi LLM ma o ka CLI, a i ʻole e kākau iho i nā waihona iā ʻoe iho:
73
+
74
+ ```sh
75
+ export ANTHROPIC_API_KEY=… # or OPENAI_API_KEY, or I18NMD_* (see below)
76
+ npx i18nmd --add french --add german
77
+ ```
78
+
79
+ **3. E hōʻuluʻulu (compile)** i `src/i18n`. E hoʻohui iā ia i kāu kūkulu ʻana (build) i hoʻohana kēlā me kēia kūkulu ʻana i nā unuhina hou loa:
80
+
81
+ ```sh
82
+ npx i18nmd compile
83
+ ```
84
+
85
+ ```json
86
+ { "scripts": { "prebuild": "i18nmd compile" } }
87
+ ```
88
+
89
+ **4. E ʻae i ka mea heluhelu e koho i ka ʻōlelo.** Unuhi nā kāhea i ka ʻōlelo o kēia manawa, ʻo ia hoʻi ka ʻōlelo o ka polokalamu kele pūnaewele (browser) o ka mea heluhelu ma ka hoʻomaka ʻana:
90
+
91
+ ```tsx
92
+ import { useSyncExternalStore } from 'react';
93
+ import { languages, getLanguage, setLanguage, onLanguageChange } from './i18n/language.mjs';
94
+
95
+ export function LanguagePicker() {
96
+ const language = useSyncExternalStore(onLanguageChange, getLanguage);
97
+ return (
98
+ <select value={language} onChange={e => void setLanguage(e.target.value)}>
99
+ {Object.entries(languages).map(([code, name]) => <option key={code} value={code} lang={code}>{name}</option>)}
100
+ </select>
101
+ );
102
+ }
103
+ ```
104
+
105
+ **5. E mālama i ke au.** Ma hope o ka hoʻoponopono ʻana i ke kikokikona kumu, hōʻike ʻo `npx i18nmd status` i nā mea e pono ai ka unuhi, a hoʻopiha ʻo `npx i18nmd translate` iā lākou.
106
+
107
+ ## Nā kahua pūnaewele HTML
108
+
109
+ ʻAʻole pono ke JavaScript no ke kahua pūnaewele i kūkulu ʻia me nā ʻaoʻao HTML. Kākau ʻo i18nmd i kope o kēlā me kēia ʻaoʻao ma kēlā me kēia ʻōlelo:
110
+
111
+ ```sh
112
+ npx i18nmd extract site --in-place # site/*.html → translations/i18n-en.md
113
+ npx i18nmd --add haw # or write translations/i18n-haw.md yourself
114
+ npx i18nmd render site --out dist --url https://example.com # dist/ and dist/haw/
115
+ ```
116
+
117
+ Kau ʻo `extract` i ka hōʻailona `data-i18n` ma kēlā me kēia ʻāpana kikokikona, a waiho i ka ʻōlelo Pelekānia ma kona wahi, no laila wehe a hoʻoponopono ʻia ka ʻaoʻao e like me ma mua:
118
+
119
+ ```html
120
+ <h1 data-i18n="from_first_leaf_to_full_grown">From first leaf to <em class="red">full grown.</em></h1>
121
+ ```
122
+
123
+ Mālama ka memo i ka māmalaʻōlelo holoʻokoʻa, me kona mau hōʻailona HTML i loko (inline markup) ma ke ʻano he mau tag: `From first leaf to <em>full grown.</em>`. Hoʻoneʻe ka mea unuhi i ka tag; noho kona class a me kona mau ʻano ʻē aʻe (attributes) ma ka ʻaoʻao. Lawe pū ʻo `extract` i ka `<title>` o ka ʻaoʻao, nā hōʻailona `<meta>` no ka wehewehe a me ka hōʻike loulou, nā attribute `alt`, `title`, `placeholder` a me `aria-label`, a me nā kikokikona i loko o nā script i kaha ʻia me `/* i18n */`:
124
+
125
+ ```js
126
+ statusEl.textContent = /* i18n */ 'Sending…';
127
+ ```
128
+
129
+ Lele ʻo ia ma luna o nā helu wahi (address) e like me `example.com`, a me nā mea a pau i loko o kekahi element me `translate="no"`, ka attribute maʻamau no nā inoa a me ke code. Papa inoa ʻo ia i ke kikokikona ʻaʻole hiki iā ia ke hoʻonoho, e like me nā huaʻōlelo ma ka ʻaoʻao o kekahi element poloka, a me nā kikokikona script e like ana me nā mea e heluhelu ai ke kanaka.
130
+
131
+ Noho ka ʻōlelo Pelekānia i kāu HTML. E hoʻoponopono i kekahi ʻaoʻao, e holo hou iā `extract`, a hahai ka waihona ʻōlelo kumu; lilo nā mana o ia kikokikona ma nā ʻōlelo ʻē aʻe i **kahiko** (stale), no ka `translate` e hoʻohou ai.
132
+
133
+ Kākau ʻo `render` i ka ʻōlelo kumu ma kahi o nā ʻaoʻao, a me kēlā me kēia ʻōlelo ʻē aʻe i loko o kekahi waihona i kapa ʻia ma kona inoa, `/haw/`, me ke kope pū ʻana i nā mea ʻē aʻe a pau o ke kahua pūnaewele. Loaʻa i kēlā me kēia kope:
134
+
135
+ - ke kikokikona i unuhi ʻia, a noho ke kikokikona i unuhi ʻole ʻia ma ka ʻōlelo kumu
136
+ - `<html lang>`, a me `dir="rtl"` no nā ʻōlelo i kākau ʻia mai ka ʻākau a i ka hema
137
+ - `<link rel="alternate" hreflang>` i ke kope o kēlā me kēia ʻōlelo, i hōʻiliʻili nā mīkini huli i kēlā me kēia
138
+ - me `--url`, kuhikuhi nā loulou `og:url` a me canonical i ia kope
139
+ - hoʻoponopono ʻia nā loulou pili (relative links) no ka waihona o ka ʻōlelo
140
+
141
+ E kau iā `<nav data-i18n-languages></nav>` ma kekahi wahi o ka ʻaoʻao, a hoʻopiha ʻo `render` iā ia me ka loulou i kēlā me kēia ʻōlelo, i kapa ʻia ma ia ʻōlelo. E hoʻolaha (deploy) iā `dist/`.
142
+
143
+ ## Nā waihona ʻōlelo
144
+
145
+ Paʻa ʻo `translations/i18n-fr.md` i ka ʻōlelo Palani wale nō. Hāʻawi ka inoa waihona i ke code ʻōlelo (`fr`, `pt-BR`, a i ʻole kekahi inoa i haku ʻia e like me `pirate`). Kapa ke poʻo inoa mua i ka ʻōlelo ma ia ʻōlelo nō, a he inoa memo (token) kēlā me kēia poʻo inoa `##`:
146
+
147
+ ````md
148
+ # Français
149
+
150
+ ## cart_items
151
+
152
+ Context: Item count in the cart header.
153
+
154
+ ```icu
155
+ {count, plural, one {# article} other {# articles}}
156
+ ```
157
+
158
+ ## greeting
159
+
160
+ Optional: a
161
+
162
+ ```icu
163
+ Bonjour {name} !
164
+ ```
165
+ ````
166
+
167
+ - Wehewehe ʻo **Context** (pōʻaiapili) i kahi e ʻike ʻia ai ka kikokikona a me nā mea e pono ai ka mea unuhi: ke ʻano o ka leo, nā palena o ka lōʻihi, nā mea ʻaʻole e unuhi ʻia.
168
+ - Papa inoa ʻo **Optional** i nā hakahaka (placeholders) e hiki ke haʻalele ʻia e ka unuhina, e like me ka ʻatikala Pelekānia (`{a}` no "a" a i ʻole "an") ʻaʻole pono i nā ʻōlelo ʻē aʻe.
169
+ - ʻO ka memo he [ICU MessageFormat](https://unicode-org.github.io/icu/userguide/format_parse/messages/): nā hakahaka me ka inoa `{name}`, `plural`, `selectordinal`, `select`, `number` (`integer`, `percent`, `::currency/EUR`), `date` a me `time` me kekahi ʻano, a me nā tag e like me `<b>…</b>`.
170
+ - He ʻokina wale nō ka ʻokina, a pēlā nō ka apostrophe. No ka kākau ʻana i ka kahakaha (brace) maoli, a i ʻole `<` ma mua o kekahi hua palapala, e hoʻopuni iā ia me ka apostrophe: `'{'`, `'<'`.
171
+
172
+ ʻO ia ke ʻano holoʻokoʻa. Noho ka moʻokāki ma ka ʻaoʻao o nā waihona ma `i18nmd.lock.json`, e hoʻopaʻa ana i ka ʻōlelo kumu a me nā unuhina e kūpono ana i ke au.
173
+
174
+ Hoʻohana kēlā me kēia waihona ʻōlelo i nā inoa memo a me nā hakahaka like me ke kumu. Nānā ʻo `i18nmd check` iā lākou a pau: hōʻike ʻia ka unuhina me ka hakahaka nalo a i ʻole ʻike ʻole ʻia, ka plural haki, a i ʻole ka tag kūlike ʻole, a hoʻohana ke kūkulu ʻana i ke kikokikona kumu a hoʻoponopono ʻia. ʻAʻole hāʻule ke kūkulu ʻana ma muli o nā unuhina nalo a kahiko paha.
175
+
176
+ ## Nā māhele
177
+
178
+ Hiki i ka polokalamu nui ke hoʻokaʻawale i kona mau kikokikona i nā māhele, hoʻokahi waihona (directory) no kēlā me kēia, ma ke ʻano e kūpono ai i ka poʻe e hoʻoponopono ana:
179
+
180
+ ```
181
+ translations/
182
+ i18nmd.lock.json
183
+ account/i18n-en.md account/i18n-fr.md
184
+ ui/i18n-en.md ui/i18n-fr.md
185
+ marketing/i18n-en.md marketing/i18n-fr.md
186
+ marketing/landing/i18n-en.md
187
+ ```
188
+
189
+ He waihona kumu a me nā unuhina ko kēlā me kēia māhele, a hoʻokumu ʻia nā inoa memo ma kona ala. ʻO `## hero` i loko o `marketing/i18n-en.md` ka inoa memo `marketing.hero`:
190
+
191
+ ```tsx
192
+ import { i18nmd } from './i18n/marketing';
193
+ import './i18n/marketing.landing';
194
+ import './i18n/knowledge-hub';
195
+
196
+ i18nmd.marketing('hero') // or i18nmd('marketing.hero')
197
+ i18nmd.marketing.landing('save') // marketing/landing/
198
+ i18nmd.knowledgeHub('search') // knowledge-hub/ becomes camelCase
199
+ ```
200
+
201
+ Mālama nā waihona i nā inoa pōkole, no laila hiki i nā māhele ʻelua ke loaʻa he `save`. E kapa i nā waihona māhele me nā hua palapala, nā helu, `_` a me `-`; hōʻole ʻia nā inoa i loaʻa i nā function a pau (`name`, `length`, `call`, …) a me `in`.
202
+
203
+ Hoʻokaʻawale pū nā māhele i kāu pūʻolo (bundle): hōʻuluʻulu ʻia kēlā me kēia i kona module ponoʻī, a hoʻouna kāu bundler iā ia me ke code e hoʻohana ana iā ia. E kau i nā kikokikona e pono ai kēlā me kēia ʻaoʻao, e like me ke komo ʻana (sign-in) a me nā ʻaoʻao hoʻouka, i loko o kekahi māhele liʻiliʻi ponoʻī, i lawe ka hoʻoili mua i kēlā mau mea wale nō.
204
+
205
+ Pili nā kauoha ma `translations/` i nā māhele a pau, a hōʻike ʻo `status` i ka holomua ma kēlā me kēia māhele. E hāʻawi i ka waihona o kekahi māhele e hana ma luna ona wale nō, e like me `i18nmd translate translations/marketing`. Noho ka lock ma luna a kaʻana like ʻia.
206
+
207
+ ## Ka hoʻohana ʻana i ke code i hana ʻia
208
+
209
+ ```sh
210
+ npx i18nmd compile # translations/ → src/i18n
211
+ npx i18nmd compile --out lib/i18n # elsewhere
212
+ ```
213
+
214
+ ### Ke kāhea ʻana i nā unuhina
215
+
216
+ ```tsx
217
+ import { i18nmd } from './i18n/i18n';
218
+
219
+ i18nmd('cart_items', { count: 3 }) // top-level token
220
+ i18nmd.ui('save') // a division's token
221
+ i18nmd.in('fr').ui('save') // a fixed language
222
+ ```
223
+
224
+ Nānā ʻo TypeScript i kēlā me kēia inoa memo a me kona mau waiwai: he hewa compile ka waiwai nalo, ke ʻano hewa, a i ʻole ka inoa memo mai kekahi māhele ʻē. ʻAe pū ka hakahaka maʻamau iā `null` a i ʻole `undefined` a hōʻike ʻole i kekahi mea, e like me JSX.
225
+
226
+ Lawe mai (import) ke code e kāhea ana i kekahi māhele iā `i18nmd` mai ka module o ia māhele (`./i18n/ui`). Lawe mai pū ka waihona e kāhea ana i ka māhele ʻelua i kona module, `import './i18n/marketing'`. Kākau ʻo `extract` i kēia mau import. Hāʻule ʻo `npx i18nmd check --in src` me ka laina e hoʻohui ai inā nalo kekahi, a hoʻohui ʻo `--fix` iā ia.
227
+
228
+ Unuhi ʻia nā kikokikona i loko o ke code e holo hoʻokahi wale nō, e like me ka constant ma ka pae module, i ia manawa nō. E hoʻopuni iā lākou i loko o kekahi function i hiki aku ka loli ʻōlelo iā lākou:
229
+
230
+ ```ts
231
+ export const steps = () => [i18nmd.ui('measure'), i18nmd.ui('cut')];
232
+ ```
233
+
234
+ ### Ka ʻōlelo o kēia manawa
235
+
236
+ Hoʻomaka ka ʻōlelo o kēia manawa me ka koho mua o ka mea heluhelu, a laila nā ʻōlelo o kona polokalamu kele pūnaewele, a laila ka ʻōlelo kumu. Mālama ʻo `language.mjs` iā ia a paʻa ʻole i nā memo, no laila hiki i kāu code komo (entry code) ke lawe mai iā ia me ke kaumaha ʻole:
237
+
238
+ | Export | Hana |
239
+ | --- | --- |
240
+ | `languages` | Kēlā me kēia code ʻōlelo a me kona inoa ma ia ʻōlelo, no ka papa koho. |
241
+ | `getLanguage()` | Ka ʻōlelo o kēia manawa. |
242
+ | `setLanguage(code)` | Hoʻouka i ka ʻōlelo, hoʻololi iā ia a hoʻomanaʻo. ʻAe ʻia ka like kokoke: koho ʻo `pt` iā `pt-BR`. |
243
+ | `onLanguageChange(listener)` | Kāhea i ka listener ma hope o kēlā me kēia loli; hoʻihoʻi i ka function e hoʻōki ai. |
244
+ | `ready` | Hoʻoholo ʻia ke hoʻouka ʻia ka ʻōlelo o ka mea heluhelu. |
245
+ | `loadLanguage(code)` | Hoʻouka i kekahi ʻōlelo no `i18nmd.in(code)`, ma ke kikowaena (server) a i ʻole nā hoʻāʻo. |
246
+
247
+ He ʻāpana ponoʻī kēlā me kēia unuhina, no laila hoʻoili ka mea heluhelu i kāna ʻōlelo wale nō. E hōʻike ma hope o `ready` i ʻike ka mea heluhelu i koho i ka ʻōlelo Palani i ka ʻōlelo Palani mai ka hōʻike mua loa; no ka ʻōlelo kumu, hoʻoholo koke ʻia:
248
+
249
+ ```tsx
250
+ import { ready, getLanguage, onLanguageChange } from './i18n/language.mjs';
251
+
252
+ function Localized() {
253
+ const language = useSyncExternalStore(onLanguageChange, getLanguage);
254
+ return <App key={language} />; // re-render everything in the new language
255
+ }
256
+ ready.then(() => createRoot(root).render(<Localized />));
257
+ ```
258
+
259
+ Huli ka ʻimi ʻōlelo i ke code pololei (`fr-CA`), a laila ka ʻōlelo kumu o ia code (`fr`), a laila ka ʻōlelo kumu o ka polokalamu. I loko o ka memo, hoʻi ka unuhina nalo i ke kikokikona kumu.
260
+
261
+ ### Ke kikokikona waiwai (rich text)
262
+
263
+ Noho nā hōʻailona i loko o ka māmalaʻōlelo, i hiki i nā mea unuhi ke hoʻonohonoho hou iā lākou:
264
+
265
+ ```icu
266
+ Read <link>the guide</link> before <b>{date, date, long}</b>.
267
+ ```
268
+
269
+ ```tsx
270
+ i18nmd('read_the_guide', {
271
+ date,
272
+ link: chunks => <a key="link" href="/guide">{chunks}</a>,
273
+ b: chunks => <b key="b">{chunks}</b>,
274
+ })
275
+ ```
276
+
277
+ Hoʻihoʻi nā memo me nā tag i kekahi array, a hōʻike pololei ʻo React iā ia.
278
+
279
+ ### Nā mea a `compile` e kākau ai
280
+
281
+ | Waihona | Paʻa |
282
+ | --- | --- |
283
+ | `i18n.ts` | Nā ʻano (types), `i18nmd`, a me nā inoa memo ma ka pae kiʻekiʻe. |
284
+ | `<division>.ts` | Nā memo ʻōlelo kumu o hoʻokahi māhele. |
285
+ | `language.mjs` | Ka ʻōlelo o kēia manawa a me nā mea hoʻouka, me ka memo ʻole. |
286
+ | `languages/<code>.mjs` | Nā memo a pau ma hoʻokahi ʻōlelo ʻē. |
287
+ | `runtime.mjs` | Ka mea hoʻonohonoho (formatter), e hoʻohana ana i ka `Intl` o ka polokalamu kele pūnaewele no nā plural, nā helu a me nā lā. |
288
+
289
+ E commit iā lākou a i ʻole e hana iā lākou i kāu kūkulu ʻana; holoi ʻo `compile` i nā waihona āna i hana ai ma mua akā ʻaʻole e kākau hou. Kau ʻo `--eager` i nā memo a me nā ʻōlelo a pau i loko o `i18n.ts`, no nā kikowaena, nā hoʻāʻo a me nā polokalamu liʻiliʻi. Kākau ʻo `--target js` i nā waihona like ma JavaScript, kākau ʻo `--target json` i hoʻokahi waihona JSON o ke kikokikona memo, a haʻalele ʻo `--skip <division>` i nā māhele e hoʻohana ʻia e kekahi polokalamu ʻē.
290
+
291
+ ### Nā hewa i ka holo ʻana
292
+
293
+ ʻAʻole e hāʻule ka ʻaoʻao ma muli o ka inoa memo ʻike ʻole ʻia a i ʻole ka waiwai nalo. Kākau ka runtime iā ia i ka moʻolelo (log) a hōʻike i ka inoa memo a i ʻole `{placeholder}` ma kahi ona. Haʻi ka inoa memo mai kekahi māhele i lawe ʻole ʻia mai e ka ʻaoʻao i ka import e hoʻohui ai.
294
+
295
+ ## Ka huki ʻana i nā kikokikona mai kāu code
296
+
297
+ ```sh
298
+ npx i18nmd extract src --in-place # everything → translations/
299
+ npx i18nmd extract src/account src/routes.tsx --out translations/account --in-place
300
+ ```
301
+
302
+ Heluhelu ʻo `extract` i JavaScript a me TypeScript, me JSX a i ʻole me ka ʻole, a hoʻoneʻe i kēia mau mea i ka waihona ʻōlelo kumu:
303
+
304
+ - Ke kikokikona JSX, me ka mālama ʻana i kēlā me kēia māmalaʻōlelo holoʻokoʻa. Lilo nā waiwai i loko o ka māmalaʻōlelo i mau hakahaka me ka inoa: lilo ʻo `{formatLength(kerf)}` iā `{kerf}`, a lilo ʻo `{items.length}` iā `{itemsCount}`.
305
+ - Nā element i loko o ka laina e like me `<b>`, `<a href>` a me `<Link to>`, e lilo ana i mau tag.
306
+ - Nā huaʻōlelo i koho ʻia ma ke code: lilo ʻo `{busy ? "Saving…" : "Save"}` i ʻelua memo.
307
+ - Nā attribute i ʻike ʻia: `alt`, `title`, `placeholder`, `label`, `aria-label`, `aria-description`.
308
+ - Kēlā me kēia kikokikona i kaha ʻia me `/* i18n */`, a i ʻole `/* i18n:token_name */` e koho ai i kona inoa memo.
309
+
310
+ Waiho ʻo ia i nā helu, nā hōʻailona a me ke kikokikona hua palapala ʻole, a me nā mea ʻaʻole hiki iā ia ke hoʻololi me ka palekana; papa inoa ʻia kēlā mau mea me ko lākou waihona a me ka laina. Hōʻailona pū ʻo ia i nā helu e pono ai ke ʻano plural a me nā huaʻōlelo i kūkulu ʻia ma ke code e pono ai ke lilo i ICU `select`.
311
+
312
+ Hele mai nā inoa memo mai nā huaʻōlelo o ka memo, e like me `welcome_back`, a ʻaʻole loli ke hoʻoponopono ʻoe i ke kikokikona. Ke holo hou ʻia ʻo `extract`, mālama ʻia nā inoa memo a me nā unuhina a pau, a hoʻohui ʻia nā kikokikona hou wale nō. Me ka ʻole o `--in-place`, kākau ʻo ia i nā kope i hoʻololi ʻia i `--dest` (ʻo `.i18n/src` ma ka maʻamau) a waiho i kāu mau waihona kumu. Huki ʻo `--out translations/<division>` i loko o kekahi māhele.
313
+
314
+ Mālama ka mea huki i ka hana mīkini. He paipai ʻo [PROMPT.md](PROMPT.md) no kekahi coding agent e hana i ke koena: nā kikokikona i loko o nā waihona `.ts` maʻamau a me nā object, nā māmalaʻōlelo i kūkulu ʻia ma ke code, a me ka nānā ʻana i kēlā me kēia ʻaoʻao.
315
+
316
+ ## Ka unuhi ʻana me ka LLM
317
+
318
+ ```sh
319
+ npx i18nmd --add french # also: Français, fr, pt-BR, "brazilian portuguese", klingon
320
+ npx i18nmd --add pirate # anything unrecognized becomes a custom style
321
+ npx i18nmd --top 10 # the 10 most widely spoken languages (i18nmd languages lists them)
322
+ npx i18nmd translate # fill every missing or outdated translation
323
+ npx i18nmd translate --only fr,de --dry-run
324
+ ```
325
+
326
+ E hoʻonoho i kekahi o kēia:
327
+
328
+ ```sh
329
+ export ANTHROPIC_API_KEY=… # Claude
330
+ export OPENAI_API_KEY=… OPENAI_BASE_URL=… # OpenAI or any compatible API
331
+ export I18NMD_BASE_URL=… I18NMD_API_KEY=… I18NMD_MODEL=… # overrides both
332
+ ```
333
+
334
+ ʻIke ʻia ka mea hoʻolako (provider) mai ka URL a i ʻole ke kī; hoʻololi ʻo `--provider`, `--base-url`, `--model` a me `--batch` iā ia no kēlā me kēia holo.
335
+
336
+ Hele nā memo ma nā pūʻulu, me ka laina pōʻaiapili o kēlā me kēia, nā unuhina i loaʻa ma ke ʻano he papa huaʻōlelo, a me ka unuhina mua o ka memo i loli. Nānā ʻia kēlā me kēia pane e like me ka unuhina i kākau ʻia e ka lima. Hoʻāʻo hou ʻia ka memo i hāʻule hoʻokahi manawa me ka hewa, a hōʻike ʻia nā mea e hāʻule mau ana a hoʻi i ka ʻōlelo kumu. Mālama ʻia ka holomua ma hope o kēlā me kēia pūʻulu, no laila ʻaʻohe mea e nalo ke ʻoki ʻia ka holo ʻana.
337
+
338
+ ## Ka mālama ʻana i nā unuhina i ke au
339
+
340
+ ```sh
341
+ npx i18nmd status
342
+ ```
343
+
344
+ ```
345
+ English (en): 1847 tokens, source
346
+ Français (fr): 1790/1847 done, 40 missing, 17 stale
347
+ ```
348
+
349
+ Lilo ka unuhina i **kahiko** (stale) ke loli kona kikokikona kumu ma hope o kona kākau ʻia ʻana. E hoʻoponopono i ka unuhina, a helu ʻo i18nmd iā ia he mea i hoʻohou ʻia; ʻaʻohe hōʻailona e holoi ai. Hoʻopiha ʻo `translate` i nā unuhina nalo a kahiko.
350
+
351
+ | Kauoha | Hana |
352
+ | --- | --- |
353
+ | `status` | Ka holomua no kēlā me kēia ʻōlelo a me kēlā me kēia māhele. |
354
+ | `check` | Nānā i kēlā me kēia waihona. Hāʻule pū ʻo `--strict` ma nā mea nalo a kahiko paha, no CI ma mua o ka hoʻopuka ʻana. Nānā ʻo `--in src` inā lawe mai kāu code i nā māhele āna e kāhea ai, a papa inoa i nā inoa memo i kāhea ʻole ʻia. |
355
+ | `sync` | Hoʻohou iā `i18nmd.lock.json` a holoi i nā inoa memo ʻaʻole i loaʻa hou ma ke kumu mai nā ʻōlelo ʻē aʻe. |
356
+ | `rename <old> <new> --in src` | Kapa hou i kekahi inoa memo ma nā ʻōlelo a pau, ka lock, a me nā kāhea i kāu code. |
357
+ | `join --out all.md` | Kākau i nā ʻōlelo a pau i hoʻokahi waihona Markdown, e nānā ai ma ka ʻaoʻao kekahi i kekahi a i ʻole e hāʻawi ai i ka LLM. |
358
+ | `split all.md --out translations` | Hoʻokaʻawale hou i ka waihona i hui ʻia i nā waihona ʻōlelo. |
359
+
360
+ ʻImi nā kauoha iā `translations/`, `i18n/`, `locales/` a i ʻole ka waihona o kēia manawa iā lākou iho; e hāʻawi i ke ala a i ʻole `--dir` no nā wahi ʻē aʻe. ʻAe pū kēlā me kēia kauoha i ka waihona i hui ʻia ma kahi o ka waihona (directory).
361
+
362
+ ## Python
363
+
364
+ ```sh
365
+ npx i18nmd compile translations/server --target python --out app/i18n
366
+ ```
367
+
368
+ ```python
369
+ from app.i18n.i18n import i18nmd, template, LANGS
370
+
371
+ i18nmd("server.parts", "de", n=3) # "3 Teile"
372
+ ```
373
+
374
+ ʻAʻohe mea e pili ai (dependencies) ka module Python. Hoʻonohonoho ʻo ia i nā hakahaka, nā plural, nā helu kaʻina (ordinal), nā select a me nā helu, me nā lula plural a me ke ʻano helu o kēlā me kēia ʻōlelo (`1.234,5` ma ka ʻōlelo Kelemānia, `12,34,567` ma ka ʻōlelo Hindi) i lawe ʻia mai ka `Intl` o kāu Node.js i ka manawa compile. Hoʻihoʻi ʻo `template(token, language)` i ka memo me nā hakahaka i hōʻike ʻia e like me `{name}`. Hoʻololi ʻo `import-python <module.py> --out translations` i kekahi papa unuhina Python i loaʻa.
375
+
376
+ ## Nā ʻano waihona ʻē aʻe
377
+
378
+ ```sh
379
+ npx i18nmd import locales/*/translation.json --from i18next
380
+ npx i18nmd import lang/en.json lang/fr.json --from formatjs
381
+ npx i18nmd export --to next-intl --out messages # messages/fr.json
382
+ npx i18nmd export --to formatjs --out lang # lang/fr.json, for react-intl
383
+ npx i18nmd export --to i18next --out locales # locales/fr/translation.json
384
+ ```
385
+
386
+ Hoʻohana mua ʻo FormatJS a me next-intl iā ICU, no laila ʻaʻohe mea e nalo i ka hoʻololi ʻana, a lilo nā wehewehe FormatJS i mau laina pōʻaiapili. No i18next, lilo ʻo `{{name}}` iā `{name}`, lilo nā hope plural i nā ICU plural, a lilo nā kī i hoʻopūnana ʻia i nā inoa memo me ke kiko. Mālama ʻia nā memo ʻaʻole hiki iā i18next ke hōʻike ma ke ʻano he kikokikona ICU no ka plugin i18next-icu, me ka ʻōlelo aʻo. Hiki iā ʻoe ke hoʻoponopono ma i18nmd a hoʻouna i nā mea a kāu polokalamu e hoʻouka nei.
387
+
388
+ ## Nā kauoha
389
+
390
+ | Kauoha | |
391
+ | --- | --- |
392
+ | `render <site>` | `--out dist`, `--url https://example.com` |
393
+ | `extract <src…>` | `--in-place`, `--out translations/<division>`, `--source en`, `--runtime src/i18n/i18n`, `--dest .i18n/src`, `--locale-expr locale` (calls use `i18nmd.in(locale)`) |
394
+ | `compile` | `--out src/i18n`, `--target ts\|js\|json\|python`, `--eager`, `--skip <division,…>` |
395
+ | `--add <language>`, `--top <n>`, `translate` | `--only fr,de`, `--dry-run`, `--provider`, `--base-url`, `--model`, `--batch 40` |
396
+ | `status`, `check`, `sync` | `--strict`, `--in src`, `--fix`, `--skip` |
397
+ | `rename <old> <new>` | `--in src` |
398
+ | `join`, `split` | `--out` |
399
+ | `import`, `export`, `import-python` | `--from`, `--to`, `--out` |
400
+ | `languages` | Ka papa inoa a `--top` e hoʻohana ai. |
401
+
402
+ Lawe kēlā me kēia kauoha iā `--dir`, a me `--source` e hoʻololi ai i ka ʻōlelo kumu i hoʻopaʻa ʻia ma ka lock. Papa inoa ʻo `npx i18nmd --help` i nā mea a pau.
403
+
404
+ ## Nā palena o kēia manawa
405
+
406
+ - Heluhelu ka mea huki i HTML, JavaScript a me TypeScript. Pono nā kikokikona i loko o nā object (`{ label: "Save" }`) a me nā waihona `.ts` maʻamau iā `/* i18n */` a i ʻole ka paipai huki.
407
+ - Hoʻouka ka hoʻololi ʻana i ka ʻōlelo i ka ʻōlelo holoʻokoʻa i ka manawa hoʻokahi, ʻaʻole ma kēlā me kēia māhele.
408
+ - Kākau ka pahuhopu Python i nā lā e like me ka mea i hāʻawi ʻia, a me ke kikokikona o ka tag me ka hōʻailona ʻole, ke hāʻawi ʻole ʻoe i kekahi function nona.
409
+
410
+ ## Ka hoʻomohala ʻana
411
+
412
+ ```sh
413
+ npm ci --ignore-scripts
414
+ npm test
415
+ ```
416
+
417
+ Hoʻohana nā hoʻāʻo i kekahi mea pani kūloko no nā API LLM ʻelua, no laila ʻaʻole pono lākou i nā kī a ʻaʻole hoʻopili i ka pūnaewele.
418
+
419
+ ## Laikini
420
+
421
+ MIT © [Liko Labs](https://likolabs.com)
package/README.md CHANGED
@@ -1,7 +1,11 @@
1
1
  # i18n.md
2
2
 
3
+ **[Try the live demo at i18n.md](https://i18n.md)** · Made by **[Liko Labs](https://likolabs.com)** in Hilo, Hawaiʻi · [ʻŌlelo Hawaiʻi](README.haw.md)
4
+
3
5
  Keep your interface strings in Markdown, for apps and for plain HTML sites: one file per language, readable and editable by translators, reviewers and LLMs. A compiler checks every language and generates typed code your app imports.
4
6
 
7
+ [Liko Labs](https://likolabs.com) created i18nmd and makes it freely available in the hope of making ʻōlelo Hawaiʻi easy to offer for any business or community group in Hawaiʻi. A website in English and Hawaiian is three commands, `extract`, `--add haw` and `render` (see [Static websites](#static-websites)), and every Hawaiian sentence stays in a plain file a fluent speaker can review and correct. For help bringing ʻōlelo Hawaiʻi to your site or app, contact [Liko Labs](https://likolabs.com).
8
+
5
9
  ````md
6
10
  # Français
7
11
 
@@ -29,10 +33,11 @@ The files are the source of truth. They diff in pull requests, and anyone can ha
29
33
  - [Extracting strings from your code](#extracting-strings-from-your-code)
30
34
  - [Translating with an LLM](#translating-with-an-llm)
31
35
  - [Keeping translations current](#keeping-translations-current)
36
+ - [Text from a database](#text-from-a-database)
32
37
  - [Python](#python)
33
38
  - [Other formats and libraries](#other-formats-and-libraries)
34
39
  - [Command reference](#command-reference)
35
- - [Limits in 0.1](#limits-in-01)
40
+ - [Current limits](#current-limits)
36
41
 
37
42
  ## Install
38
43
 
@@ -161,6 +166,7 @@ Bonjour {name} !
161
166
  - **Context** explains where a string appears and anything a translator needs: tone, length limits, what not to translate.
162
167
  - **Optional** lists placeholders a translation may leave out, such as an English article (`{a}` for "a" or "an") that other languages don't need.
163
168
  - The message is [ICU MessageFormat](https://unicode-org.github.io/icu/userguide/format_parse/messages/): named placeholders `{name}`, `plural`, `selectordinal`, `select`, `number` (`integer`, `percent`, `::currency/EUR`), `date` and `time` with a style, and tags such as `<b>…</b>`.
169
+ - **App formatters** cover values ICU can't write, such as `1-1/2"`: `{len, length}` passes `len` to a function your app supplies. Declare the names once in `i18nmd.lock.json`, `"formatters": ["length"]`, and `check` and `compile` reject any other type. Translators may move the placeholder but not change its type; put unit notes in the context line. The same value can also choose a plural branch, `{len, plural, …}`.
164
170
  - An apostrophe is just an apostrophe. To write a literal brace, or `<` before a letter, quote it: `'{'`, `'<'`.
165
171
 
166
172
  That is the whole format. Bookkeeping lives beside the files in `i18nmd.lock.json`, which records the source language and which translations are current.
@@ -270,6 +276,18 @@ i18nmd('read_the_guide', {
270
276
 
271
277
  Messages with tags return an array, which React renders directly.
272
278
 
279
+ ### App formatters
280
+
281
+ Register each formatter the lock declares before rendering. It receives the value and the language being written, and returns text:
282
+
283
+ ```ts
284
+ import { registerFormatter } from './i18n/i18n';
285
+
286
+ registerFormatter('length', (inches, language) => language === 'en' ? toFractionalInches(inches) : `${Math.round(inches * 25.4)} mm`);
287
+ ```
288
+
289
+ i18nmd ships no formatters; your app decides what `length` means. A missing or failing formatter is reported like other runtime errors and the raw value is shown.
290
+
273
291
  ### What compile writes
274
292
 
275
293
  | File | Holds |
@@ -286,6 +304,8 @@ Commit these or generate them in your build; compile removes files it generated
286
304
 
287
305
  An unknown token or a missing value never crashes the page. The runtime logs it and shows the token name or `{placeholder}` instead. A token from a division the page never imported says which import to add.
288
306
 
307
+ `i18nmd.has('cart_items')`, or `i18nmd.ui.has('save')` for a division, says whether a token is in the compiled catalog without logging anything, so code can tell text added since the last compile from a mistake.
308
+
289
309
  ## Extracting strings from your code
290
310
 
291
311
  ```sh
@@ -353,6 +373,20 @@ A translation is **stale** when its source text changed after it was written. Ed
353
373
 
354
374
  Commands find `translations/`, `i18n/`, `locales/` or the current directory on their own; pass a path or `--dir` for anywhere else. Every command also accepts a joined file in place of a directory.
355
375
 
376
+ ## Text from a database
377
+
378
+ Sentences stored in a database, such as narration written by people or agents, are translated through the same files. i18nmd has no database adapter and never translates at request time; the Markdown files and the lock are the cache, so each sentence is translated once.
379
+
380
+ 1. A job in your app writes new or changed sentences as FormatJS JSON, `{"step_42": {"defaultMessage": "Cut the board to {len, length}.", "description": "Narration for step 42"}}`, and merges them into a division's source file:
381
+
382
+ ```sh
383
+ npx i18nmd import sentences.json --from formatjs --merge --out translations/procedures
384
+ ```
385
+
386
+ `--merge` appends new tokens and replaces changed text. It removes nothing and leaves other languages and the lock alone, so `status` lists new sentences as missing and changed ones as stale.
387
+ 2. `npx i18nmd translate` fills the other languages, and `npx i18nmd compile` builds the catalog.
388
+ 3. Until a sentence is translated, the runtime shows it in the source language. Until it is compiled, `i18nmd.has(token)` is false and the app shows its own copy of the text.
389
+
356
390
  ## Python
357
391
 
358
392
  ```sh
@@ -365,7 +399,9 @@ from app.i18n.i18n import i18nmd, template, LANGS
365
399
  i18nmd("server.parts", "de", n=3) # "3 Teile"
366
400
  ```
367
401
 
368
- The Python module has no dependencies. It formats placeholders, plurals, ordinals, selects and numbers, with plural rules taken from your Node.js's `Intl` at compile time. `template(token, language)` returns the message with placeholders shown as `{name}`. `import-python <module.py> --out translations` converts an existing literal Python translation table.
402
+ The Python module has no dependencies, and it renders every message exactly as the JavaScript runtime does: placeholders, plurals, ordinals, selects and numbers, including decimals such as French `1,5`, which takes the `one` form. Plural rules come from CLDR, the data `Intl` uses, and each language's number style (`1.234,5` in German, `12,34,567` in Hindi, currencies) from your Node.js's `Intl` at compile time. Python numbers are read as doubles, as JavaScript reads them. A test renders a spread of languages and values in both runtimes and requires identical output. That parity holds against CLDR 48 (Node.js 26); a browser with older CLDR data can differ from the Python rules in rare cases, which is a difference in `Intl` versions, not a bug in i18nmd.
403
+
404
+ `template(token, language)` returns the message with placeholders shown as `{name}`, `has(token)` says whether a token is in the module, and `register_formatter("length", fn)` supplies an app formatter, where `fn(value, language)` returns text. Keep each formatter the same in both languages; i18nmd can only promise identical output for its own formatting. `import-python <module.py> --out translations` converts an existing literal Python translation table.
369
405
 
370
406
  ## Other formats and libraries
371
407
 
@@ -390,16 +426,17 @@ FormatJS and next-intl already use ICU, so conversion is lossless, and FormatJS
390
426
  | `status`, `check`, `sync` | `--strict`, `--in src`, `--fix`, `--skip` |
391
427
  | `rename <old> <new>` | `--in src` |
392
428
  | `join`, `split` | `--out` |
393
- | `import`, `export`, `import-python` | `--from`, `--to`, `--out` |
429
+ | `import`, `export`, `import-python` | `--from`, `--to`, `--out`, `--merge` |
394
430
  | `languages` | The ranked list `--top` uses. |
395
431
 
396
432
  Every command takes `--dir`, and `--source` to override the source language recorded in the lock. `npx i18nmd --help` lists everything.
397
433
 
398
- ## Limits in 0.1
434
+ ## Current limits
399
435
 
400
436
  - The extractor reads HTML, JavaScript and TypeScript. Strings in object properties (`{ label: "Save" }`) and plain `.ts` files need `/* i18n */` or the extraction prompt.
401
437
  - Switching language loads that whole language at once, not per division.
402
- - The Python target ignores tags and formats numbers without locale grouping; dates are passed through as given.
438
+ - The Python target writes dates as given, and a tag's text without its markup unless you pass a function for it.
439
+ - App formatters are yours to keep identical in JavaScript and Python.
403
440
 
404
441
  ## Development
405
442
 
@@ -412,4 +449,4 @@ The tests use a local stand-in for both LLM APIs, so they need no keys and make
412
449
 
413
450
  ## License
414
451
 
415
- MIT
452
+ MIT © [Liko Labs](https://likolabs.com)
package/bin/i18nmd.mjs CHANGED
@@ -41,6 +41,8 @@ Build
41
41
 
42
42
  Other tools
43
43
  import <files.json...> --from ${FORMATS.join('|')} [--out translations] [--source en]
44
+ --merge Add source-language messages to the source file in
45
+ --out, keeping its tokens, translations and the lock.
44
46
  export --to ${FORMATS.join('|')} --out <dir>
45
47
  import-python <module.py> --out <dir>
46
48
  languages The ranked language list used by --top.
@@ -122,8 +124,8 @@ async function open(input, options) {
122
124
  const strict = !!options.strict;
123
125
  const raw = directory ? await readLanguageFiles(input) : { [input]: await readFile(input, 'utf8') };
124
126
  const catalog = directory
125
- ? parseLanguageFiles(raw, { source: options.source || lock.source, syntax: options.syntax || 'icu', strict, prefix: await divisionPrefix(input, lockFile) })
126
- : parseCatalog(raw[input], { filename: input, syntax: options.syntax, allowIncomplete: !strict, lenient: !strict });
127
+ ? parseLanguageFiles(raw, { source: options.source || lock.source, syntax: options.syntax || 'icu', strict, prefix: await divisionPrefix(input, lockFile), formatters: lock.formatters })
128
+ : parseCatalog(raw[input], { filename: input, syntax: options.syntax, allowIncomplete: !strict, lenient: !strict, formatters: lock.formatters });
127
129
  catalog.orphans ||= {};
128
130
  catalog.divisions ||= [''];
129
131
  let queue = Promise.resolve();
@@ -476,6 +478,29 @@ async function main() {
476
478
  if (!options.from) throw new Error(`Choose the format with --from ${FORMATS.join('|')}.`);
477
479
  const out = options.out || 'translations';
478
480
  const warnings = [];
481
+ // --merge adds source-language messages to the source file in --out: new tokens
482
+ // are appended, changed text replaces the old, and nothing is removed. Other
483
+ // languages and the lock are left alone, so status shows what to translate.
484
+ if (options.merge) {
485
+ const lock = await readLock((await findLock(out)).file);
486
+ const source = options.source || lock.source || 'en';
487
+ const file = path.join(out, `i18n-${source}.md`);
488
+ const catalog = await exists(file) ? parseCatalog(await readFile(file, 'utf8'), { filename: file, locale: source, formatters: lock.formatters }) : { title: 'Translations', source, syntax: 'icu', languages: { [source]: resolveLanguage(source).name }, messages: [] };
489
+ const index = new Map(catalog.messages.map(m => [m.key, m]));
490
+ let added = 0, changed = 0;
491
+ for (const input of positional) {
492
+ for (const m of importMessages(JSON.parse(await readFile(input, 'utf8')), options.from, warnings, lock.formatters)) {
493
+ const existing = index.get(m.key);
494
+ if (!existing) { const message = { key: m.key, context: m.context, optional: [], translations: { [source]: m.text } }; catalog.messages.push(message); index.set(m.key, message); added++; continue; }
495
+ if (existing.translations[source] !== m.text) { existing.translations[source] = m.text; changed++; }
496
+ if (m.context) existing.context = m.context;
497
+ }
498
+ }
499
+ warnings.forEach(w => console.warn(`i18nmd: ${w}`));
500
+ if (!added && !changed) { console.log(`${file} already has every message.`); return; }
501
+ await save(file, serializeCatalog(catalog, { locale: source }));
502
+ console.log(`Added ${plural(added, 'token')} and updated ${plural(changed, 'token')} in ${file}. Run i18nmd translate to fill the other languages.`); return;
503
+ }
479
504
  const byLocale = new Map();
480
505
  const files = positional.map(file => ({ file, ...localeFromJsonPath(file) }));
481
506
  const namespaces = new Set(files.map(f => f.namespace).filter(Boolean));
@@ -506,9 +531,10 @@ async function main() {
506
531
  if (await exists(file)) throw new Error(`${file} exists; import into an empty directory.`);
507
532
  }
508
533
  for (const locale of Object.keys(languages)) await save(path.join(out, `i18n-${locale}.md`), serializeCatalog(catalog, { locale, allowIncomplete: true }));
509
- const lock = { source, translations: {} };
510
- syncLock(catalog, lock);
511
- await save(lockPathFor(out, true), serializeLock(lock));
534
+ // The lock that covers out, so a division shares the tree's lock and token names.
535
+ const ws = await open(out, { source });
536
+ syncLock(ws.catalog, ws.lock, { partial: ws.partial });
537
+ await ws.saveLock();
512
538
  [...warnings, ...(catalog.warnings || [])].forEach(w => console.warn(`i18nmd: ${w}`));
513
539
  console.log(`Imported ${plural(messages.length, 'token')} in ${plural(Object.keys(languages).length, 'language')} into ${out}.`); return;
514
540
  }