@ng-linguo/linguo 0.9.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.
@@ -0,0 +1,454 @@
1
+ import * as _ngrx_signals from '@ngrx/signals';
2
+ import * as _angular_core from '@angular/core';
3
+ import { EnvironmentProviders, PipeTransform, OnInit, TemplateRef, InjectionToken } from '@angular/core';
4
+
5
+ /**
6
+ * A flat map of translation keys to their resolved string values for a single
7
+ * language. Values are plain strings (optionally containing `[name]...[/name]`
8
+ * slot tags and ICU syntax) — never HTML. This is the on-disk JSON shape, kept
9
+ * stable since v1 so translators can keep using existing tooling.
10
+ */
11
+ type Translations = Readonly<Record<string, string>>;
12
+ /**
13
+ * Strategy for fetching the {@link Translations} for a given language.
14
+ *
15
+ * Implementations live in their own packages (for example
16
+ * `@ng-linguo/linguo/http`); `@ng-linguo/linguo` only depends on this interface
17
+ * so it never reaches for `fetch`/XHR itself.
18
+ */
19
+ interface TranslationLoader {
20
+ /**
21
+ * Resolve the translations for `lang`. Rejecting the promise signals a load
22
+ * failure to the store; resolving with an empty map is a valid "no
23
+ * translations" result.
24
+ *
25
+ * @param lang BCP-47 language tag, for example `'en'` or `'pl'`.
26
+ */
27
+ load(lang: string): Promise<Translations>;
28
+ }
29
+ /**
30
+ * Bootstrap configuration for the translation runtime, passed to
31
+ * `provideTranslate`. Loading is never started implicitly from this config —
32
+ * the consumer triggers the first load explicitly.
33
+ */
34
+ interface TranslateConfig {
35
+ /** Language the store reports as current before anything is loaded. */
36
+ readonly defaultLang: string;
37
+ /**
38
+ * How translations are fetched. Either a ready-made {@link TranslationLoader}
39
+ * (e.g. a static dictionary or a `fetch`-based loader), or a **factory** that
40
+ * returns one. The factory runs inside Angular's injection context, so use it
41
+ * for loaders that inject Angular services — such as `createHttpLoader()` from
42
+ * `@ng-linguo/linguo/http`, which needs `HttpClient`:
43
+ *
44
+ * ```ts
45
+ * provideTranslate({ defaultLang: 'en', loader: () => createHttpLoader() });
46
+ * ```
47
+ */
48
+ readonly loader: TranslationLoader | (() => TranslationLoader);
49
+ /**
50
+ * Languages the app ships. Used by {@link TranslateStore.restoreLang} to match
51
+ * a persisted or browser-preferred value to one that actually exists. Browser
52
+ * matching is skipped when this is omitted (so an unshipped language is never
53
+ * auto-selected).
54
+ */
55
+ readonly supportedLangs?: readonly string[];
56
+ /**
57
+ * Write the active language to `localStorage` whenever it changes (the
58
+ * **persist** side). Defaults to `true`. SSR-safe (a no-op when not running
59
+ * in a browser). To stop reading the saved value on startup as well, see
60
+ * {@link restoreSelectedLanguage}.
61
+ */
62
+ readonly persistSelectedLanguage?: boolean;
63
+ /**
64
+ * Read the persisted language back on startup, inside
65
+ * {@link TranslateStore.restoreLang} (the **restore** side). Decoupled from
66
+ * {@link persistSelectedLanguage} so an app can keep persisting while owning
67
+ * the restore decision itself — set this to `false` and run your own
68
+ * selection logic, or just call `setLang(...)` instead of `restoreLang()`.
69
+ *
70
+ * Defaults to `persistSelectedLanguage` (so disabling persistence alone still
71
+ * disables restore, as before); set it explicitly to override. SSR-safe.
72
+ */
73
+ readonly restoreSelectedLanguage?: boolean;
74
+ /** `localStorage` key for the persisted language. Defaults to `ng-linguo.lang`. */
75
+ readonly persistKey?: string;
76
+ /**
77
+ * On first run (nothing persisted), use the browser's preferred language
78
+ * (`navigator.languages`) matched against {@link supportedLangs}. Defaults to
79
+ * `true`. SSR-safe.
80
+ */
81
+ readonly detectBrowserLanguage?: boolean;
82
+ }
83
+ /**
84
+ * Options for the `t` pipe, passed as a single object:
85
+ * `{{ 'Hello {$name}!' | t: { params: { name } } }}`.
86
+ */
87
+ interface TranslateOptions {
88
+ /** ICU arguments, formatted via `@ng-linguo/linguo/icu` when provided. */
89
+ readonly params?: Record<string, unknown>;
90
+ /**
91
+ * Disambiguating/contextual text for keys that share the same source text.
92
+ * It is part of the key (gettext `msgctxt`), so distinct contexts get
93
+ * distinct translations; it may be descriptive ("Button that starts a game").
94
+ */
95
+ readonly context?: string;
96
+ }
97
+
98
+ /**
99
+ * The reactive translation runtime, implemented as an `@ngrx/signals`
100
+ * `signalStore`. Consumers inject it and read state through signals; there are
101
+ * no `Observable` getters on the public surface.
102
+ *
103
+ * State is not loaded implicitly. Call {@link TranslateStore.restoreLang} once
104
+ * at startup to pick the language (persisted → browser → default) and load it,
105
+ * or {@link TranslateStore.setLang} to switch to a specific one.
106
+ * {@link TranslateStore.isReady} stays `false` until a language has loaded, so
107
+ * UI can gate on it to avoid a flash of untranslated content.
108
+ *
109
+ * @example
110
+ * ```ts
111
+ * const store = inject(TranslateStore);
112
+ * store.currentLang(); // Signal<string>
113
+ * store.isReady(); // Signal<boolean>
114
+ * await store.restoreLang(); // startup: persisted ?? browser ?? default
115
+ * await store.setLang('pl'); // explicit switch
116
+ * store.translate('greeting');
117
+ * ```
118
+ */
119
+ declare const TranslateStore: _angular_core.Type<{
120
+ readonly currentLang: _angular_core.Signal<string>;
121
+ readonly isReady: _angular_core.Signal<boolean>;
122
+ readonly translations: _angular_core.Signal<Readonly<Record<string, string>>>;
123
+ setLang: (lang: string) => Promise<void>;
124
+ restoreLang: () => Promise<void>;
125
+ translate: (key: string, context?: string) => string;
126
+ } & _ngrx_signals.StateSource<{
127
+ readonly currentLang: string;
128
+ readonly isReady: boolean;
129
+ readonly translations: Translations;
130
+ }>>;
131
+
132
+ /**
133
+ * Register the translation runtime for an application or a feature scope.
134
+ *
135
+ * Add the returned providers to `bootstrapApplication`'s `providers` (or a
136
+ * route's `providers`). This only wires up configuration — it never starts a
137
+ * load, so no HTTP is fired during DI initialization. Trigger the first load
138
+ * explicitly via `TranslateStore.setLang`.
139
+ *
140
+ * The `loader` may be a ready-made loader object, or a factory `() => loader`
141
+ * that is run inside the injection context — use the factory form for loaders
142
+ * that inject Angular services (e.g. `createHttpLoader()`, which needs
143
+ * `HttpClient`).
144
+ *
145
+ * @example
146
+ * ```ts
147
+ * // a static dictionary or fetch-based loader
148
+ * provideTranslate({ defaultLang: 'en', loader: myLoader });
149
+ *
150
+ * // an HttpClient-backed loader (factory form)
151
+ * provideTranslate({ defaultLang: 'en', loader: () => createHttpLoader() });
152
+ * ```
153
+ */
154
+ declare function provideTranslate(config: TranslateConfig): EnvironmentProviders;
155
+
156
+ /**
157
+ * Resolve a translation key to its string for the current language, with options
158
+ * passed as a single object: ICU `params` and a disambiguating `context`.
159
+ *
160
+ * @example
161
+ * ```html
162
+ * {{ 'Play' | t }}
163
+ * {{ 'Play' | t: { context: 'game' } }}
164
+ * {{ 'Hello {$name}!' | t: { params: { name } } }}
165
+ * ```
166
+ *
167
+ * The key is the source text; a missing key renders as itself. ICU `params` are
168
+ * applied by the formatter from `@ng-linguo/linguo/icu` (`provideIcu`); without
169
+ * it the message is returned unformatted.
170
+ *
171
+ * Impure so it re-evaluates against the store's signals each change detection,
172
+ * re-rendering when the language changes. To keep that cheap it **memoizes** by
173
+ * value: the lookup/format only re-runs when the key, `context`, `params`
174
+ * contents, or the active language actually change — so a fresh `{ params: … }`
175
+ * literal on every change-detection pass costs only an equality check. For hot
176
+ * or looped bindings, prefer `injectTranslate()` inside a `computed()`, which
177
+ * does zero work per change-detection pass.
178
+ */
179
+ declare class TranslatePipe implements PipeTransform {
180
+ private readonly t;
181
+ private readonly translations;
182
+ private cache;
183
+ transform(key: string, options?: TranslateOptions): string;
184
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<TranslatePipe, never>;
185
+ static ɵpipe: _angular_core.ɵɵPipeDeclaration<TranslatePipe, "t", true>;
186
+ }
187
+
188
+ /**
189
+ * Resolves a key to its translation for the current language, with optional ICU
190
+ * `params` and `context` — the function form of the `t` pipe.
191
+ */
192
+ type TranslateFn = (key: string, options?: TranslateOptions) => string;
193
+ /**
194
+ * Obtain a {@link TranslateFn} for use in component code — the TypeScript
195
+ * counterpart of the `t` pipe.
196
+ *
197
+ * Call it in an injection context (a field initializer or constructor); the
198
+ * returned `t` reads the store's signals each time it runs, so it stays reactive
199
+ * when used in a template binding or inside a `computed`.
200
+ *
201
+ * @example
202
+ * ```ts
203
+ * export class Greeting {
204
+ * private readonly t = injectTranslate();
205
+ * protected readonly name = signal('Ada');
206
+ * protected readonly text = computed(() =>
207
+ * this.t('Hello {$name}!', { params: { name: this.name() } }),
208
+ * );
209
+ * }
210
+ * ```
211
+ */
212
+ declare function injectTranslate(): TranslateFn;
213
+
214
+ /**
215
+ * Options for {@link mark}. Only `context` is read (by `@ng-linguo/extract`);
216
+ * `params` is accepted so a single options object can be shared with the `t`
217
+ * pipe / directive call that ultimately renders the message.
218
+ */
219
+ interface MarkOptions {
220
+ /**
221
+ * Disambiguating/contextual text recorded as the catalog `msgctxt`. It is part
222
+ * of the key, so it must match the `context` passed at the consuming
223
+ * pipe/directive for the runtime lookup to resolve. It doubles as a note for
224
+ * translators (e.g. `'file = a document on disk'`).
225
+ */
226
+ readonly context?: string;
227
+ }
228
+ /**
229
+ * Mark a string as a translatable message so `@ng-linguo/extract` collects it,
230
+ * returning the string unchanged (it does not translate at runtime).
231
+ *
232
+ * Use it for messages that are not written inline at a `t` pipe or `[t]`
233
+ * directive — for example an ICU message kept in a component field (an MF2
234
+ * pattern contains `{{ … }}`, which collides with Angular's `{{ }}` binding).
235
+ * The marked string is still translated at render time by the pipe/directive
236
+ * that consumes it.
237
+ *
238
+ * Pass `context` to record a `msgctxt`/translator note for the entry. Because
239
+ * context is part of the key, the **same** `context` must be supplied where the
240
+ * marked string is rendered (`| t: { context }` or `tContext`), or the runtime
241
+ * lookup will not find the contextual entry.
242
+ *
243
+ * @example
244
+ * ```ts
245
+ * readonly fileCount = mark(
246
+ * '.input {$count :number} .match $count one {{{$count} file}} * {{{$count} files}}',
247
+ * { context: 'file = a document on disk' },
248
+ * );
249
+ * // template: {{ fileCount | t: { params: { count: count() }, context: 'file = a document on disk' } }}
250
+ * ```
251
+ */
252
+ declare function mark(message: string, options?: MarkOptions): string;
253
+
254
+ /**
255
+ * A node in a parsed slot tree produced by {@link parseSlots}.
256
+ *
257
+ * - `text` nodes carry literal, translator-authored text. They are rendered as
258
+ * DOM text nodes — never as HTML — which is how the library keeps its XSS
259
+ * surface at zero by construction.
260
+ * - `slot` nodes correspond to a `[name]...[/name]` region that the developer
261
+ * fills with an `<ng-template>`. The bracket syntax resembles BBCode, but the
262
+ * names are arbitrary and author-chosen and carry no predefined rendering —
263
+ * each is a named slot bound to a template. Slots may nest.
264
+ */
265
+ type SlotNode = {
266
+ readonly kind: 'text';
267
+ readonly value: string;
268
+ } | {
269
+ readonly kind: 'slot';
270
+ readonly name: string;
271
+ readonly children: readonly SlotNode[];
272
+ };
273
+ /**
274
+ * Parse a translator-authored string into a tree of {@link SlotNode}s.
275
+ *
276
+ * The parser is total: every input produces a tree, and any malformed
277
+ * construct (a stray `[`, an unclosed tag, a mismatched closing tag) degrades
278
+ * gracefully to literal text rather than throwing. Well-formed
279
+ * `[name]...[/name]` regions — including nested ones — become `slot` nodes. To
280
+ * include a literal `[` (so text such as `[note]` is not read as a tag), double
281
+ * it: `[[` parses to a single `[`.
282
+ *
283
+ * @example
284
+ * ```ts
285
+ * parseSlots('Hello [b]world[/b]!');
286
+ * // [
287
+ * // { kind: 'text', value: 'Hello ' },
288
+ * // { kind: 'slot', name: 'b', children: [{ kind: 'text', value: 'world' }] },
289
+ * // { kind: 'text', value: '!' },
290
+ * // ]
291
+ *
292
+ * parseSlots('See [[b] for bold');
293
+ * // [{ kind: 'text', value: 'See [b] for bold' }]
294
+ * ```
295
+ */
296
+ declare function parseSlots(input: string): readonly SlotNode[];
297
+ /**
298
+ * Flatten a slot string to plain text: slot tags are dropped and their inner
299
+ * text is kept. This is how the `t` pipe and {@link injectTranslate} render a
300
+ * message containing `[name]...[/name]` slots — they return a string, so the
301
+ * markup degrades to text (the `[t]` directive renders the slots into templates
302
+ * instead).
303
+ *
304
+ * @example
305
+ * ```ts
306
+ * slotsToText('Read the [docs]documentation[/docs] now'); // 'Read the documentation now'
307
+ * slotsToText('Press [[Enter] to send'); // 'Press [Enter] to send'
308
+ * ```
309
+ */
310
+ declare function slotsToText(input: string): string;
311
+
312
+ /**
313
+ * Context handed to a slot's `<ng-template tFor>`:
314
+ *
315
+ * - `$implicit` — the slot's inner text, flattened to a string. `let-text` binds
316
+ * it, so `{{ text }}` renders a plain-text slot (the common case).
317
+ * - `children` — the slot's parsed child nodes, for binding *nested* slots to
318
+ * their own templates with `*tRender` instead of flattening them to text. Bind
319
+ * it with `let-kids="children"`. See `TranslateSlotOutlet`.
320
+ */
321
+ interface TranslateSlotContext {
322
+ readonly $implicit: string;
323
+ readonly children: readonly SlotNode[];
324
+ }
325
+
326
+ /**
327
+ * Provides an `<ng-template>` as the renderer for a named slot of the enclosing
328
+ * `[t]` element. The `[name]...[/name]` bracket syntax resembles BBCode, but the
329
+ * name is arbitrary and author-chosen — it identifies a slot to fill, not a tag
330
+ * with predefined HTML. Declared as a child of the `[t]` element (it renders as
331
+ * an invisible anchor), so the name is scoped to that element. The template
332
+ * receives the slot's inner text (`let-text`) and parsed children
333
+ * (`let-kids="children"`, for nesting via `*tRender`) as its context.
334
+ *
335
+ * @example
336
+ * ```html
337
+ * <ng-template tFor="docs" let-text>
338
+ * <a routerLink="/docs">{{ text }}</a>
339
+ * </ng-template>
340
+ * ```
341
+ */
342
+ declare class TranslateSlot {
343
+ /** Slot name, matching `[name]...[/name]` in the translation. */
344
+ readonly name: _angular_core.InputSignal<string>;
345
+ readonly templateRef: TemplateRef<TranslateSlotContext>;
346
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<TranslateSlot, never>;
347
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<TranslateSlot, "ng-template[tFor]", never, { "name": { "alias": "tFor"; "required": true; "isSignal": true; }; }, {}, never, never, true, never>;
348
+ }
349
+ /**
350
+ * Translate the `t` message and render it into the element, applying ICU `tParams`
351
+ * and binding any `[name]...[/name]` slot regions to `<ng-template tFor>`
352
+ * children.
353
+ *
354
+ * The message is the `t` attribute (a string expression), so it may contain both
355
+ * ICU (`{$name}`) and slot tags (`[name]`) safely. Rendered text is emitted as DOM
356
+ * text nodes, never HTML (CLAUDE.md §5.1); a slot with no matching template
357
+ * degrades to its inner text. Nested slots bind to their own templates when the
358
+ * parent template marks where they go with `*tRender` (see
359
+ * `TranslateSlotOutlet`); otherwise a nested slot renders as text. Re-renders
360
+ * when the language, params, or a slot template changes.
361
+ *
362
+ * @example
363
+ * ```html
364
+ * <p t="Hello {$name}!" [tParams]="{ name }"></p>
365
+ *
366
+ * <p t="Read the [docs]documentation[/docs] to get started">
367
+ * <ng-template tFor="docs" let-text>
368
+ * <a routerLink="/docs">{{ text }}</a>
369
+ * </ng-template>
370
+ * </p>
371
+ * ```
372
+ */
373
+ declare class TranslateDirective implements OnInit {
374
+ private readonly store;
375
+ private readonly host;
376
+ private readonly renderer;
377
+ private readonly formatter;
378
+ private readonly slotRenderer;
379
+ /** Source message (the translation key); may contain ICU and slot tags. */
380
+ readonly message: _angular_core.InputSignal<string>;
381
+ /** ICU arguments, formatted via `@ng-linguo/linguo/icu` when provided. */
382
+ readonly tParams: _angular_core.InputSignal<Record<string, unknown> | undefined>;
383
+ /** Optional disambiguating/contextual text (part of the key). */
384
+ readonly tContext: _angular_core.InputSignal<string>;
385
+ private readonly slots;
386
+ private templates;
387
+ constructor();
388
+ ngOnInit(): void;
389
+ /**
390
+ * Render a nested slot's `nodes` into `parent` before `before`, reusing the
391
+ * current templates and the shared renderer. Internal: called by
392
+ * `TranslateSlotOutlet` (`*tRender`); not part of the consumer-facing API.
393
+ */
394
+ renderChildren(parent: Node, before: Node, nodes: readonly SlotNode[]): void;
395
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<TranslateDirective, never>;
396
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<TranslateDirective, "[t]", never, { "message": { "alias": "t"; "required": true; "isSignal": true; }; "tParams": { "alias": "tParams"; "required": false; "isSignal": true; }; "tContext": { "alias": "tContext"; "required": false; "isSignal": true; }; }, {}, ["slots"], never, true, never>;
397
+ }
398
+
399
+ /**
400
+ * Renders the *children* of a slot at this position, so a nested slot binds to
401
+ * its own `<ng-template>` instead of flattening to text. Without it, a slot
402
+ * inside a templated slot renders as plain text (its `$implicit` value); with
403
+ * it, each nested slot finds its own `tFor` template.
404
+ *
405
+ * Bind the `children` from the enclosing slot's context and place the outlet
406
+ * where the nested content should appear:
407
+ *
408
+ * @example
409
+ * ```html
410
+ * <p t="Agree to our [link]Terms and [b]Conditions[/b][/link]">
411
+ * <ng-template tFor="link" let-kids="children">
412
+ * <a href="/terms"><ng-container *tRender="kids" /></a>
413
+ * </ng-template>
414
+ * <ng-template tFor="b" let-text><strong>{{ text }}</strong></ng-template>
415
+ * </p>
416
+ * ```
417
+ *
418
+ * Valid only inside a `[t]` element's slot template — it resolves the enclosing
419
+ * {@link TranslateDirective} and renders through its shared engine, so nested
420
+ * views are torn down with the rest on a language or params change.
421
+ */
422
+ declare class TranslateSlotOutlet implements OnInit {
423
+ /** The nodes to render — the `children` value from the slot context. */
424
+ readonly nodes: _angular_core.InputSignal<readonly SlotNode[]>;
425
+ private readonly host;
426
+ private readonly anchor;
427
+ ngOnInit(): void;
428
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<TranslateSlotOutlet, never>;
429
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<TranslateSlotOutlet, "[tRender]", never, { "nodes": { "alias": "tRender"; "required": true; "isSignal": true; }; }, {}, never, never, true, never>;
430
+ }
431
+
432
+ /**
433
+ * Formats a resolved message against runtime arguments — the seam that lets the
434
+ * `translate` pipe render ICU messages without `core` depending on any ICU
435
+ * library. `@ng-linguo/linguo/icu`'s `provideIcu` supplies an implementation; when none
436
+ * is provided the pipe simply returns the message unformatted.
437
+ */
438
+ interface MessageFormatter {
439
+ /**
440
+ * @param message The resolved (translated) message, possibly containing ICU
441
+ * placeholders.
442
+ * @param args Named arguments to substitute (counts, names, dates…).
443
+ * @param locale BCP-47 locale for plural rules and number/date formatting.
444
+ */
445
+ format(message: string, args: Record<string, unknown>, locale: string): string;
446
+ }
447
+ /**
448
+ * DI token for an optional {@link MessageFormatter}. Provided by
449
+ * `@ng-linguo/linguo/icu`'s `provideIcu`; absent by default.
450
+ */
451
+ declare const MESSAGE_FORMATTER: InjectionToken<MessageFormatter>;
452
+
453
+ export { MESSAGE_FORMATTER, TranslateDirective, TranslatePipe, TranslateSlot, TranslateSlotOutlet, TranslateStore, injectTranslate, mark, parseSlots, provideTranslate, slotsToText };
454
+ export type { MarkOptions, MessageFormatter, SlotNode, TranslateConfig, TranslateFn, TranslateOptions, TranslateSlotContext, TranslationLoader, Translations };