@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 +429 -129
- package/bin/presentia-gen-lang-keys.mjs +111 -0
- package/fesm2022/reforgium-presentia.mjs +649 -172
- package/fesm2022/reforgium-presentia.mjs.map +1 -1
- package/package.json +5 -2
- package/types/reforgium-presentia.d.ts +221 -93
- package/types/reforgium-presentia.d.ts.map +1 -1
package/README.md
CHANGED
|
@@ -3,190 +3,490 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@reforgium/presentia)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
30
|
+
npm i @reforgium/presentia
|
|
22
31
|
```
|
|
23
32
|
|
|
24
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
64
|
+
Main provider:
|
|
65
|
+
- `provideReInit(config: AppConfig)`
|
|
60
66
|
|
|
61
|
-
|
|
62
|
-
-
|
|
63
|
-
-
|
|
64
|
-
-
|
|
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
|
-
|
|
81
|
+
Adaptive exports:
|
|
82
|
+
- `AdaptiveService`
|
|
83
|
+
- `IfDeviceDirective`
|
|
84
|
+
- `DEVICE_BREAKPOINTS`, `defaultBreakpoints`
|
|
69
85
|
|
|
70
|
-
|
|
71
|
-
-
|
|
72
|
-
-
|
|
73
|
-
-
|
|
74
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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
|
-
|
|
106
|
-
- Methods: `switch()`, `isLight()`, etc.
|
|
172
|
+
### Basic Usage
|
|
107
173
|
|
|
108
|
-
|
|
174
|
+
```ts
|
|
175
|
+
const lang = inject(LangService);
|
|
109
176
|
|
|
110
|
-
|
|
177
|
+
lang.setLang('en');
|
|
178
|
+
await lang.loadNamespace('layout');
|
|
111
179
|
|
|
112
|
-
|
|
113
|
-
|
|
180
|
+
const title = lang.get('layout.title');
|
|
181
|
+
const welcome = lang.get('layout.welcome', { name: 'John' });
|
|
182
|
+
```
|
|
114
183
|
|
|
115
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
128
|
-
<div *reIfDevice="['tablet', 'desktop']">Tablet and desktop</div>
|
|
219
|
+
### Namespace Cache Policy
|
|
129
220
|
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
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
|
-
|
|
186
|
-
-
|
|
187
|
-
-
|
|
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
|
|