@reforgium/presentia 1.2.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,190 +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 (20+).**
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
-
33
- Example integration in the root application:
34
35
  ```ts
36
+ import { bootstrapApplication } from '@angular/platform-browser';
37
+ import { provideReInit } from '@reforgium/presentia';
38
+
35
39
  bootstrapApplication(AppComponent, {
36
40
  providers: [
37
41
  provideReInit({
38
42
  locale: {
43
+ url: '/assets/i18n',
39
44
  isFromAssets: true,
40
45
  defaultLang: 'ru',
46
+ fallbackLang: 'en',
47
+ supportedLangs: ['de'],
48
+ preloadNamespaces: ['layout', 'common'],
41
49
  },
42
- theme: {
43
- defaultTheme: 'dark',
44
- darkThemePrefix: 're-dark',
45
- },
46
- breakpoints: {
47
- 'mobile': '(max-width: 719px)',
48
- 'tablet': '(min-width: 720px) and (max-width: 1399px)',
49
- 'desktop-s': '(min-width: 1400px) and (max-width: 1919px)',
50
- 'desktop': '(min-width: 1920px)',
50
+ langPipe: {
51
+ ttlMs: 300_000,
52
+ maxCacheSize: 500,
53
+ placeholder: '...',
51
54
  },
55
+ langMissingKeyHandler: (key, ctx) => `[${ctx.lang}] ${key}`,
56
+ theme: { defaultTheme: 'light', darkThemePrefix: 'dark' },
52
57
  }),
53
58
  ],
54
59
  });
55
60
  ```
56
61
 
57
- ---
62
+ ## What You Get
58
63
 
59
- ## Package Structure
64
+ Main provider:
65
+ - `provideReInit(config: AppConfig)`
60
66
 
61
- - **seo** — SEO management (title, meta, open graph, twitter, json-ld) with route tracking.
62
- - **lang** — signals-based localization with dynamic dictionary loading.
63
- - **theme** — light/dark theme management with system sync.
64
- - **adaptive** — responsive breakpoints tracking and device-specific rendering.
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.
65
75
 
66
- ---
76
+ Theme exports:
77
+ - `ThemeService`
78
+ - `THEME_CONFIG`
79
+ - `themes`, `darkThemePrefix`
67
80
 
68
- ## Localization: LangService, LangPipe, LangDirective
81
+ Adaptive exports:
82
+ - `AdaptiveService`
83
+ - `IfDeviceDirective`
84
+ - `DEVICE_BREAKPOINTS`, `defaultBreakpoints`
69
85
 
70
- - [`LangService`](`src/lang/lang.service.ts`) — manages current language and dictionaries (static/dynamic), provides signal selectors for translation by key.
71
- - [`LangPipe`](`src/lang/lang.pipe.ts`), `pure: false` — dynamically translates strings by key and parameters: `{{ 'app.hello' | lang:{ name: userName } }}`.
72
- - [`LangDirective`](`src/lang/lang.directive.ts`) — directive for automatic translation of attributes (`title`, `label`, `placeholder`). Usage: `<input placeholder="common.search" reLang />`.
73
- - [`LANG_CONFIG`] — InjectionToken with translation source configuration.
74
- - Types: `Langs`, `LangParams`, `LangModel`, `LangDto`, `LocaleConfig`.
86
+ Routes and SEO exports:
87
+ - `RouteWatcher`
88
+ - `SeoService`
89
+ - `SeoRouteListener`
90
+
91
+ ## `provideReInit` Configuration
92
+
93
+ `AppConfig` includes:
94
+ - `locale: LocaleConfig` (required)
95
+ - `theme?: ThemeConfig`
96
+ - `breakpoints?: DeviceBreakpoints`
97
+ - `langPipe?: LangPipeConfig`
98
+ - `langMissingKeyHandler?: LangMissingKeyHandler`
99
+
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`
75
111
 
76
- Example:
77
112
  ```ts
78
- import { Component, computed, inject } from '@angular/core';
79
- import { LangPipe, LangService } from '.../lang';
80
-
81
- @Component({
82
- selector: 'app-hello',
83
- standalone: true,
84
- imports: [LangPipe],
85
- template: `
86
- <h1>{{ 'common.hello' | lang }}</h1>
87
- <p>{{ 'users.welcome' | lang: { name: userName } }}</p>
88
- <button (click)="switch()">RU/KG</button>
89
- `,
90
- })
91
- export class HelloComponent {
92
- private lang = inject(LangService);
93
- userName = 'Vasya';
94
-
95
- switch() {
96
- this.lang.setLang(this.lang.currentLang() === 'ru' ? 'kg' : 'ru');
97
- }
98
- }
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
+ };
99
162
  ```
100
163
 
101
- ---
164
+ ### Behavior Notes
102
165
 
103
- ## 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.
104
171
 
105
- - [`ThemeService`](`src/theme/theme.service.ts`) — manages light/dark theme.
106
- - Methods: `switch()`, `isLight()`, etc.
172
+ ### Basic Usage
107
173
 
108
- ---
174
+ ```ts
175
+ const lang = inject(LangService);
109
176
 
