@reforgium/presentia 1.3.0 → 1.4.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.
package/README.md CHANGED
@@ -3,195 +3,490 @@
3
3
  [![npm version](https://badge.fury.io/js/%40reforgium%2Fpresentia.svg)](https://www.npmjs.com/package/@reforgium/presentia)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
5
 
6
- **Signals-first state stores and caching utilities for Angular (18+).**
7
-
8
- Presentia Contents:
9
- - Providers and initialization: [`provideReInit`](`src/providers/init.provider.ts`) (locale, themes, breakpoints).
10
- - Localization: [`LangService`](`src/lang/lang.service.ts`), [`LangPipe`](`src/lang/lang.pipe.ts`), [`LangDirective`](`src/lang/lang.directive.ts`).
11
- - Themes: [`ThemeService`](`src/theme/theme.service.ts`).
12
- - Adaptivity: [`AdaptiveService`](`src/adaptive/adaptive.service.ts`), [`IfDeviceDirective`](`src/adaptive/device-type.directive.ts`).
13
-
14
- Designed for **application infrastructure**.
15
-
16
- ---
17
-
18
- ## Installation
6
+ Infrastructure package for Angular applications:
7
+ - localization (`LangService`, `lang` pipe, `reLang` directive),
8
+ - theming (`ThemeService`),
9
+ - adaptive behavior (`AdaptiveService`, `*reIfDevice`),
10
+ - route state helper (`RouteWatcher`),
11
+ - SEO automation (`SeoService`, `SeoRouteListener`).
12
+
13
+ ## Release Highlights (1.4.0)
14
+
15
+ - Extended localization HTTP pipeline:
16
+ `requestBuilder`, `requestOptionsFactory`, `responseAdapter`.
17
+ - Batch namespace loading:
18
+ `loadNamespaces`, `batchRequestBuilder`, `batchResponseAdapter`.
19
+ - Namespace cache policy:
20
+ `namespaceCache.maxNamespaces`, `namespaceCache.ttlMs`,
21
+ plus `evictNamespace` and `clearNamespaceCache`.
22
+ - Typed localization keys support:
23
+ `LangKeyRegistry` extension point for `get()` and `observe()`.
24
+ - Consumer CLI for key generation:
25
+ `presentia-gen-lang-keys`.
26
+
27
+ ## Install
19
28
 
20
29
  ```bash
21
- npm install @reforgium/presentia
30
+ npm i @reforgium/presentia
22
31
  ```
23
32
 
24
- The provider factory [`provideReInit(config: AppConfig)`](`src/providers/init.provider.ts`) performs basic application setup: localization, adaptive breakpoints, store config, and DI tokens for validation messages.
25
- Location: [`init.provider.ts`](`src/providers/init.provider.ts`).
33
+ ## Quick Start
26
34
 
27
- What it does:
28
- - Registers DI tokens:
29
- - `DEVICE_BREAKPOINTS` (breakpoints for AdaptiveService).
30
- - `THEME_CONFIG` (theme configuration and dark theme prefix).
31
- - `LANG_CONFIG`, `LOCALE_ID` (languages and dictionaries for `LangService`).
32
- - `LANG_PIPE_CONFIG` (optional cache settings for `LangPipe`).
33
-
34
- Example integration in the root application:
35
35
  ```ts
36
+ import { bootstrapApplication } from '@angular/platform-browser';
37
+ import { provideReInit } from '@reforgium/presentia';
38
+
36
39
  bootstrapApplication(AppComponent, {
37
40
  providers: [
38
41
  provideReInit({
39
42
  locale: {
43
+ url: '/assets/i18n',
40
44
  isFromAssets: true,
41
45
  defaultLang: 'ru',
46
+ fallbackLang: 'en',
47
+ supportedLangs: ['de'],
48
+ preloadNamespaces: ['layout', 'common'],
42
49
  },
43
50
  langPipe: {
44
- ttlMs: 300000,
51
+ ttlMs: 300_000,
45
52
  maxCacheSize: 500,
53
+ placeholder: '...',
46
54
  },
47
- theme: {
48
- defaultTheme: 'dark',
49
- darkThemePrefix: 're-dark',
50
- },
51
- breakpoints: {
52
- 'mobile': '(max-width: 719px)',
53
- 'tablet': '(min-width: 720px) and (max-width: 1399px)',
54
- 'desktop-s': '(min-width: 1400px) and (max-width: 1919px)',
55
- 'desktop': '(min-width: 1920px)',
56
- },
55
+ langMissingKeyHandler: (key, ctx) => `[${ctx.lang}] ${key}`,
56
+ theme: { defaultTheme: 'light', darkThemePrefix: 'dark' },
57
57
  }),
58
58
  ],
59
59
  });
60
60
  ```
61
61
 
62
- ---
62
+ ## What You Get
63
+
64
+ Main provider:
65
+ - `provideReInit(config: AppConfig)`
66
+
67
+ Localization exports:
68
+ - `LangService`
69
+ - `LangPipe`
70
+ - `LangDirective`
71
+ - `LANG_CONFIG`
72
+ - `LANG_PIPE_CONFIG`
73
+ - `LANG_MISSING_KEY_HANDLER`
74
+ - Types: `LocaleConfig`, `LangModel`, `LangDto`, `LangParams`, `LangKey`, `LangKeyRegistry`, etc.
75
+
76
+ Theme exports:
77
+ - `ThemeService`
78
+ - `THEME_CONFIG`
79
+ - `themes`, `darkThemePrefix`
63
80
 
64
- ## Package Structure
81
+ Adaptive exports:
82
+ - `AdaptiveService`
83
+ - `IfDeviceDirective`
84
+ - `DEVICE_BREAKPOINTS`, `defaultBreakpoints`
65
85
 
66
- - **seo** - SEO management (title, meta, open graph, twitter, json-ld) with route tracking.
67
- - **lang** - signals-based localization with dynamic dictionary loading.
68
- - **theme** - light/dark theme management with system sync.
69
- - **adaptive** - responsive breakpoints tracking and device-specific rendering.
86
+ Routes and SEO exports:
87
+ - `RouteWatcher`
88
+ - `SeoService`
89
+ - `SeoRouteListener`
70
90
 
71
- ---
91
+ ## `provideReInit` Configuration
72
92
 
73
- ## Localization: LangService, LangPipe, LangDirective
93
+ `AppConfig` includes:
94
+ - `locale: LocaleConfig` (required)
95
+ - `theme?: ThemeConfig`
96
+ - `breakpoints?: DeviceBreakpoints`
97
+ - `langPipe?: LangPipeConfig`
98
+ - `langMissingKeyHandler?: LangMissingKeyHandler`
74
99
 
75
- - [`LangService`](`src/lang/lang.service.ts`) - manages current language and dictionaries (static/dynamic), provides signal selectors for translation by key.
76
- - [`LangPipe`](`src/lang/lang.pipe.ts`), `pure: false` - dynamically translates strings by key and parameters: `{{ 'app.hello' | lang:{ name: userName } }}`. Cache options: provide `LANG_PIPE_CONFIG` (defaults: `ttlMs: 300000`, `maxCacheSize: 500`, optional `placeholder`).
77
- - [`LangDirective`](`src/lang/lang.directive.ts`) - directive for automatic translation of attributes (`title`, `label`, `placeholder`) and text. Usage: `<input placeholder="common.search" reLang />`. Explicit key: `<div [reLang]="'common.hello'"></div>` or `<div [reLangKey]="'common.hello'"></div>`. Explicit attribute map: `<input [reLangAttrs]="{ 'aria-label': 'forms.username' }" />`. Config object: `<div [reLang]="{ mode: 'only-content', textKey: 'common.hello', attrs: { 'aria-label': 'forms.username' } }"></div>`.
78
- - [`LANG_CONFIG`] - InjectionToken with translation source configuration.
79
- - Types: `Langs`, `LangParams`, `LangModel`, `LangDto`, `LocaleConfig`.
100
+ It also wires integration tokens from `@reforgium/internal`:
101
+ - `TRANSLATION`
102
+ - `SELECTED_LANG`
103
+ - `CHANGE_LANG`
104
+ - `SELECTED_THEME`
105
+ - `CHANGE_THEME`
106
+ - `CURRENT_DEVICE`
107
+
108
+ ## Localization
109
+
110
+ ### `LocaleConfig`
80
111
 
81
- Example:
82
112
  ```ts
83
- import { Component, computed, inject } from '@angular/core';
84
- import { LangPipe, LangService } from '.../lang';
85
-
86
- @Component({
87
- selector: 'app-hello',
88
- standalone: true,
89
- imports: [LangPipe],
90
- template: `
91
- <h1>{{ 'common.hello' | lang }}</h1>
92
- <p>{{ 'users.welcome' | lang: { name: userName } }}</p>
93
- <button (click)="switch()">RU/KG</button>
94
- `,
95
- })
96
- export class HelloComponent {
97
- private lang = inject(LangService);
98
- userName = 'Vasya';
99
-
100
- switch() {
101
- this.lang.setLang(this.lang.currentLang() === 'ru' ? 'kg' : 'ru');
102
- }
103
- }
113
+ type LocaleConfig = {
114
+ url: string;
115
+ isFromAssets: boolean;
116
+ defaultLang?: Langs;
117
+ fallbackLang?: Langs;
118
+ defaultValue?: string;
119
+ kgValue?: 'kg' | 'ky';
120
+ supportedLangs?: readonly string[];
121
+ preloadNamespaces?: readonly string[];
122
+ requestBuilder?: (ctx: {
123
+ ns: string;
124
+ lang: Langs;
125
+ isFromAssets: boolean;
126
+ baseUrl: string;
127
+ }) => string;
128
+ requestOptionsFactory?: (ctx: {
129
+ ns: string;
130
+ lang: Langs;
131
+ isFromAssets: boolean;
132
+ baseUrl: string;
133
+ }) => {
134
+ headers?: HttpHeaders | Record<string, string | string[]>;
135
+ params?: HttpParams | Record<string, string | number | boolean | readonly (string | number | boolean)[]>;
136
+ withCredentials?: boolean;
137
+ responseType?: 'json' | 'text' | 'blob' | 'arraybuffer';
138
+ context?: HttpContext;
139
+ transferCache?: boolean | { includeHeaders?: string[] };
140
+ };
141
+ responseAdapter?: (response: unknown, ctx: {
142
+ ns: string;
143
+ lang: Langs;
144
+ isFromAssets: boolean;
145
+ }) => LangModel | LangDto[];
146
+ batchRequestBuilder?: (ctx: {
147
+ namespaces: readonly string[];
148
+ lang: Langs;
149
+ isFromAssets: boolean;
150
+ baseUrl: string;
151
+ }) => string;
152
+ batchResponseAdapter?: (response: unknown, ctx: {
153
+ namespaces: readonly string[];
154
+ lang: Langs;
155
+ isFromAssets: boolean;
156
+ }) => Record<string, LangModel | LangDto[]>;
157
+ namespaceCache?: {
158
+ maxNamespaces?: number;
159
+ ttlMs?: number;
160
+ };
161
+ };
104
162
  ```
105
163
 
106
- ---
164
+ ### Behavior Notes
107
165
 
108
- ## Theming: ThemeService
166
+ - Built-in languages: `ru`, `kg`, `en`.
167
+ - Alias `ky` is accepted and normalized to internal `kg`.
168
+ - `supportedLangs` lets you add custom language codes.
169
+ - If key is missing, `defaultValue` is used (or missing key handler result).
170
+ - Stale HTTP responses are ignored when language changes mid-flight.
109
171
 
110
- - [`ThemeService`](`src/theme/theme.service.ts`) - manages light/dark theme.
111
- - Methods: `switch()`, `isLight()`, etc.
172
+ ### Basic Usage
112
173
 
113
- ---
174
+ ```ts
175
+ const lang = inject(LangService);
114
176
 
115
- ## Adaptivity: AdaptiveService and IfDeviceDirective
177
+ lang.setLang('en');
178
+ await lang.loadNamespace('layout');
116
179
 
117
- - [`AdaptiveService`](`src/adaptive/adaptive.service.ts`) - tracks sizes, device type (mobile/tablet/desktop), supports signals for width/height/orientation.
118
- - [`IfDeviceDirective`](`src/adaptive/device-type.directive.ts`) - structural directive for conditional display by device type.
180
+ const title = lang.get('layout.title');
181
+ const welcome = lang.get('layout.welcome', { name: 'John' });
182
+ ```
119
183
 
120
- How `IfDeviceDirective` works:
121
- - Selector: `[reIfDevice]` (standalone).
122
- - Inputs:
123
- - `reIfDevice: Devices | Devices[]` - allowed device types (one or list).
124
- - `inverse?: boolean` - invert condition (show everywhere except specified devices).
125
- - Reactively tracks current device type from `AdaptiveService.device`, creates/clears embedded view.
184
+ ### API Mode With Custom Payload
126
185
 
127
- Usage examples:
128
- ```html
129
- <!-- 1) Mobile only -->
130
- <div *reIfDevice="'mobile'">Mobile phones only</div>
186
+ ```ts
187
+ provideReInit({
188
+ locale: {
189
+ url: '/api/i18n',
190
+ isFromAssets: false,
191
+ requestBuilder: ({ ns, lang, baseUrl }) => `${baseUrl}/${ns}?language=${lang}`,
192
+ requestOptionsFactory: () => ({
193
+ headers: { 'x-tenant-id': 'tenant-1' },
194
+ withCredentials: true,
195
+ responseType: 'json',
196
+ }),
197
+ responseAdapter: (response) => (response as { data: LangDto[] }).data,
198
+ },
199
+ });
200
+ ```
201
+
202
+ ### Batch Namespace Loading
203
+
204
+ ```ts
205
+ provideReInit({
206
+ locale: {
207
+ url: '/api/i18n',
208
+ isFromAssets: false,
209
+ batchRequestBuilder: ({ lang, namespaces }) =>
210
+ `/api/i18n/batch?language=${lang}&ns=${namespaces.join(',')}`,
211
+ batchResponseAdapter: (response) => response as Record<string, LangDto[]>,
212
+ },
213
+ });
131
214
 
132
- <!-- 2) Tablets and desktops -->
133
- <div *reIfDevice="['tablet', 'desktop']">Tablet and desktop</div>
215
+ const lang = inject(LangService);
216
+ await lang.loadNamespaces(['layout', 'common', 'breadcrumbs']);
217
+ ```
218
+
219
+ ### Namespace Cache Policy
134
220
 
135
- <!-- 3) Hide on desktop (inverse) -->
136
- <div *reIfDevice="'desktop'; inverse: true">Visible everywhere except desktop</div>
221
+ ```ts
222
+ provideReInit({
223
+ locale: {
224
+ url: '/assets/i18n',
225
+ isFromAssets: true,
226
+ namespaceCache: {
227
+ maxNamespaces: 20,
228
+ ttlMs: 10 * 60 * 1000,
229
+ },
230
+ },
231
+ });
137
232
  ```
138
233
 
139
- ---
234
+ Cache control methods:
235
+ - `lang.evictNamespace('layout')`
236
+ - `lang.clearNamespaceCache()`
237
+
238
+ ### Copy-Paste Recipes
140
239
 
141
- ## SEO: SeoService and SeoRouteListener
142
- - [`SeoService`](`src/seo/seo.service.ts`) - service for working with SEO
240
+ #### 1. Assets JSON (simple frontend-only i18n)
143
241
 
144
- Example:
145
242
  ```ts
146
- const service = inject(SeoService);
243
+ provideReInit({
244
+ locale: {
245
+ url: '/assets/locales',
246
+ isFromAssets: true,
247
+ defaultLang: 'en',
248
+ fallbackLang: 'en',
249
+ preloadNamespaces: ['layout', 'common'],
250
+ },
251
+ });
252
+ ```
253
+
254
+ Expected files:
255
+ - `/assets/locales/layout.en.json`
256
+ - `/assets/locales/common.en.json`
147
257
 
148
- service.setTitle('My page');
149
- service.setDescription('My page description');
150
- service.setKeywords(['angular', 'signals', 'seo', 'meta']);
151
- service.setCanonical('https://example.com/page');
152
- service.setTwitter({ card: 'summary_large_image', site: '@myapp' });
258
+ #### 2. Flat API DTO (`LangDto[]`)
259
+
260
+ ```ts
261
+ provideReInit({
262
+ locale: {
263
+ url: '/api/i18n',
264
+ isFromAssets: false,
265
+ requestBuilder: ({ ns, lang, baseUrl }) => `${baseUrl}/${ns}?language=${lang}`,
266
+ // default parser already supports LangDto[]
267
+ },
268
+ });
153
269
  ```
154
270
 
155
- - [`SeoRouteListener`](`src/seo/seo-route.listener.ts`) - service for working with SEO, with route tracking
271
+ Expected payload:
272
+
273
+ ```json
274
+ [
275
+ { "namespace": "layout", "code": "layout.title", "localization": "Dashboard" }
276
+ ]
277
+ ```
278
+
279
+ #### 3. Envelope API DTO (`{ data: [...] }`)
156
280
 
157
- Example:
158
281
  ```ts
159
- bootstrapApplication(AppComponent, {
160
- providers: [provideRouter(routes)],
161
- }).then(ref => {
162
- // base site URL
163
- ref.injector.get(SeoRouteListener).init('https://example.com');
282
+ provideReInit({
283
+ locale: {
284
+ url: '/api/i18n',
285
+ isFromAssets: false,
286
+ responseAdapter: (response) => (response as { data: LangDto[] }).data,
287
+ },
164
288
  });
289
+ ```
165
290
 
166
- export default [
167
- {
168
- path: '',
169
- loadComponent: () => import('./pages/home.page').then(m => m.HomePage),
170
- data: {
171
- title: 'Home - MyApp',
172
- description: 'Home page description',
173
- og: { image: 'https://example.com/og/home.png', type: 'website', site_name: 'MyApp' },
174
- twitter: { card: 'summary_large_image', site: '@myapp' },
175
- jsonld: {
176
- '@context': 'https://schema.org',
177
- '@type': 'WebSite',
178
- name: 'MyApp',
179
- url: 'https://example.com'
180
- }
181
- },
291
+ Expected payload:
292
+
293
+ ```json
294
+ {
295
+ "data": [{ "namespace": "layout", "code": "layout.title", "localization": "Dashboard" }]
296
+ }
297
+ ```
298
+
299
+ #### 4. Multi-tenant API with headers and params
300
+
301
+ ```ts
302
+ provideReInit({
303
+ locale: {
304
+ url: '/api/i18n',
305
+ isFromAssets: false,
306
+ requestOptionsFactory: ({ lang }) => ({
307
+ headers: { 'x-tenant-id': 'tenant-1' },
308
+ params: { region: 'eu', language: lang },
309
+ withCredentials: true,
310
+ }),
311
+ requestBuilder: ({ ns, baseUrl }) => `${baseUrl}/${ns}`,
182
312
  },
183
- ]
313
+ });
314
+ ```
315
+
316
+ #### 5. Batch endpoint (`/batch`) for many namespaces
317
+
318
+ ```ts
319
+ provideReInit({
320
+ locale: {
321
+ url: '/api/i18n',
322
+ isFromAssets: false,
323
+ batchRequestBuilder: ({ namespaces, lang, baseUrl }) =>
324
+ `${baseUrl}/batch?language=${lang}&ns=${namespaces.join(',')}`,
325
+ batchResponseAdapter: (response) =>
326
+ response as Record<string, LangDto[]>,
327
+ },
328
+ });
329
+
330
+ const lang = inject(LangService);
331
+ await lang.loadNamespaces(['layout', 'common', 'breadcrumbs']);
332
+ ```
333
+
334
+ ## `LangPipe`
335
+
336
+ - Name: `lang`
337
+ - Standalone, impure pipe (`pure: false`)
338
+ - Internal cache with TTL and max size
339
+ - Configurable placeholder while namespace is loading
340
+
341
+ ```html
342
+ <h1>{{ 'layout.title' | lang }}</h1>
343
+ <p>{{ 'users.welcome' | lang: { name: userName, count: 12 } }}</p>
344
+ ```
345
+
346
+ `LangPipeConfig` (via `LANG_PIPE_CONFIG` or `provideReInit.langPipe`):
347
+ - `ttlMs?: number`
348
+ - `maxCacheSize?: number`
349
+ - `placeholder?: string | ((query: string) => string)`
350
+
351
+ ## `reLang` Directive
352
+
353
+ Auto-localizes text and selected attributes.
354
+
355
+ Supported modes:
356
+ - `'all'`
357
+ - `'only-content'`
358
+ - `'only-placeholder'`
359
+ - `'only-label'`
360
+ - `'only-title'`
361
+
362
+ Examples:
363
+
364
+ ```html
365
+ <button reLang title="common.save">common.save</button>
366
+ <button [reLang]="'only-title'" title="common.cancel">Cancel</button>
367
+ <input reLang [langForAttr]="'aria-label'" aria-label="forms.username" />
368
+ <div [reLang]="{ mode: 'only-content', textKey: 'layout.title' }"></div>
369
+ ```
370
+
371
+ ## Typed Lang Keys (Opt-in)
372
+
373
+ `LangService.get()` and `LangService.observe()` support typed keys via module augmentation.
374
+
375
+ Generate keys from locale JSON in consumer app:
376
+
377
+ ```bash
378
+ npx presentia-gen-lang-keys --locales src/assets/locales --out src/types/presentia-lang-keys.d.ts
379
+ ```
380
+
381
+ This generates:
382
+ - `declare module '@reforgium/presentia'`
383
+ - `interface LangKeyRegistry { keys: 'layout.title' | 'common.save' | ... }`
384
+
385
+ After generation, invalid keys in `get()`/`observe()` are compile-time errors.
386
+
387
+ ## Theme
388
+
389
+ `ThemeService`:
390
+ - `theme()` current theme (`'light' | 'dark'`)
391
+ - `isLight()`
392
+ - `switch(theme?)` explicit switch or toggle
393
+
394
+ ```ts
395
+ const theme = inject(ThemeService);
396
+ theme.switch('dark');
184
397
  ```
185
398
 
186
- ---
399
+ `ThemeConfig`:
400
+ - `defaultTheme?: 'light' | 'dark'`
401
+ - `darkThemePrefix?: string`
187
402
 
188
- ## Compatibility
403
+ ## Adaptive
189
404
 
190
- - Angular: 18+
191
- - Signals: required
192
- - Zone.js: optional
405
+ `AdaptiveService` provides reactive signals:
406
+ - `device()` -> `'desktop' | 'tablet' | 'mobile'`
407
+ - `width()`
408
+ - `height()`
409
+ - `isDesktop()`
410
+ - `isPortrait()`
411
+
412
+ `*reIfDevice` structural directive:
413
+
414
+ ```html
415
+ <div *reIfDevice="'desktop'">Desktop only</div>
416
+ <div *reIfDevice="['mobile', 'tablet']">Mobile and tablet</div>
417
+ <div *reIfDevice="'mobile'; inverse: true">Hidden on mobile</div>
418
+ ```
419
+
420
+ Breakpoints are configurable via `DeviceBreakpoints`.
421
+
422
+ ## Route State Helper
423
+
424
+ `RouteWatcher` gives reactive deepest-route snapshot:
425
+ - `params()`
426
+ - `query()`
427
+ - `data()`
428
+ - `url()`
429
+ - `fragment()`
430
+ - `state()`
431
+ - `selectData<T>(key)`
432
+
433
+ Use when you want route-derived UI state without manual router subscriptions.
434
+
435
+ ## SEO
436
+
437
+ ### `SeoService`
438
+
439
+ Methods:
440
+ - `setTitle`
441
+ - `setDescription`
442
+ - `setKeywords`
443
+ - `setRobots`
444
+ - `setCanonical`
445
+ - `setOg`
446
+ - `setTwitter`
447
+ - `setJsonLd`
448
+
449
+ ### `SeoRouteListener`
450
+
451
+ Auto-applies SEO from route `data` on navigation:
452
+
453
+ ```ts
454
+ const seoRoute = inject(SeoRouteListener);
455
+ seoRoute.init('https://example.com');
456
+ ```
193
457
 
194
- ---
458
+ Expected route `data` keys:
459
+ - `title`
460
+ - `description`
461
+ - `robots`
462
+ - `canonical`
463
+ - `og`
464
+ - `twitter`
465
+ - `jsonld`
466
+
467
+ ## Performance Notes
468
+
469
+ - Use `preloadNamespaces` for first-screen keys.
470
+ - Use `loadNamespaces` + batch hooks for chatty APIs.
471
+ - Use `namespaceCache` limits to control memory in long-lived sessions.
472
+ - Keep `LangPipe.maxCacheSize` realistic for your screen complexity.
473
+
474
+ ## Troubleshooting
475
+
476
+ If some texts appear untranslated until reload:
477
+ - Ensure keys are valid (`namespace.key` format).
478
+ - Confirm namespace is loaded for current language.
479
+ - Check API adapter (`responseAdapter`) returns correct shape.
480
+ - Verify route/lazy components do not call `get()` before namespace load if you require strict first render.
481
+ - Prefer `observe()` / `lang` pipe for reactive updates.
482
+
483
+ If custom language is ignored:
484
+ - Add it to `supportedLangs`.
485
+ - Pass lowercase code or rely on normalization.
486
+
487
+ If `ky`/`kg` behavior is unexpected:
488
+ - `ky` input is normalized to internal `kg`.
489
+ - `currentLang()` returns `kgValue` when language is `kg`.
195
490
 
196
491
  ## License
197
492