ngx-kit 8.5.0 → 9.0.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.
@@ -0,0 +1,612 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, inject, signal, computed, isDevMode, Injectable, Pipe, TemplateRef, ViewContainerRef, Input, Directive, ElementRef, Renderer2, effect, makeEnvironmentProviders, provideAppInitializer, provideEnvironmentInitializer, assertInInjectionContext } from '@angular/core';
3
+ import { DOCUMENT } from '@angular/common';
4
+ import { firstValueFrom, defer, from, retry } from 'rxjs';
5
+ import { HttpClient } from '@angular/common/http';
6
+
7
+ /** Defaults applied by `provideI18n()`. */
8
+ const I18N_DEFAULTS = { fileExtension: '.json', retries: 1 };
9
+ const I18N_CONFIG = new InjectionToken('I18N_CONFIG');
10
+ const I18N_LOADER = new InjectionToken('I18N_LOADER');
11
+
12
+ /** Flatten a nested dictionary into `target` (dot-separated keys). Non-string leaves are stringified. */
13
+ function flatten(dict, target = new Map(), onConflict, prefix = '') {
14
+ for (const k of Object.keys(dict)) {
15
+ const v = dict[k];
16
+ const key = prefix ? `${prefix}.${k}` : k;
17
+ if (v !== null && typeof v === 'object') {
18
+ flatten(v, target, onConflict, key);
19
+ }
20
+ else if (v !== null && v !== undefined) {
21
+ const s = String(v);
22
+ if (onConflict) {
23
+ const prev = target.get(key);
24
+ if (prev !== undefined && prev !== '' && s !== '' && prev !== s)
25
+ onConflict(key);
26
+ }
27
+ target.set(key, s);
28
+ }
29
+ }
30
+ return target;
31
+ }
32
+ const RE = /\{\{\s*([\w$.-]+)\s*\}\}/g;
33
+ function getPath(obj, path) {
34
+ if (path in obj)
35
+ return obj[path];
36
+ let cur = obj;
37
+ for (const part of path.split('.')) {
38
+ if (cur == null)
39
+ return undefined;
40
+ cur = cur[part];
41
+ }
42
+ return cur;
43
+ }
44
+ /** `Hello {{ name }}` + `{name:'Ali'}` -> `Hello Ali`. Unknown placeholders are left untouched. */
45
+ function interpolate(text, params) {
46
+ if (!params || text.indexOf('{{') === -1)
47
+ return text; // fast path: no regex
48
+ return text.replace(RE, (m, k) => {
49
+ const v = getPath(params, k);
50
+ return v == null ? m : String(v);
51
+ });
52
+ }
53
+ function shallowEqual(a, b) {
54
+ if (a === b)
55
+ return true;
56
+ if (!a || !b)
57
+ return false;
58
+ const ka = Object.keys(a);
59
+ if (ka.length !== Object.keys(b).length)
60
+ return false;
61
+ for (const k of ka)
62
+ if (a[k] !== b[k])
63
+ return false;
64
+ return true;
65
+ }
66
+ const RTL_LANGS = new Set(['ar', 'fa', 'he', 'ur', 'ps', 'sd', 'ug', 'yi', 'ckb', 'dv']);
67
+ /** Identity function the extractor recognises: `const k = marker('orders.title')`. */
68
+ function marker(key) {
69
+ return key;
70
+ }
71
+ /** Copy only the properties of `patch` that are not `undefined` (so an omitted option never wipes a default). */
72
+ function assignDefined(target, patch) {
73
+ for (const k of Object.keys(patch)) {
74
+ if (patch[k] !== undefined)
75
+ target[k] = patch[k];
76
+ }
77
+ return target;
78
+ }
79
+ /**
80
+ * Build `{base}/{scope}/{lang}{ext}` without ever producing `undefined`, `//` or a trailing slash problem:
81
+ * ("/i18n", undefined, "fa", ".json") -> "/i18n/fa.json"
82
+ * ("https://x.com/", "setting", "fa", "json") -> "https://x.com/setting/fa.json"
83
+ * (undefined | "", undefined, "fa") -> "/fa"
84
+ * The protocol part of `base` ("https://") is left alone.
85
+ */
86
+ function buildI18nUrl(base, scope, lang, ext = '') {
87
+ const trim = (s) => s.trim().replace(/^\/+|\/+$/g, '');
88
+ const e = ext ? (ext.startsWith('.') ? ext : '.' + ext) : '';
89
+ const path = [scope ? trim(scope) : '', trim(lang) + e]
90
+ .filter(Boolean)
91
+ .join('/')
92
+ .replace(/\/{2,}/g, '/');
93
+ return `${(base ?? '').trim().replace(/\/+$/, '')}/${path}`;
94
+ }
95
+
96
+ const TAG = '[lazy-i18n]';
97
+ const EMPTY = [];
98
+ class I18nService {
99
+ cfg = inject(I18N_CONFIG);
100
+ loader = inject(I18N_LOADER);
101
+ doc = inject(DOCUMENT);
102
+ _lang = signal(this.cfg.defaultLang ?? '', /* @ts-ignore */
103
+ ...(ngDevMode ? [{ debugName: "_lang" }] : /* istanbul ignore next */ []));
104
+ _rev = signal(0, /* @ts-ignore */
105
+ ...(ngDevMode ? [{ debugName: "_rev" }] : /* istanbul ignore next */ []));
106
+ _langs = signal(this.cfg.langs ?? EMPTY, /* @ts-ignore */
107
+ ...(ngDevMode ? [{ debugName: "_langs" }] : /* istanbul ignore next */ []));
108
+ _ready = signal(false, /* @ts-ignore */
109
+ ...(ngDevMode ? [{ debugName: "_ready" }] : /* istanbul ignore next */ []));
110
+ /** Active language (reactive). */
111
+ lang = this._lang.asReadonly();
112
+ /** Bumps whenever the active language or any dictionary changes. */
113
+ revision = this._rev.asReadonly();
114
+ dir = computed(() => RTL_LANGS.has(this._lang().split('-')[0]) ? 'rtl' : 'ltr', /* @ts-ignore */
115
+ ...(ngDevMode ? [{ debugName: "dir" }] : /* istanbul ignore next */ []));
116
+ /** Supported languages (a signal: they may be supplied after startup via `init()` / `configure()`). */
117
+ langs = this._langs.asReadonly();
118
+ /** `true` once the first language has been activated. Handy for a splash screen. */
119
+ ready = this._ready.asReadonly();
120
+ // read live, so values supplied later through configure() are honoured
121
+ get fallbacks() {
122
+ return this.cfg.fallbackLangs ?? EMPTY;
123
+ }
124
+ get warn() {
125
+ return this.cfg.warnMissing ?? isDevMode();
126
+ }
127
+ dicts = new Map(); // lang -> flat map
128
+ loaded = new Set(); // `${lang}|${scope}`
129
+ inflight = new Map();
130
+ scopes = new Set();
131
+ pending = new Set();
132
+ warned = new Set();
133
+ useToken = 0;
134
+ // ───────────────────────── translate ─────────────────────────
135
+ /** Translate. Reads signals, so it is reactive inside `computed`, `effect` and templates. */
136
+ t(key, params) {
137
+ this._rev();
138
+ return this.resolve(key, params, this._lang());
139
+ }
140
+ /** Same as `t`, but never creates a reactive dependency. */
141
+ instant(key, params) {
142
+ return this.resolve(key, params, this._lang());
143
+ }
144
+ /** `computed` signal for one key. `params` may be a function reading other signals. */
145
+ select(key, params) {
146
+ return computed(() => this.t(key, typeof params === 'function' ? params() : params));
147
+ }
148
+ /** True when the key has a non-empty value in the active language or a fallback. */
149
+ has(key) {
150
+ this._rev();
151
+ return this.lookup(key, this._lang()) !== undefined;
152
+ }
153
+ /** Returns every key below `prefix` as a nested object (values are raw, not interpolated). */
154
+ getObject(prefix) {
155
+ this._rev();
156
+ const out = {};
157
+ const p = prefix + '.';
158
+ for (const lang of [this._lang(), ...this.fallbacks]) {
159
+ for (const [k, v] of this.dicts.get(lang) ?? []) {
160
+ if (!k.startsWith(p) || !v)
161
+ continue;
162
+ const parts = k.slice(p.length).split('.');
163
+ let cur = out;
164
+ for (let i = 0; i < parts.length - 1; i++)
165
+ cur = typeof cur[parts[i]] === 'object' ? cur[parts[i]] : (cur[parts[i]] = {});
166
+ if (!(parts[parts.length - 1] in cur))
167
+ cur[parts[parts.length - 1]] = v;
168
+ }
169
+ }
170
+ return out;
171
+ }
172
+ lookup(key, lang) {
173
+ let raw = this.dicts.get(lang)?.get(key);
174
+ if (raw)
175
+ return raw;
176
+ for (const fb of this.fallbacks) {
177
+ if (fb === lang)
178
+ continue;
179
+ raw = this.dicts.get(fb)?.get(key);
180
+ if (raw)
181
+ return raw;
182
+ }
183
+ return undefined; // '' (extractor placeholder) counts as missing -> falls back
184
+ }
185
+ resolve(key, params, lang) {
186
+ const raw = this.lookup(key, lang);
187
+ if (raw === undefined)
188
+ return this.onMissing(key, lang);
189
+ return this.cfg.transpile ? this.cfg.transpile(raw, params, lang) : interpolate(raw, params);
190
+ }
191
+ onMissing(key, lang) {
192
+ if (this.warn && this.dicts.has(lang) && !this.warned.has(key)) {
193
+ this.warned.add(key);
194
+ console.warn(`${TAG} Missing translation "${key}" (lang: ${lang})`);
195
+ }
196
+ const h = this.cfg.missing ?? 'key';
197
+ return typeof h === 'function' ? h(key, lang) : h === 'empty' ? '' : key;
198
+ }
199
+ // ───────────────────────── language ─────────────────────────
200
+ /**
201
+ * Load global + all registered scopes (and fallback languages) for `lang`, THEN activate it.
202
+ * Nothing renders half-translated and no "missing" flash occurs.
203
+ */
204
+ use(lang) {
205
+ const p = this.activate(lang);
206
+ // tracked so whenReady() / i18nReadyGuard also wait for an in-flight language change
207
+ const tracked = p
208
+ .then(() => undefined, () => undefined)
209
+ .then(() => {
210
+ this.pending.delete(tracked);
211
+ });
212
+ this.pending.add(tracked);
213
+ return p;
214
+ }
215
+ /**
216
+ * Complete the configuration and activate the first language in one call. Use it from your own
217
+ * APP_INITIALIZER when the language list is only known at runtime (e.g. from the backend):
218
+ *
219
+ * ```ts
220
+ * await i18n.init({ langs: ['fa', 'en'], defaultLang: 'fa', baseUrl: apiUrl, lang: user.lang });
221
+ * ```
222
+ * `lang` is used when it is in `langs`; otherwise stored > browser (opt-in) > `defaultLang`.
223
+ * Rejects when the files cannot be loaded, so the initializer can show an error page.
224
+ */
225
+ init(options = {}) {
226
+ const { lang, ...patch } = options;
227
+ this.configure(patch);
228
+ const wanted = lang && (this.cfg.langs ?? EMPTY).includes(lang) ? lang : this.detectInitialLang();
229
+ return this.use(wanted);
230
+ }
231
+ /**
232
+ * Update options after startup (languages, baseUrl, fallbacks, ...). Already downloaded files stay cached.
233
+ * `loader` and `autoInit` can only be set in `provideI18n()`.
234
+ */
235
+ configure(patch) {
236
+ assignDefined(this.cfg, patch);
237
+ this.syncLangs();
238
+ if (patch.defaultLang && !this._ready())
239
+ this._lang.set(patch.defaultLang);
240
+ }
241
+ syncLangs() {
242
+ const l = this.cfg.langs ?? EMPTY;
243
+ if (l !== this._langs())
244
+ this._langs.set(l);
245
+ }
246
+ async activate(lang) {
247
+ this.syncLangs();
248
+ const langs = this.cfg.langs ?? EMPTY;
249
+ if (!langs.length) {
250
+ throw new Error(`${TAG} No languages configured. Pass "langs" to provideI18n() or call I18nService.init({ langs, defaultLang }) first.`);
251
+ }
252
+ if (!langs.includes(lang))
253
+ throw new Error(`${TAG} Unsupported language "${lang}"`);
254
+ const token = ++this.useToken;
255
+ do {
256
+ await this.loadLang(lang);
257
+ } while (!this.isComplete(lang)); // a scope may have been registered meanwhile
258
+ if (token !== this.useToken)
259
+ return; // a newer use() won
260
+ this._lang.set(lang);
261
+ this._rev.update((v) => v + 1);
262
+ this._ready.set(true);
263
+ this.persist(lang);
264
+ this.applyDocument(lang);
265
+ }
266
+ /** Download a language without activating it. */
267
+ preload(lang) {
268
+ return this.loadLang(lang);
269
+ }
270
+ /** Language to start with: stored > browser (opt-in) > default. */
271
+ detectInitialLang() {
272
+ const langs = this.cfg.langs ?? EMPTY;
273
+ const key = this.storageKey();
274
+ try {
275
+ const s = key ? localStorage.getItem(key) : null;
276
+ if (s && langs.includes(s))
277
+ return s;
278
+ }
279
+ catch {
280
+ /* storage unavailable (SSR / privacy mode) */
281
+ }
282
+ if (this.cfg.detectBrowserLang && typeof navigator !== 'undefined') {
283
+ for (const n of navigator.languages ?? [navigator.language]) {
284
+ if (langs.includes(n))
285
+ return n;
286
+ const base = n.split('-')[0];
287
+ if (langs.includes(base))
288
+ return base;
289
+ }
290
+ }
291
+ const def = this.cfg.defaultLang ?? '';
292
+ return langs.includes(def) ? def : (langs[0] ?? def);
293
+ }
294
+ // ───────────────────────── scopes ─────────────────────────
295
+ /** Register a scope and start loading it for the active language. Idempotent. Used by `provideI18nScope`. */
296
+ registerScope(scope) {
297
+ if (!scope)
298
+ return;
299
+ this.scopes.add(scope);
300
+ // before the first activation only remember it: init()/use() loads every registered scope
301
+ if (!this._ready())
302
+ return;
303
+ const lang = this._lang();
304
+ const p = Promise.all([lang, ...this.fallbacks.filter((f) => f !== lang)].map((l) => this.fetch(l, scope).catch((e) => {
305
+ console.error(`${TAG} Failed to load scope "${scope}" (${l})`, e);
306
+ }))).then(() => {
307
+ this.pending.delete(p);
308
+ });
309
+ this.pending.add(p);
310
+ }
311
+ /** Resolves once every scope registered so far is available. Never rejects. */
312
+ async whenReady() {
313
+ while (this.pending.size)
314
+ await Promise.all([...this.pending]);
315
+ }
316
+ // ───────────────────────── data ─────────────────────────
317
+ /** Manually set translations (tests, SSR transfer state, ...). */
318
+ setTranslation(lang, dict, merge = true) {
319
+ if (!merge)
320
+ this.dicts.delete(lang);
321
+ this.merge(lang, dict);
322
+ }
323
+ merge(lang, dict) {
324
+ let m = this.dicts.get(lang);
325
+ if (!m)
326
+ this.dicts.set(lang, (m = new Map()));
327
+ flatten(dict, m, this.warn
328
+ ? (k) => console.warn(`${TAG} Key "${k}" is defined twice with different values (${lang})`)
329
+ : undefined);
330
+ this._rev.update((v) => v + 1);
331
+ }
332
+ fetch(lang, scope) {
333
+ scope = scope || undefined; // never hand '' / null to a loader
334
+ const id = `${lang}|${scope ?? ''}`;
335
+ if (this.loaded.has(id))
336
+ return Promise.resolve();
337
+ let p = this.inflight.get(id);
338
+ if (!p) {
339
+ p = firstValueFrom(defer(() => from(this.loader.load(lang, scope))).pipe(retry({ count: this.cfg.retries ?? 1, delay: 250 })))
340
+ .then((d) => {
341
+ this.merge(lang, d);
342
+ this.loaded.add(id);
343
+ })
344
+ .finally(() => this.inflight.delete(id));
345
+ this.inflight.set(id, p);
346
+ }
347
+ return p;
348
+ }
349
+ loadLang(lang) {
350
+ const scopes = [undefined, ...this.scopes];
351
+ const main = scopes.map((s) => this.fetch(lang, s));
352
+ const fb = this.fallbacks
353
+ .filter((f) => f !== lang)
354
+ .flatMap((f) => scopes.map((s) => this.fetch(f, s).catch((e) => console.error(`${TAG} Fallback "${f}" failed`, e))));
355
+ return Promise.all([...main, ...fb]).then(() => undefined);
356
+ }
357
+ isComplete(lang) {
358
+ return (this.loaded.has(`${lang}|`) && [...this.scopes].every((s) => this.loaded.has(`${lang}|${s}`)));
359
+ }
360
+ storageKey() {
361
+ return this.cfg.storageKey === undefined ? 'lazy-i18n.lang' : this.cfg.storageKey;
362
+ }
363
+ persist(lang) {
364
+ const key = this.storageKey();
365
+ try {
366
+ if (key)
367
+ localStorage.setItem(key, lang);
368
+ }
369
+ catch {
370
+ /* ignore */
371
+ }
372
+ }
373
+ applyDocument(lang) {
374
+ if (this.cfg.manageDocument === false)
375
+ return;
376
+ const el = this.doc?.documentElement;
377
+ if (!el)
378
+ return;
379
+ el.setAttribute('lang', lang);
380
+ el.setAttribute('dir', this.dir());
381
+ }
382
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: I18nService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
383
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: I18nService, providedIn: 'root' });
384
+ }
385
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: I18nService, decorators: [{
386
+ type: Injectable,
387
+ args: [{ providedIn: 'root' }]
388
+ }] });
389
+
390
+ /**
391
+ * `{{ 'orders.title' | tr }}` / `{{ 'hello' | tr: { name } }}`
392
+ *
393
+ * Impure on purpose (it must follow signals), but memoised: when key, params and the
394
+ * service revision are unchanged it returns the cached string in O(1) with no lookup.
395
+ */
396
+ class TrPipe {
397
+ svc = inject(I18nService);
398
+ k;
399
+ p;
400
+ rev = -1;
401
+ out = '';
402
+ transform(key, params) {
403
+ if (!key)
404
+ return '';
405
+ const rev = this.svc.revision(); // tracked: marks the view dirty on lang / dictionary change
406
+ if (rev === this.rev && key === this.k && shallowEqual(params, this.p))
407
+ return this.out;
408
+ this.k = key;
409
+ this.p = params;
410
+ this.rev = rev;
411
+ return (this.out = this.svc.t(key, params));
412
+ }
413
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: TrPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
414
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.4", ngImport: i0, type: TrPipe, isStandalone: true, name: "tr", pure: false });
415
+ }
416
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: TrPipe, decorators: [{
417
+ type: Pipe,
418
+ args: [{ name: 'tr', pure: false }]
419
+ }] });
420
+
421
+ /**
422
+ * Structural directive. The view is created ONCE; `t` reads signals, so only the bindings that use it update.
423
+ *
424
+ * <ng-container *tr="let t; prefix: 'orders'">
425
+ * <h1>{{ t('title') }}</h1> <!-- orders.title -->
426
+ * </ng-container>
427
+ */
428
+ class TrDirective {
429
+ tpl = inject(TemplateRef);
430
+ vcr = inject(ViewContainerRef);
431
+ svc = inject(I18nService);
432
+ prefix = signal(undefined, /* @ts-ignore */
433
+ ...(ngDevMode ? [{ debugName: "prefix" }] : /* istanbul ignore next */ []));
434
+ /** `prefix: 'orders'` in the microsyntax. */
435
+ set trPrefix(v) { this.prefix.set(v); }
436
+ static ngTemplateContextGuard(_dir, _ctx) {
437
+ return true;
438
+ }
439
+ constructor() {
440
+ const t = (key, params) => {
441
+ const p = this.prefix();
442
+ return this.svc.t(p ? p + '.' + key : key, params);
443
+ };
444
+ this.vcr.createEmbeddedView(this.tpl, { $implicit: t, t, lang: this.svc.lang });
445
+ }
446
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: TrDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
447
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.4", type: TrDirective, isStandalone: true, selector: "[tr],[translate]", inputs: { trPrefix: "trPrefix" }, ngImport: i0 });
448
+ }
449
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: TrDirective, decorators: [{
450
+ type: Directive,
451
+ args: [{ selector: '[tr],[translate]' }]
452
+ }], ctorParameters: () => [], propDecorators: { trPrefix: [{
453
+ type: Input,
454
+ args: ['trPrefix']
455
+ }] } });
456
+ /**
457
+ * Attribute directive.
458
+ * <span trKey="orders.title"></span>
459
+ * <input trKey="form.name" trAttr="placeholder" />
460
+ * <p trKey="hello" [trParams]="{ name: user.name }"></p>
461
+ */
462
+ class TrKeyDirective {
463
+ // @Input setters feeding signals: works with JIT, AOT and every supported Angular version.
464
+ key = signal(undefined, /* @ts-ignore */
465
+ ...(ngDevMode ? [{ debugName: "key" }] : /* istanbul ignore next */ []));
466
+ params = signal(undefined, /* @ts-ignore */
467
+ ...(ngDevMode ? [{ debugName: "params" }] : /* istanbul ignore next */ []));
468
+ attr = signal(undefined, /* @ts-ignore */
469
+ ...(ngDevMode ? [{ debugName: "attr" }] : /* istanbul ignore next */ []));
470
+ prefix = signal(undefined, /* @ts-ignore */
471
+ ...(ngDevMode ? [{ debugName: "prefix" }] : /* istanbul ignore next */ []));
472
+ set trKey(v) { this.key.set(v); }
473
+ set trParams(v) { this.params.set(v); }
474
+ set trAttr(v) { this.attr.set(v); }
475
+ set trPrefix(v) { this.prefix.set(v); }
476
+ constructor() {
477
+ const svc = inject(I18nService);
478
+ const el = inject(ElementRef).nativeElement;
479
+ const r = inject(Renderer2);
480
+ effect(() => {
481
+ const k = this.key();
482
+ if (!k)
483
+ return;
484
+ const p = this.prefix();
485
+ const text = svc.t(p ? p + '.' + k : k, this.params());
486
+ const a = this.attr();
487
+ if (a)
488
+ r.setAttribute(el, a, text);
489
+ else if (el.textContent !== text)
490
+ el.textContent = text;
491
+ });
492
+ }
493
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: TrKeyDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
494
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.4", type: TrKeyDirective, isStandalone: true, selector: "[trKey]", inputs: { trKey: "trKey", trParams: "trParams", trAttr: "trAttr", trPrefix: "trPrefix" }, ngImport: i0 });
495
+ }
496
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: TrKeyDirective, decorators: [{
497
+ type: Directive,
498
+ args: [{ selector: '[trKey]' }]
499
+ }], ctorParameters: () => [], propDecorators: { trKey: [{
500
+ type: Input,
501
+ args: ['trKey']
502
+ }], trParams: [{
503
+ type: Input,
504
+ args: ['trParams']
505
+ }], trAttr: [{
506
+ type: Input,
507
+ args: ['trAttr']
508
+ }], trPrefix: [{
509
+ type: Input,
510
+ args: ['trPrefix']
511
+ }] } });
512
+
513
+ /**
514
+ * Default loader: GET `{baseUrl}/{lang}{ext}` and `{baseUrl}/{scope}/{lang}{ext}`.
515
+ * Goes through your HttpClient interceptors.
516
+ * Config is read on every request, so values supplied later via `I18nService.init()` /
517
+ * `configure()` (for example `baseUrl`) are picked up.
518
+ */
519
+ class HttpI18nLoader {
520
+ http = inject(HttpClient);
521
+ cfg = inject(I18N_CONFIG);
522
+ load(lang, scope) {
523
+ const { url, baseUrl, fileExtension } = this.cfg;
524
+ const u = url
525
+ ? url(lang, scope)
526
+ : buildI18nUrl(baseUrl ?? '/i18n', scope, lang, fileExtension ?? I18N_DEFAULTS.fileExtension);
527
+ return this.http.get(u);
528
+ }
529
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: HttpI18nLoader, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
530
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: HttpI18nLoader, providedIn: 'root' });
531
+ }
532
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: HttpI18nLoader, decorators: [{
533
+ type: Injectable,
534
+ args: [{ providedIn: 'root' }]
535
+ }] });
536
+
537
+ /**
538
+ * Static configuration (everything known at build time):
539
+ * ```ts
540
+ * provideHttpClient(),
541
+ * provideI18n({ langs: ['fa', 'en'], defaultLang: 'fa', fallbackLangs: ['en'] })
542
+ * ```
543
+ * The global dictionary of the initial language is then loaded before the app starts.
544
+ *
545
+ * Deferred configuration (languages come from your backend): omit `langs`/`defaultLang`
546
+ * and call `await inject(I18nService).init({ langs, defaultLang, baseUrl, lang })`
547
+ * from your own APP_INITIALIZER once you know them:
548
+ * ```ts
549
+ * provideI18n({ fallbackLangs: ['fa'] }), // no automatic initializer
550
+ * provideAppInitializer(() => AppInitializer()), // calls i18n.init(...)
551
+ * ```
552
+ * Pass `autoInit: false` to force this mode even when `langs` is given.
553
+ */
554
+ function provideI18n(config = {}) {
555
+ const autoInit = config.autoInit ?? (!!config.langs?.length && !!config.defaultLang);
556
+ return makeEnvironmentProviders([
557
+ // a fresh, mutable object per injector (the service updates it through configure())
558
+ { provide: I18N_CONFIG, useFactory: () => assignDefined({ ...I18N_DEFAULTS }, config) },
559
+ ...(config.loader ? [config.loader] : []),
560
+ { provide: I18N_LOADER, useFactory: () => inject(config.loader ?? HttpI18nLoader) },
561
+ ...(autoInit
562
+ ? [
563
+ provideAppInitializer(() => {
564
+ const svc = inject(I18nService);
565
+ return svc.use(svc.detectInitialLang());
566
+ }),
567
+ ]
568
+ : []),
569
+ ]);
570
+ }
571
+
572
+ /**
573
+ * Put in `route.providers`. Runs when the route injector is created (before guards),
574
+ * so loading starts as early as possible. Child routes need nothing.
575
+ */
576
+ function provideI18nScope(scope) {
577
+ const scopes = Array.isArray(scope) ? scope : [scope];
578
+ return makeEnvironmentProviders([
579
+ provideEnvironmentInitializer(() => {
580
+ const svc = inject(I18nService);
581
+ for (const s of scopes)
582
+ svc.registerScope(s);
583
+ }),
584
+ ]);
585
+ }
586
+ /** Put ONCE on the root path-less route: `canActivateChild: [i18nReadyGuard]`. Waits for every registered scope. */
587
+ const i18nReadyGuard = async () => {
588
+ await inject(I18nService).whenReady();
589
+ return true;
590
+ };
591
+
592
+ /** `readonly title = translateSignal('orders.title')` (injection context) */
593
+ function translateSignal(key, params) {
594
+ assertInInjectionContext(translateSignal);
595
+ return inject(I18nService).select(key, params);
596
+ }
597
+ /** `const t = injectT(); computed(() => t('a.b'))` — reactive translate function. */
598
+ function injectT() {
599
+ assertInInjectionContext(injectT);
600
+ const svc = inject(I18nService);
601
+ return (key, params) => svc.t(key, params);
602
+ }
603
+
604
+ /** `imports: [TR]` brings the pipe and both directives into a standalone component. */
605
+ const TR = [TrPipe, TrDirective, TrKeyDirective];
606
+
607
+ /**
608
+ * Generated bundle index. Do not edit.
609
+ */
610
+
611
+ export { HttpI18nLoader, I18N_CONFIG, I18N_DEFAULTS, I18N_LOADER, I18nService, TR, TrDirective, TrKeyDirective, TrPipe, buildI18nUrl, flatten, i18nReadyGuard, injectT, interpolate, marker, provideI18n, provideI18nScope, translateSignal };
612
+ //# sourceMappingURL=ngx-kit-lazy-i18n.mjs.map