110
- ## Adaptivity: AdaptiveService and IfDeviceDirective
177
+ lang.setLang('en');
178
+ await lang.loadNamespace('layout');
111
179
 
112
- - [`AdaptiveService`](`src/adaptive/adaptive.service.ts`) tracks sizes, device type (mobile/tablet/desktop), supports signals for width/height/orientation.
113
- - [`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
+ ```
114
183
 
115
- How `IfDeviceDirective` works:
116
- - Selector: `[reIfDevice]` (standalone).
117
- - Inputs:
118
- - `reIfDevice: Devices | Devices[]` — allowed device types (one or list).
119
- - `inverse?: boolean` — invert condition (show everywhere except specified devices).
120
- - Reactively tracks current device type from `AdaptiveService.device`, creates/clears embedded view.
184
+ ### API Mode With Custom Payload
121
185
 
122
- Usage examples:
123
- ```html
124
- <!-- 1) Mobile only -->
125
- <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
+ });
214
+
215
+ const lang = inject(LangService);
216
+ await lang.loadNamespaces(['layout', 'common', 'breadcrumbs']);
217
+ ```
126
218
 
127
- <!-- 2) Tablets and desktops -->
128
- <div *reIfDevice="['tablet', 'desktop']">Tablet and desktop</div>
219
+ ### Namespace Cache Policy
129
220
 
130
- <!-- 3) Hide on desktop (inverse) -->
131
- <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
+ });
132
232
  ```
133
233
 
134
- ---
234
+ Cache control methods:
235
+ - `lang.evictNamespace('layout')`
236
+ - `lang.clearNamespaceCache()`
237
+
238
+ ### Copy-Paste Recipes
135
239
 
136
- ## SEO: SeoService and SeoRouteListener
137
- - [`SeoService`](`src/seo/seo.service.ts`) — service for working with SEO
240
+ #### 1. Assets JSON (simple frontend-only i18n)
138
241
 
139
- Example:
140
242
  ```ts
141
- 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`
257
+
258
+ #### 2. Flat API DTO (`LangDto[]`)
142
259
 
143
- service.setTitle('My page');
144
- service.setDescription('My page description');
145
- service.setKeywords(['angular', 'signals', 'seo', 'meta']);
146
- service.setCanonical('https://example.com/page');
147
- service.setTwitter({ card: 'summary_large_image', site: '@myapp' });
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
+ });
148
269
  ```
149
270
 
150
- - [`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: [...] }`)
151
280
 
152
- Example:
153
281
  ```ts
154
- bootstrapApplication(AppComponent, {
155
- providers: [provideRouter(routes)],
156
- }).then(ref => {
157
- // base site URL
158
- 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
+ },
159
288
  });
289
+ ```
160
290
 
161
- export default [
162
- {
163
- path: '',
164
- loadComponent: () => import('./pages/home.page').then(m => m.HomePage),
165
- data: {
166
- title: 'Home — MyApp',
167
- description: 'Home page description',
168
- og: { image: 'https://example.com/og/home.png', type: 'website', site_name: 'MyApp' },
169
- twitter: { card: 'summary_large_image', site: '@myapp' },
170
- jsonld: {
171
- '@context': 'https://schema.org',
172
- '@type': 'WebSite',
173
- name: 'MyApp',
174
- url: 'https://example.com'
175
- }
176
- },
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}`,
177
312
  },
178
- ]
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']);
179
332
  ```
180
333
 
181
- ---
334
+ ## `LangPipe`
182
335
 
183
- ## Compatibility
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
+ ```
184
345
 
185
- - Angular: 18+
186
- - Signals: required
187
- - Zone.js: optional
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');
397
+ ```
398
+
399
+ `ThemeConfig`:
400
+ - `defaultTheme?: 'light' | 'dark'`
401
+ - `darkThemePrefix?: string`
402
+
403
+ ## Adaptive
404
+
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
+ ```
188
457
 
189
- ---
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`.
190
490
 
191
491
  ## License
192
492