@ng-linguo/linguo 0.9.0 → 0.9.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.
Files changed (2) hide show
  1. package/README.md +388 -9
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,3 +1,9 @@
1
+ <!--
2
+ GENERATED FILE — do not edit.
3
+ Synced from the repository root README.md by tools/sync-readme.mjs.
4
+ Edit /README.md instead; this copy is regenerated on every linguo build.
5
+ -->
6
+
1
7
  <p align="center">
2
8
  <img
3
9
  src="https://raw.githubusercontent.com/jmwierzbicki/linguo/main/apps/playground/public/linguo-logo.png"
@@ -6,20 +12,393 @@
6
12
  />
7
13
  </p>
8
14
 
9
- # @ng-linguo/linguo
15
+ # ng-linguo
16
+
17
+ **Signal-native internationalization for Angular.** A modern, complete i18n
18
+ toolkit for Angular 18+, built on SignalStore — an independent, from-scratch
19
+ alternative to `@ngx-translate/core` and Transloco, reactive by default with
20
+ zero RxJS plumbing in your components.
21
+
22
+ ```html
23
+ <!-- translators edit plain text; this renders a real Angular link -->
24
+ <p t="Read the [docs]documentation[/docs] to get started">
25
+ <ng-template tFor="docs" let-text><a routerLink="/docs">{{ text }}</a></ng-template>
26
+ </p>
27
+ ```
28
+
29
+ <p align="center">
30
+ <a href="https://jmwierzbicki.github.io/linguo/">
31
+ <img
32
+ src="https://raw.githubusercontent.com/jmwierzbicki/linguo/main/apps/playground/public/linguo-demo.gif"
33
+ alt="Switching languages in the ng-linguo playground — every example re-renders reactively"
34
+ width="800"
35
+ />
36
+ </a>
37
+ <br />
38
+ <em>Switch the language and every binding re-renders — no reload, no subscriptions.</em>
39
+ <br />
40
+ <strong><a href="https://jmwierzbicki.github.io/linguo/">▶ Try the live demo</a></strong>
41
+ </p>
42
+
43
+ > **Status:** pre-release (`0.9.x`) — published to npm and usable today. The
44
+ > runtime, the extraction CLI, and the full test suite are in place and green.
45
+ > APIs may still shift before `1.0`.
46
+
47
+ ## Why ng-linguo
48
+
49
+ **Writing code**
50
+
51
+ - **Signals, not subscriptions.** Translations are reactive through
52
+ [`@ngrx/signals`](https://ngrx.io/guide/signals) — switch language and the UI
53
+ updates on its own, with no `async` pipe and no manual `subscribe`/`unsubscribe`.
54
+ - **Three ways to translate, one for each job.** A `t` pipe for templates, a
55
+ `[t]` directive for elements and rich text, and `injectTranslate()` for
56
+ TypeScript — see [which to use](#which-api-should-i-use).
57
+ - **Zoneless-ready, SSR-friendly, tree-shakeable.** No `zone.js` dependency,
58
+ safe to render on the server, and the optional ICU and HTTP pieces live in
59
+ separate entry points so you only ship what you import.
60
+
61
+ **Writing translations**
62
+
63
+ - **English _is_ the key — no key files to maintain.** You write real English
64
+ in your components, and that text _is_ the translation key. There are no
65
+ `home.header.title` paths to invent, keep unique, and keep in sync, and
66
+ nothing opaque for a translator (or an LLM) to guess at — they always see a
67
+ full, meaningful sentence. For the rare clash, a `context` disambiguates
68
+ (`Play` in a game vs. a music player).
69
+ - **Translators never see HTML.** Named slots `[name]…[/name]` (a BBCode-like
70
+ syntax) bind to _your_ `<ng-template>`, so links, buttons, and bindings render
71
+ as real Angular while the translation file stays plain text. Translated text
72
+ is never inserted as HTML, so cross-site scripting (XSS) is impossible by design.
73
+ - **Correct grammar in every language (ICU MessageFormat 2, and MF1).** Real
74
+ plurals, `select`, and gendered text per locale — Polish gets four plural
75
+ forms, English gets two, all from one message.
76
+
77
+ **Shipping translations**
78
+
79
+ - **A real, additive extraction pipeline.** Extract your source strings to
80
+ standard gettext `.po` files (works with Crowdin / Lokalise / Phrase) and
81
+ compile them to runtime JSON. Re-running extraction is _additive_: new and
82
+ changed strings merge in while every existing translation is kept.
83
+ - **Add a language in seconds.** Add its code to the config and extract — with
84
+ an AI translator wired up, filling it in is a single command (or a couple of
85
+ clicks in the interactive CLI).
86
+ - **Translate with AI — your model, your key.** Copy a ready-made prompt into
87
+ any chat model, or point ng-linguo at a translator module that calls your own
88
+ provider. ng-linguo writes the prompt (it teaches the model your context,
89
+ slot tags, and plural rules) and merges the reply; your SDK and API key never
90
+ leave your machine.
91
+ - **Automatable in CI.** Every command runs non-interactively and is
92
+ deterministic, so extraction and compilation drop straight into a pipeline.
93
+
94
+ **Fast by default**
95
+
96
+ - The `t` pipe memoizes its result, `injectTranslate()` + `computed()` does zero
97
+ work per change-detection pass, and ICU messages are compiled once and cached.
98
+ See [Performance](#performance).
99
+
100
+ ## Install
101
+
102
+ ```bash
103
+ npm i @ng-linguo/linguo @ngrx/signals
104
+ ```
105
+
106
+ Requires **Angular 18+**. `@ngrx/signals` is a peer dependency — bring your own.
107
+
108
+ ## Getting started
109
+
110
+ The fastest path from an empty Angular app to a translated one. Steps 1–3 get
111
+ the runtime working; step 4 generates the real translation files.
112
+
113
+ ### 1. Configure the runtime
114
+
115
+ Add the providers to your `app.config.ts` (or `bootstrapApplication`). Pick a
116
+ loader — loading is explicit, so nothing is fetched during DI setup.
10
117
 
11
- A modern, complete i18n toolkit for Angular 21+, built on SignalStore.
118
+ Most apps load their translation JSON over HTTP:
12
119
 
13
120
  ```ts
14
121
  import { provideTranslate } from '@ng-linguo/linguo';
15
- import { provideIcu } from '@ng-linguo/linguo/icu';
16
122
  import { createHttpLoader } from '@ng-linguo/linguo/http';
123
+ import { provideIcu } from '@ng-linguo/linguo/icu';
124
+ import { provideHttpClient } from '@angular/common/http';
125
+
126
+ export const appConfig = {
127
+ providers: [
128
+ provideHttpClient(),
129
+ provideTranslate({
130
+ defaultLang: 'en', // required: reported before load, and the fallback
131
+ // optional: only matches a saved/browser language to one you ship.
132
+ // This is a runtime concern — separate from the CLI's linguo.config.json.
133
+ supportedLangs: ['en', 'pl', 'de'],
134
+ // factory form: the loader is built in DI, so it can use HttpClient.
135
+ // GETs /assets/i18n/<lang>.json by default.
136
+ loader: () => createHttpLoader(),
137
+ }),
138
+ provideIcu(), // optional — enables ICU MessageFormat (defaults to MF2)
139
+ ],
140
+ };
141
+ ```
142
+
143
+ Prefer to **bundle** translations (no network)? A loader is just an object with
144
+ a `load(lang)` method, so a static import works too:
145
+
146
+ ```ts
147
+ import en from './i18n/en.json';
148
+ import pl from './i18n/pl.json';
149
+
150
+ const dictionaries: Record<string, unknown> = { en, pl };
151
+
152
+ provideTranslate({
153
+ defaultLang: 'en',
154
+ loader: { load: (lang) => Promise.resolve(dictionaries[lang] ?? {}) },
155
+ });
156
+ ```
157
+
158
+ ### 2. Load a language at startup
159
+
160
+ The store never loads on its own. Call `restoreLang()` once, usually in your
161
+ root component. It picks the startup language for you —
162
+ **persisted choice → browser preference → `defaultLang`** — and loads it. Gate
163
+ your UI on the `isReady` signal to avoid a flash of untranslated content:
164
+
165
+ ```ts
166
+ import { Component, inject } from '@angular/core';
167
+ import { TranslateStore } from '@ng-linguo/linguo';
168
+
169
+ @Component({
170
+ selector: 'app-root',
171
+ template: `@if (store.isReady()) {
172
+ <router-outlet />
173
+ } @else {
174
+ <app-splash />
175
+ }`,
176
+ })
177
+ export class App {
178
+ protected readonly store = inject(TranslateStore);
179
+ constructor() {
180
+ void this.store.restoreLang(); // resolve + load the startup language
181
+ }
182
+ }
183
+ ```
184
+
185
+ The active language is saved to `localStorage` (key `ng-linguo.lang`), and the
186
+ browser's preferred language is used on the first visit — both **on by default**
187
+ and **SSR-safe** (no-ops on the server). Set `supportedLangs` so a stored or
188
+ browser value can be matched to a language you actually ship. To switch language
189
+ later, call `store.setLang('pl')` (which also saves the choice). The full set of
190
+ options — `persistSelectedLanguage`, `restoreSelectedLanguage`, `persistKey`,
191
+ `detectBrowserLanguage` — is in [Configuration](#configuration).
192
+
193
+ ### 3. Translate
194
+
195
+ In templates use the `t` pipe or the `[t]` directive; in TypeScript use
196
+ `injectTranslate()`.
197
+
198
+ ```html
199
+ <!-- plain text -->
200
+ {{ 'Save' | t }}
201
+
202
+ <!-- ICU placeholders & plurals -->
203
+ {{ 'Hello {$name}!' | t: { params: { name } } }}
204
+
205
+ <!-- context: same text, different translation -->
206
+ {{ 'Play' | t: { context: 'game' } }}
207
+
208
+ <!-- rich text: [tag] placeholders bound to your own templates (see the hero above) -->
209
+ <p t="[b]Warning:[/b] this cannot be undone">
210
+ <ng-template tFor="b" let-text><strong>{{ text }}</strong></ng-template>
211
+ </p>
212
+ ```
213
+
214
+ ```ts
215
+ import { injectTranslate } from '@ng-linguo/linguo';
216
+
217
+ const t = injectTranslate();
218
+
219
+ // Reactive and efficient: recomputes only when `name()` or the active language
220
+ // changes. Prefer this for frequently-updated or looped bindings.
221
+ readonly greeting = computed(() => t('Hello {$name}!', { params: { name: this.name() } }));
222
+ ```
223
+
224
+ Until you generate translation files (step 4), every string falls through to the
225
+ English you wrote, so the app is fully usable from the first line of code.
226
+
227
+ ### 4. Generate the translation files
228
+
229
+ The strings above are also your source catalog. Use the
230
+ [`@ng-linguo/extract`](#translation-workflow) CLI to collect them and produce the
231
+ JSON your loader serves:
232
+
233
+ ```bash
234
+ npx linguo-extract init --locales en,pl,de # one-time: create linguo.config.json
235
+ linguo-extract extract # scan source → en/pl/de .po catalogs
236
+ linguo-extract translate --all # fill missing entries with AI (optional)
237
+ linguo-extract compile # .po → runtime JSON
238
+ ```
239
+
240
+ That's the whole loop. See [Translation workflow](#translation-workflow) for the
241
+ interactive menu, adding languages, and translating by hand.
242
+
243
+ ### Which API should I use?
244
+
245
+ All three resolve the same translations; they differ in where they run and what
246
+ they can render.
247
+
248
+ | Use… | When | Notes |
249
+ | ------------------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
250
+ | `[t]` **directive** | Translating an element, **rich text with slots**, or hot lists | The most efficient option for the DOM. Re-renders via an `effect()` only when something changes. |
251
+ | `injectTranslate()` | TypeScript, or a binding read inside a `computed()` | Zero work per change-detection pass. Best for frequently-updated or looped bindings. Slots → text. |
252
+ | `t` **pipe** | Quick inline strings, attribute bindings | Convenient, but it's an **impure** pipe (see below). Slots degrade to plain text. |
253
+
254
+ > **A note on the `t` pipe.** Angular re-evaluates an impure pipe on _every_
255
+ > change-detection pass. The `t` pipe has to be impure to react to a language
256
+ > switch (a pure pipe only re-runs when its input reference changes), so it
257
+ > memoizes aggressively to stay cheap. That's perfectly fine for ordinary
258
+ > templates — but in a long `@for` list or a hot binding, prefer the `[t]`
259
+ > directive or `injectTranslate()` + `computed()`, which do no per-pass work.
260
+ > The directive is also the only option that renders slot tags as real DOM; the
261
+ > pipe and `injectTranslate()` return a string, so slots collapse to their text.
262
+
263
+ ### Performance
264
+
265
+ - **The `t` pipe is memoized.** It only re-translates when the key, `params`,
266
+ `context`, or language actually change — so passing a fresh `{ params: … }`
267
+ object on every render is just a quick equality check, not a re-format.
268
+ - **`injectTranslate()` + `computed()` does no per-pass work** — it recomputes
269
+ only when its signal inputs change. Reach for it on hot paths.
270
+ - **ICU messages are compiled once and cached** per `(format, locale, message)`,
271
+ so repeated formatting of the same pattern is a map lookup.
272
+
273
+ ## Translation workflow
274
+
275
+ `@ng-linguo/extract` is a pure-Node CLI (no Angular dependency, so it never
276
+ drags the framework into your tooling) that turns your source into translation
277
+ files and back. It reads a `linguo.config.json` — auto-discovered — listing your
278
+ locales and paths.
279
+
280
+ ```bash
281
+ linguo-extract init # create/edit linguo.config.json
282
+ linguo-extract extract # scan source → <locale>.po catalogs (additive)
283
+ linguo-extract translate # fill missing entries with AI (needs a translator)
284
+ linguo-extract compile # .po catalogs → runtime <locale>.json
285
+ ```
286
+
287
+ ### The interactive menu
288
+
289
+ New to the tool? Run it with **no command** to open a guided menu that walks
290
+ through every step — extract, compile, translate, run the full pipeline — and
291
+ includes a BIOS-style settings editor where each config field carries an
292
+ inline description:
293
+
294
+ ```bash
295
+ npx linguo-extract # guided menu (also creates/edits the config)
296
+ ```
297
+
298
+ Everything the menu does is also a flag-driven command, so you can graduate to
299
+ scripts whenever you like.
300
+
301
+ ### Extraction is additive
302
+
303
+ `extract` scans your `.ts` and `.html` for the `t` pipe, the `[t]` directive,
304
+ `injectTranslate()` calls, and `mark()`, then **merges** the results into your
305
+ existing `.po` catalogs. New strings are added, removed ones are dropped, and
306
+ **every translation you already have is preserved** — entries are matched by
307
+ their source text plus `context`. Re-running it is safe and idempotent.
308
+
309
+ (Need to keep a documentation sample or fixture out of the scan? Wrap it in
310
+ `linguo-ignore-start` / `linguo-ignore-end` comments.)
311
+
312
+ ### Adding a language
313
+
314
+ Adding a locale is a couple of steps — or a couple of clicks in the menu:
315
+
316
+ ```bash
317
+ linguo-extract init --locales en,pl,de,fr # add `fr` to the config
318
+ linguo-extract extract # seeds fr.po with the source strings
319
+ linguo-extract translate --locale fr # fill it in with AI…
320
+ # …or: linguo-extract copyprompt fr # …or copy a prompt for any chat model
321
+ linguo-extract compile # produce fr.json
322
+ ```
323
+
324
+ ### Translating with AI
325
+
326
+ Because the source strings are full English sentences (not opaque keys), an LLM
327
+ has all the context it needs. ng-linguo writes a self-contained prompt that
328
+ teaches the model your `context` notes, slot tags, and plural rules, and only
329
+ ever sends entries that are still missing. Two ways to run it:
330
+
331
+ - **Clipboard (no key, no config):** `linguo-extract copyprompt pl` copies the
332
+ prompt; paste it into any chat model and save the reply over `pl.po`.
333
+ - **Automatic:** point the `translator` config field at a small module that
334
+ calls your AI provider. ng-linguo builds the prompt and merges the reply; your
335
+ SDK and API key stay yours. See the
336
+ [`@ng-linguo/extract` README](https://github.com/jmwierzbicki/linguo/tree/main/packages/extract#readme)
337
+ for a copy-paste module (OpenAI, Anthropic, or any provider).
338
+
339
+ ### In CI
340
+
341
+ Every command runs non-interactively and deterministically, so the pipeline
342
+ drops into CI as-is:
343
+
344
+ ```bash
345
+ linguo-extract extract # fails the build if it errors; idempotent otherwise
346
+ linguo-extract translate --all # optional: fill any gaps (needs a translator)
347
+ linguo-extract compile
348
+ ```
349
+
350
+ `init` is scriptable too: `linguo-extract init --locales en,pl,de --out public/i18n`.
351
+
352
+ ## Configuration
353
+
354
+ ng-linguo has two independent configs that don't overlap by accident: this
355
+ **runtime** config (passed to `provideTranslate`, shipped in your browser
356
+ bundle) and the **build-time** [`linguo.config.json`](#translation-workflow)
357
+ (read only by the Node CLI). The runtime never reads the CLI's file. The one
358
+ thing both name is the locale list — `supportedLangs` here vs. `locales` there —
359
+ and `supportedLangs` is optional: it exists purely to match a saved or
360
+ browser-preferred language to one you actually ship.
361
+
362
+ ### `provideTranslate(options)`
363
+
364
+ | Option | Type | Default | What it does |
365
+ | ------------------------- | ---------------------------------------------- | --------------------------- | ----------------------------------------------------------------------------------------------------------------- |
366
+ | `defaultLang` | `string` | _(required)_ | The language reported before anything loads, and the guaranteed fallback. |
367
+ | `loader` | `TranslationLoader \| () => TranslationLoader` | _(required)_ | How translations are fetched. The factory form runs in DI, so the loader can inject services (e.g. `HttpClient`). |
368
+ | `supportedLangs` | `string[]` | _(none)_ | Languages you ship. Used to match a persisted/browser value; browser detection is skipped when omitted. |
369
+ | `persistSelectedLanguage` | `boolean` | `true` | Save the active language to `localStorage` when it changes. SSR-safe. |
370
+ | `restoreSelectedLanguage` | `boolean` | = `persistSelectedLanguage` | Read the saved language back on startup (inside `restoreLang()`). SSR-safe. |
371
+ | `persistKey` | `string` | `'ng-linguo.lang'` | The `localStorage` key used for the saved language. |
372
+ | `detectBrowserLanguage` | `boolean` | `true` | On first run, match `navigator.languages` against `supportedLangs`. SSR-safe. |
373
+
374
+ `provideIcu({ defaultFormat })` accepts `'mf2'` (default) or `'mf1'`.
375
+ `createHttpLoader({ prefix, suffix })` defaults to `/assets/i18n/` + `.json`,
376
+ fetching `${prefix}${lang}${suffix}`. A loader is just an object with a
377
+ `load(lang): Promise<Record<string, string>>` method, so any source works.
378
+
379
+ ## Packages & entry points
380
+
381
+ | Import | What it gives you |
382
+ | -------------------------- | -------------------------------------------------------------------------------------------------- |
383
+ | `@ng-linguo/linguo` | `TranslateStore`, `provideTranslate`, the `t` pipe, the `[t]` directive, `injectTranslate`, `mark` |
384
+ | `@ng-linguo/linguo/icu` | `provideIcu` — ICU MessageFormat 1 + 2 |
385
+ | `@ng-linguo/linguo/http` | `createHttpLoader` — `HttpClient`-backed loader |
386
+ | `@ng-linguo/extract` | build-time extraction/translate/compile CLI (pure Node) |
387
+ | `@ng-linguo/eslint-plugin` | lint config so the a11y linter trusts empty `[t]` elements |
388
+
389
+ ## Contributing
390
+
391
+ This is an Nx + pnpm monorepo.
392
+ [CLAUDE.md](https://github.com/jmwierzbicki/linguo/blob/main/CLAUDE.md) is the
393
+ source of truth for architecture, code style, testing, and release conventions —
394
+ read it first.
395
+
396
+ ```bash
397
+ pnpm install
398
+ pnpm nx run-many -t lint test build # the full suite (what CI runs)
399
+ pnpm nx serve playground # the demo app
17
400
  ```
18
401
 
19
- - **`@ng-linguo/linguo`** — the runtime: `TranslateStore`, the `t` pipe, the
20
- `[t]` directive, `injectTranslate`, slot tags.
21
- - **`@ng-linguo/linguo/icu`** — ICU MessageFormat (MF1 + MF2) via `provideIcu`.
22
- - **`@ng-linguo/linguo/http`** — an HTTP `TranslationLoader` (`createHttpLoader`).
402
+ ## License
23
403
 
24
- The build-time extraction CLI lives in the separate `@ng-linguo/extract`
25
- package (pure Node, no Angular dependency).
404
+ [MIT](https://github.com/jmwierzbicki/linguo/blob/main/LICENSE)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ng-linguo/linguo",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "A modern, signal-first i18n runtime for Angular 18+ — built on SignalStore, with a translator-safe slot syntax, ICU, and tree-shakeable HTTP loading.",
5
5
  "license": "MIT",
6
6
  "author": "jmwierzbicki",