@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 +427 -132
- package/bin/presentia-gen-lang-keys.mjs +111 -0
- package/fesm2022/reforgium-presentia.mjs +362 -118
- package/fesm2022/reforgium-presentia.mjs.map +1 -1
- package/package.json +5 -2
- package/types/reforgium-presentia.d.ts +133 -55
- package/types/reforgium-presentia.d.ts.map +1 -1
package/README.md
CHANGED
|
@@ -3,195 +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
|
-
- `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:
|
|
51
|
+
ttlMs: 300_000,
|
|
45
52
|
maxCacheSize: 500,
|
|
53
|
+
placeholder: '...',
|
|
46
54
|
},
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
81
|
+
Adaptive exports:
|
|
82
|
+
- `AdaptiveService`
|
|
83
|
+
- `IfDeviceDirective`
|
|
84
|
+
- `DEVICE_BREAKPOINTS`, `defaultBreakpoints`
|
|
65
85
|
|
|
66
|
-
|
|
67
|
-
-
|
|
68
|
-
-
|
|
69
|
-
-
|
|
86
|
+
Routes and SEO exports:
|
|
87
|
+
- `RouteWatcher`
|
|
88
|
+
- `SeoService`
|
|
89
|
+
- `SeoRouteListener`
|
|
70
90
|
|
|
71
|
-
|
|
91
|
+
## `provideReInit` Configuration
|
|
72
92
|
|
|
73
|
-
|
|
93
|
+
`AppConfig` includes:
|
|
94
|
+
- `locale: LocaleConfig` (required)
|
|
95
|
+
- `theme?: ThemeConfig`
|
|
96
|
+
- `breakpoints?: DeviceBreakpoints`
|
|
97
|
+
- `langPipe?: LangPipeConfig`
|
|
98
|
+
- `langMissingKeyHandler?: LangMissingKeyHandler`
|
|
74
99
|
|
|
75
|
-
|
|
76
|
-
-
|
|
77
|
-
-
|
|
78
|
-
-
|
|
79
|
-
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
- Methods: `switch()`, `isLight()`, etc.
|
|
172
|
+
### Basic Usage
|
|
112
173
|
|
|
113
|
-
|
|
174
|
+
```ts
|
|
175
|
+
const lang = inject(LangService);
|
|
114
176
|
|
|
115
|
-
|
|
177
|
+
lang.setLang('en');
|
|
178
|
+
await lang.loadNamespace('layout');
|
|
116
179
|
|
|
117
|
-
|
|
118
|
-
|
|
180
|
+
const title = lang.get('layout.title');
|
|
181
|
+
const welcome = lang.get('layout.welcome', { name: 'John' });
|
|
182
|
+
```
|
|
119
183
|
|
|
120
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
133
|
-
|
|
215
|
+
const lang = inject(LangService);
|
|
216
|
+
await lang.loadNamespaces(['layout', 'common', 'breadcrumbs']);
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Namespace Cache Policy
|
|
134
220
|
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
##
|
|
403
|
+
## Adaptive
|
|
189
404
|
|
|
190
|
-
|
|
191
|
-
-
|
|
192
|
-
-
|
|
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
|
|