@seliseblocks/blocks-angular-localization 0.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.
package/README.md
ADDED
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
# @seliseblocks/blocks-angular-localization
|
|
2
|
+
|
|
3
|
+
Standalone Angular SDK for SELISE UILM (Unified Internationalization & Localization Management).
|
|
4
|
+
|
|
5
|
+
**Zero external translation dependencies** - no Transloco, ngx-translate, or similar. Built entirely on Angular signals with two loading strategies, two-tier caching (in-memory + IndexedDB), local JSON fallback, module aliasing, and route-level scoping.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Setup
|
|
10
|
+
|
|
11
|
+
### 1. Choose a loading strategy
|
|
12
|
+
|
|
13
|
+
The SDK supports two strategies for loading translations:
|
|
14
|
+
|
|
15
|
+
| Strategy | Behaviour | Best for |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| `'modular'` (default) | Lazy per-route loading via `provideUilmScope()`. Shows empty placeholders while fetching. | Large apps with many modules |
|
|
18
|
+
| `'eager'` | All modules fetched **before** the app renders (blocks bootstrap). No flicker. | Smaller apps or flicker-sensitive UIs |
|
|
19
|
+
|
|
20
|
+
### 2. App-level configuration
|
|
21
|
+
|
|
22
|
+
#### Modular strategy (default)
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
// app.config.ts
|
|
26
|
+
import { provideBlocksLocalization } from '@seliseblocks/blocks-angular-localization';
|
|
27
|
+
|
|
28
|
+
export const appConfig: ApplicationConfig = {
|
|
29
|
+
providers: [
|
|
30
|
+
provideBlocksLocalization({
|
|
31
|
+
uilmApiBaseUrl: 'YOUR_UILM_API_BASE_URL',
|
|
32
|
+
projectKey: 'YOUR_PROJECT_KEY',
|
|
33
|
+
accessToken: 'OPTIONAL_BEARER_TOKEN',
|
|
34
|
+
availableLangs: ['en', 'de', 'fr', 'it'],
|
|
35
|
+
defaultLang: 'en',
|
|
36
|
+
localeMapping: { en: 'en-US', de: 'de-DE', fr: 'fr-FR', it: 'it-IT' },
|
|
37
|
+
prefixKeysWithModule: true,
|
|
38
|
+
cacheTimeout: 300_000, // 5 min TTL (0 = no cache)
|
|
39
|
+
cacheStorage: 'indexeddb', // persist across sessions (default: 'memory')
|
|
40
|
+
revalidateInBackground: true, // serve from cache, refresh from API silently
|
|
41
|
+
preloadModules: ['common'], // only shared module preloaded
|
|
42
|
+
}),
|
|
43
|
+
],
|
|
44
|
+
};
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Then use `provideUilmScope()` on each route to load module-specific translations lazily (see [Route-level module loading](#3-route-level-module-loading)).
|
|
48
|
+
|
|
49
|
+
#### Eager strategy
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
// app.config.ts
|
|
53
|
+
import { provideBlocksLocalization } from '@seliseblocks/blocks-angular-localization';
|
|
54
|
+
|
|
55
|
+
export const appConfig: ApplicationConfig = {
|
|
56
|
+
providers: [
|
|
57
|
+
provideBlocksLocalization({
|
|
58
|
+
uilmApiBaseUrl: 'YOUR_UILM_API_BASE_URL',
|
|
59
|
+
projectKey: 'YOUR_PROJECT_KEY',
|
|
60
|
+
accessToken: 'OPTIONAL_BEARER_TOKEN',
|
|
61
|
+
availableLangs: ['en', 'de', 'fr', 'it'],
|
|
62
|
+
defaultLang: 'en',
|
|
63
|
+
localeMapping: { en: 'en-US', de: 'de-DE', fr: 'fr-FR', it: 'it-IT' },
|
|
64
|
+
prefixKeysWithModule: true,
|
|
65
|
+
strategy: 'eager',
|
|
66
|
+
cacheStorage: 'indexeddb',
|
|
67
|
+
revalidateInBackground: true,
|
|
68
|
+
cacheTimeout: 300_000,
|
|
69
|
+
preloadModules: [
|
|
70
|
+
{ module: 'common', alias: '' },
|
|
71
|
+
{ module: 'opportunity', alias: 'op' },
|
|
72
|
+
{ module: 'backend', alias: 'be' },
|
|
73
|
+
{ module: 'service', alias: 'sv' },
|
|
74
|
+
'dashboard',
|
|
75
|
+
'invoice',
|
|
76
|
+
'customer',
|
|
77
|
+
// ... list ALL module scopes here
|
|
78
|
+
],
|
|
79
|
+
}),
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
In eager mode, `provideUilmScope()` on routes becomes a **no-op** for the initial language (translations are already cached). Language switches re-fetch automatically in both modes.
|
|
85
|
+
|
|
86
|
+
### 3. Route-level module loading
|
|
87
|
+
|
|
88
|
+
Use `provideUilmScope` to declare which UILM modules a route needs:
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
// dashboard.route.ts
|
|
92
|
+
import { provideUilmScope } from '@seliseblocks/blocks-angular-localization';
|
|
93
|
+
|
|
94
|
+
export const DASHBOARD_ROUTE: Route[] = [{
|
|
95
|
+
path: 'dashboard',
|
|
96
|
+
providers: [
|
|
97
|
+
provideUilmScope({
|
|
98
|
+
modules: [
|
|
99
|
+
'dashboard', // prefix: dashboard.KEY
|
|
100
|
+
{ module: 'opportunity', alias: 'op' }, // prefix: op.KEY
|
|
101
|
+
{ module: 'backend', alias: 'be' }, // prefix: be.KEY
|
|
102
|
+
],
|
|
103
|
+
}),
|
|
104
|
+
],
|
|
105
|
+
children: [
|
|
106
|
+
{ path: '', loadComponent: () => import('./dashboard').then(c => c.Dashboard) },
|
|
107
|
+
],
|
|
108
|
+
}];
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### 4. Loading screen (eager mode)
|
|
112
|
+
|
|
113
|
+
When using `'eager'` strategy, you can show a loading screen while translations are being fetched. The SDK provides a ready-made component with a default UI:
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
import { UilmLoadingScreenComponent, UilmStore } from '@seliseblocks/blocks-angular-localization';
|
|
117
|
+
|
|
118
|
+
@Component({
|
|
119
|
+
imports: [UilmLoadingScreenComponent, RouterOutlet],
|
|
120
|
+
template: `
|
|
121
|
+
@if (!store.ready()) {
|
|
122
|
+
<uilm-loading-screen
|
|
123
|
+
title="Loading"
|
|
124
|
+
description="Loading translations..." />
|
|
125
|
+
} @else {
|
|
126
|
+
<router-outlet />
|
|
127
|
+
}
|
|
128
|
+
`,
|
|
129
|
+
})
|
|
130
|
+
export class AppComponent {
|
|
131
|
+
protected readonly store = inject(UilmStore);
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
#### Custom loading UI
|
|
136
|
+
|
|
137
|
+
Pass a `customTemplate` to fully replace the default loading screen with your own design:
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
@Component({
|
|
141
|
+
imports: [UilmLoadingScreenComponent, RouterOutlet],
|
|
142
|
+
template: `
|
|
143
|
+
<ng-template #brandLoader>
|
|
144
|
+
<div class="my-loader">
|
|
145
|
+
<img src="assets/logo.svg" alt="Loading" />
|
|
146
|
+
<h1>Please wait...</h1>
|
|
147
|
+
<my-spinner />
|
|
148
|
+
</div>
|
|
149
|
+
</ng-template>
|
|
150
|
+
|
|
151
|
+
@if (!store.ready()) {
|
|
152
|
+
<uilm-loading-screen [customTemplate]="brandLoader" />
|
|
153
|
+
} @else {
|
|
154
|
+
<router-outlet />
|
|
155
|
+
}
|
|
156
|
+
`,
|
|
157
|
+
})
|
|
158
|
+
export class AppComponent {
|
|
159
|
+
protected readonly store = inject(UilmStore);
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
When `customTemplate` is provided, the default logo, title, description, and progress bar are completely replaced by your template content. The outer `.uilm-loading-screen` wrapper (fixed, centered, full-screen) is preserved.
|
|
164
|
+
|
|
165
|
+
> **Note:** In eager mode the `APP_INITIALIZER` blocks Angular bootstrap, so `store.ready()` is `true` by the time the app renders. The loading screen is most useful if you combine eager preloading of some modules with lazy loading of others.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Translation APIs
|
|
170
|
+
|
|
171
|
+
### Directive: `*uilmTranslate`
|
|
172
|
+
|
|
173
|
+
Structural directive providing a translation function. Re-renders on language/translation changes.
|
|
174
|
+
|
|
175
|
+
```html
|
|
176
|
+
<section *uilmTranslate="let t">
|
|
177
|
+
<h1>{{ t('dashboard.LABEL.TITLE') }}</h1>
|
|
178
|
+
<p>{{ t('dashboard.HINT.WELCOME', { name: userName }) }}</p>
|
|
179
|
+
</section>
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
With scope (auto-prefixes keys):
|
|
183
|
+
|
|
184
|
+
```html
|
|
185
|
+
<section *uilmTranslate="let t; scope: 'dashboard'">
|
|
186
|
+
<h1>{{ t('LABEL.TITLE') }}</h1> <!-- resolves to dashboard.LABEL.TITLE -->
|
|
187
|
+
</section>
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
> **Fallback:** If a key has no matching translation, the raw key string is returned as-is. This means pre-translated or non-key values passed through `t()` render normally instead of appearing blank.
|
|
191
|
+
|
|
192
|
+
**Component setup:**
|
|
193
|
+
```typescript
|
|
194
|
+
import { UilmTranslateDirective } from '@seliseblocks/blocks-angular-localization';
|
|
195
|
+
|
|
196
|
+
@Component({
|
|
197
|
+
imports: [UilmTranslateDirective],
|
|
198
|
+
...
|
|
199
|
+
})
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Pipe: `uilmTranslate`
|
|
203
|
+
|
|
204
|
+
Inline pipe for simple translations:
|
|
205
|
+
|
|
206
|
+
```html
|
|
207
|
+
<p>{{ 'dashboard.LABEL.TITLE' | uilmTranslate }}</p>
|
|
208
|
+
<p>{{ 'dashboard.HINT.WELCOME' | uilmTranslate: { name: userName } }}</p>
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**Component setup:**
|
|
212
|
+
```typescript
|
|
213
|
+
import { UilmTranslatePipe } from '@seliseblocks/blocks-angular-localization';
|
|
214
|
+
|
|
215
|
+
@Component({
|
|
216
|
+
imports: [UilmTranslatePipe],
|
|
217
|
+
...
|
|
218
|
+
})
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Pipe: `multiLang`
|
|
222
|
+
|
|
223
|
+
Resolves a multilingual object to the value matching the active language. Falls back to the default language if the active language key is missing:
|
|
224
|
+
|
|
225
|
+
```html
|
|
226
|
+
<!-- Given: { en: 'Hello', de: 'Hallo', fr: 'Bonjour' } -->
|
|
227
|
+
{{ item.name | multiLang }} <!-- outputs 'Hello' when lang is 'en' -->
|
|
228
|
+
|
|
229
|
+
<!-- Given: { en: 'Hello' } with active lang 'de' -->
|
|
230
|
+
{{ item.name | multiLang }} <!-- falls back to 'Hello' (default lang) -->
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**Component setup:**
|
|
234
|
+
```typescript
|
|
235
|
+
import { MultiLangPipe } from '@seliseblocks/blocks-angular-localization';
|
|
236
|
+
|
|
237
|
+
@Component({
|
|
238
|
+
imports: [MultiLangPipe],
|
|
239
|
+
...
|
|
240
|
+
})
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Service: `UilmTranslateService`
|
|
244
|
+
|
|
245
|
+
Signal-based service for component classes:
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
import { UilmTranslateService } from '@seliseblocks/blocks-angular-localization';
|
|
249
|
+
|
|
250
|
+
@Component({ ... })
|
|
251
|
+
export class MyComponent {
|
|
252
|
+
private readonly uilm = inject(UilmTranslateService);
|
|
253
|
+
|
|
254
|
+
// Signal-based (reactive — updates on lang change + translation load)
|
|
255
|
+
title = this.uilm.t('dashboard.LABEL.TITLE');
|
|
256
|
+
// In template: {{ title() }}
|
|
257
|
+
|
|
258
|
+
// Batch signals
|
|
259
|
+
labels = this.uilm.tMany(['dashboard.LABEL.TITLE', 'dashboard.LABEL.SUBTITLE']);
|
|
260
|
+
// In template: {{ labels()['dashboard.LABEL.TITLE'] }}
|
|
261
|
+
|
|
262
|
+
// Sync snapshot (does NOT react to changes)
|
|
263
|
+
label = this.uilm.translate('dashboard.LABEL.TITLE');
|
|
264
|
+
|
|
265
|
+
// Active language signal
|
|
266
|
+
lang = this.uilm.activeLang; // Signal<string>
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## Language Switching
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
import { BlocksLangSwitcher } from '@seliseblocks/blocks-angular-localization';
|
|
276
|
+
|
|
277
|
+
@Component({ ... })
|
|
278
|
+
export class LangSwitcher {
|
|
279
|
+
private readonly langSwitcher = inject(BlocksLangSwitcher);
|
|
280
|
+
|
|
281
|
+
// Signal
|
|
282
|
+
currentLang = this.langSwitcher.activeLang;
|
|
283
|
+
|
|
284
|
+
switchTo(lang: string): void {
|
|
285
|
+
this.langSwitcher.setActiveLang(lang);
|
|
286
|
+
// Translations re-fetch automatically.
|
|
287
|
+
// Logs a warning and no-ops if `lang` is not in `availableLangs`.
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## Interpolation
|
|
295
|
+
|
|
296
|
+
Values from the UILM API support `{{ param }}` interpolation, including dotted paths for nested objects:
|
|
297
|
+
|
|
298
|
+
```
|
|
299
|
+
API value: "Hello {{ name }}, you have {{ count }} items"
|
|
300
|
+
API value: "Welcome {{ user.name }} from {{ user.company }}"
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
```html
|
|
304
|
+
{{ t('LABEL.GREETING', { name: 'John', count: 5 }) }}
|
|
305
|
+
<!-- Output: Hello John, you have 5 items -->
|
|
306
|
+
|
|
307
|
+
{{ t('LABEL.WELCOME', { user: { name: 'Jane', company: 'SELISE' } }) }}
|
|
308
|
+
<!-- Output: Welcome Jane from SELISE -->
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## Caching
|
|
314
|
+
|
|
315
|
+
### Two-tier cache architecture
|
|
316
|
+
|
|
317
|
+
The SDK uses a two-tier caching system for optimal performance:
|
|
318
|
+
|
|
319
|
+
| Layer | Storage | Lifetime | When active |
|
|
320
|
+
|-------|---------|----------|-------------|
|
|
321
|
+
| **L1** | In-memory `Map` | Current browser session | Always |
|
|
322
|
+
| **L2** | IndexedDB | Cross-session (survives reload) | `cacheStorage: 'indexeddb'` |
|
|
323
|
+
|
|
324
|
+
### Lookup order
|
|
325
|
+
|
|
326
|
+
```
|
|
327
|
+
1. L1 hit (in-memory, valid TTL) → return instantly
|
|
328
|
+
2. In-flight dedup → share existing HTTP Observable
|
|
329
|
+
3. L2 hit (IndexedDB, valid TTL) → populate L1, return
|
|
330
|
+
4. HTTP fetch (UILM API) → populate L1 + L2, return
|
|
331
|
+
5. Error fallback chain:
|
|
332
|
+
└─ Stale L1 → Stale L2 → Local JSON → empty {}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### Configuration
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
provideBlocksLocalization({
|
|
339
|
+
// ...
|
|
340
|
+
cacheTimeout: 300_000, // 5 min TTL (0 = no expiry, cached until cleared)
|
|
341
|
+
cacheStorage: 'indexeddb', // 'memory' (default) | 'indexeddb'
|
|
342
|
+
})
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
| Option | Type | Default | Description |
|
|
346
|
+
|---|---|---|---|
|
|
347
|
+
| `cacheTimeout` | `number` | `0` | Cache TTL in milliseconds. `0` means no expiry (cached until cleared). |
|
|
348
|
+
| `cacheStorage` | `'memory' \| 'indexeddb'` | `'memory'` | Where to persist cached translations. `'indexeddb'` enables cross-session persistence. |
|
|
349
|
+
| `revalidateInBackground` | `boolean` | `false` | When `true` and `cacheStorage` is `'indexeddb'`, serves cached translations immediately and refreshes from API in the background. No effect with `'memory'` storage. |
|
|
350
|
+
|
|
351
|
+
### How it works
|
|
352
|
+
|
|
353
|
+
- **`'memory'` (default):** L1-only. Translations are cached in a JS `Map` for the duration of the session. Page reload fetches everything fresh.
|
|
354
|
+
- **`'indexeddb'`:** L1 + L2. On first load, translations are fetched from the API and stored in both memory and IndexedDB. On subsequent page loads, translations are served instantly from IndexedDB (L2) while in-memory cache (L1) provides zero-latency lookups within the session.
|
|
355
|
+
|
|
356
|
+
### Stale-while-revalidate
|
|
357
|
+
|
|
358
|
+
When `revalidateInBackground: true` is set with `cacheStorage: 'indexeddb'`, the SDK uses a stale-while-revalidate strategy:
|
|
359
|
+
|
|
360
|
+
1. **IndexedDB has cached translations** — serve them immediately (no UI blocking), then fetch from the API in the background. If the API returns updated data, both caches and the translation store are silently updated.
|
|
361
|
+
2. **IndexedDB is empty** — fetch from the API as usual (blocks until data arrives).
|
|
362
|
+
|
|
363
|
+
This gives you instant page loads from cache while keeping translations fresh. The UI never blocks on repeat visits, and updates appear seamlessly when the API responds with new data.
|
|
364
|
+
|
|
365
|
+
```typescript
|
|
366
|
+
provideBlocksLocalization({
|
|
367
|
+
// ...
|
|
368
|
+
cacheStorage: 'indexeddb',
|
|
369
|
+
revalidateInBackground: true,
|
|
370
|
+
})
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### Fault tolerance
|
|
374
|
+
|
|
375
|
+
IndexedDB operations are **fire-and-forget safe**:
|
|
376
|
+
- If IndexedDB is unavailable (SSR, restrictive incognito, quota exceeded), the SDK silently falls back to memory-only mode.
|
|
377
|
+
- No errors are thrown, no user-facing impact — caching simply degrades gracefully.
|
|
378
|
+
- If the browser closes the IndexedDB connection (storage pressure, user clearing data), the SDK automatically reconnects.
|
|
379
|
+
- Multi-tab safe: the SDK yields its database connection when another tab needs to upgrade, then reconnects.
|
|
380
|
+
- On API failure, the SDK tries stale L1, then stale L2 (ignoring TTL), then local JSON files, before returning an empty map.
|
|
381
|
+
|
|
382
|
+
---
|
|
383
|
+
|
|
384
|
+
## Local JSON Fallback
|
|
385
|
+
|
|
386
|
+
When the UILM API is unreachable, the SDK automatically falls back to local JSON files. This is enabled by default.
|
|
387
|
+
|
|
388
|
+
### File structure
|
|
389
|
+
|
|
390
|
+
```
|
|
391
|
+
assets/i18n/
|
|
392
|
+
en.json # root/common module (alias: '')
|
|
393
|
+
de.json
|
|
394
|
+
dashboard/
|
|
395
|
+
en.json # dashboard module
|
|
396
|
+
de.json
|
|
397
|
+
opportunity/
|
|
398
|
+
en.json # opportunity module
|
|
399
|
+
de.json
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### Nested JSON is flattened automatically
|
|
403
|
+
|
|
404
|
+
Local files use nested JSON:
|
|
405
|
+
```json
|
|
406
|
+
{
|
|
407
|
+
"LABEL": {
|
|
408
|
+
"TITLE": "Dashboard",
|
|
409
|
+
"WELCOME": "Welcome {{ name }}"
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
The SDK flattens this to dot-notation (`LABEL.TITLE`, `LABEL.WELCOME`) to match the UILM API format. If `prefixKeysWithModule` is enabled, the module prefix is also applied (e.g. `dashboard.LABEL.TITLE`).
|
|
415
|
+
|
|
416
|
+
### Configuration
|
|
417
|
+
|
|
418
|
+
```typescript
|
|
419
|
+
provideBlocksLocalization({
|
|
420
|
+
// ...
|
|
421
|
+
fallbackToLocal: true, // default: true (set false to disable)
|
|
422
|
+
localAssetsPath: 'assets/i18n', // default
|
|
423
|
+
})
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
---
|
|
427
|
+
|
|
428
|
+
## Configuration Reference
|
|
429
|
+
|
|
430
|
+
| Option | Type | Default | Description |
|
|
431
|
+
|---|---|---|---|
|
|
432
|
+
| `uilmApiBaseUrl` | `string` | *required* | UILM API base URL |
|
|
433
|
+
| `projectKey` | `string` | *required* | Project key for `x-blocks-key` header |
|
|
434
|
+
| `accessToken` | `string` | — | Optional Bearer token |
|
|
435
|
+
| `availableLangs` | `string[]` | *required* | Available language short codes |
|
|
436
|
+
| `defaultLang` | `string` | *required* | Default language short code |
|
|
437
|
+
| `localeMapping` | `Record<string, string>` | — | Short to full locale mapping |
|
|
438
|
+
| `strategy` | `'modular' \| 'eager'` | `'modular'` | Loading strategy (see [above](#1-choose-a-loading-strategy)) |
|
|
439
|
+
| `preloadModules` | `UilmModuleEntry[]` | — | Modules to preload at startup. In eager mode, list **all** modules here. |
|
|
440
|
+
| `cacheTimeout` | `number` | `0` | Cache TTL in ms (`0` = no expiry) |
|
|
441
|
+
| `cacheStorage` | `'memory' \| 'indexeddb'` | `'memory'` | Cache persistence layer (see [Caching](#caching)) |
|
|
442
|
+
| `revalidateInBackground` | `boolean` | `false` | Serve cached IndexedDB translations immediately, refresh from API silently (see [Stale-while-revalidate](#stale-while-revalidate)) |
|
|
443
|
+
| `prefixKeysWithModule` | `boolean` | `false` | Namespace keys with module/alias |
|
|
444
|
+
| `fallbackToLocal` | `boolean` | `true` | Fall back to local JSON files on API failure |
|
|
445
|
+
| `localAssetsPath` | `string` | `'assets/i18n'` | Base path for local fallback JSON files |
|
|
446
|
+
| `langStorage` | `'localStorage' \| 'sessionStorage' \| 'none'` | `'localStorage'` | Where to persist the active language preference |
|
|
447
|
+
| `langStorageKey` | `string` | `'uilmLang'` | Custom storage key for the active language |
|
|
448
|
+
| `prodMode` | `boolean` | `false` | Suppress warnings |
|
|
449
|
+
|
|
450
|
+
---
|
|
451
|
+
|
|
452
|
+
## Architecture
|
|
453
|
+
|
|
454
|
+
```
|
|
455
|
+
provideBlocksLocalization(config) App-level: config + strategy + preload
|
|
456
|
+
|
|
|
457
|
+
|-- [eager] provideAppInitializer Blocks bootstrap until all modules loaded
|
|
458
|
+
|-- [modular] preloadModules only Loads shared modules, routes load the rest
|
|
459
|
+
|
|
|
460
|
+
+-- UilmStore Signal-based reactive translation store
|
|
461
|
+
| activeLang: Signal<string>
|
|
462
|
+
| version: Signal<number>
|
|
463
|
+
| ready: Signal<boolean>
|
|
464
|
+
| translate(key, params)
|
|
465
|
+
| setTranslation(data, lang)
|
|
466
|
+
|
|
|
467
|
+
+-- UilmLoader HTTP layer with two-tier cache
|
|
468
|
+
| L1: in-memory Map Always active
|
|
469
|
+
| L2: UilmIndexedDbCache Active when cacheStorage='indexeddb'
|
|
470
|
+
| fetchModuleTranslations() L1 -> L2 -> API -> fallbacks
|
|
471
|
+
| ensureMetadataLoaded()
|
|
472
|
+
| clearCache() Clears L1 + L2 + in-flight
|
|
473
|
+
|
|
|
474
|
+
+-- UilmIndexedDbCache IndexedDB persistence layer
|
|
475
|
+
| get(key, ttl) TTL-aware read
|
|
476
|
+
| getStale(key) Read ignoring TTL (for fallback)
|
|
477
|
+
| set(key, data) Fire-and-forget write
|
|
478
|
+
| clear() Wipe all stored translations
|
|
479
|
+
|
|
|
480
|
+
provideUilmScope({ modules }) Route-level: lazy load + merge into store
|
|
481
|
+
| (no-op in eager mode for initial language)
|
|
482
|
+
|
|
|
483
|
+
+-- UilmTranslateDirective *uilmTranslate="let t; scope: 'x'"
|
|
484
|
+
+-- UilmTranslatePipe {{ key | uilmTranslate }}
|
|
485
|
+
+-- UilmTranslateService inject() -> t(), translate(), tMany()
|
|
486
|
+
+-- MultiLangPipe {{ obj | multiLang }}
|
|
487
|
+
+-- BlocksLangSwitcher setActiveLang(), getAvailableLangs()
|
|
488
|
+
+-- UilmLoadingScreenComponent Centered loading UI for eager mode
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
### Reactivity
|
|
492
|
+
- `UilmStore.activeLang`, `version`, and `ready` are Angular signals
|
|
493
|
+
- All pipes, directives, and service methods react to both language changes and new translation loads
|
|
494
|
+
- No RxJS subscriptions leak — `DestroyRef` + `takeUntilDestroyed` throughout
|
|
495
|
+
|
|
496
|
+
---
|
|
497
|
+
|
|
498
|
+
## Public API
|
|
499
|
+
|
|
500
|
+
| Export | Kind | Purpose |
|
|
501
|
+
|---|---|---|
|
|
502
|
+
| `provideBlocksLocalization` | Provider | App-level setup + strategy + preload |
|
|
503
|
+
| `provideUilmScope` | Provider | Route-level lazy module loading |
|
|
504
|
+
| `UilmStore` | Service | Core reactive translation store |
|
|
505
|
+
| `UilmLoader` | Service | Low-level UILM API access + two-tier cache |
|
|
506
|
+
| `UilmIndexedDbCache` | Service | IndexedDB persistence layer |
|
|
507
|
+
| `UilmTranslateDirective` | Directive | `*uilmTranslate="let t"` |
|
|
508
|
+
| `UilmTranslatePipe` | Pipe | `{{ key \| uilmTranslate }}` |
|
|
509
|
+
| `MultiLangPipe` | Pipe | `{{ obj \| multiLang }}` |
|
|
510
|
+
| `UilmTranslateService` | Service | Signal/sync translations for TS |
|
|
511
|
+
| `BlocksLangSwitcher` | Service | Language switching |
|
|
512
|
+
| `UilmLoadingScreenComponent` | Component | Centered loading screen for eager mode |
|
|
513
|
+
| `TranslationMap` | Type | `Record<string, string>` alias |
|
|
514
|
+
| `UilmLangStorage` | Type | `'localStorage' \| 'sessionStorage' \| 'none'` |
|
|
515
|
+
| `UilmLoadingStrategy` | Type | `'modular' \| 'eager'` |
|
|
516
|
+
| `UilmCacheStorage` | Type | `'memory' \| 'indexeddb'` |
|
|
517
|
+
| `UilmModuleEntry` | Type | `string \| { module: string; alias?: string }` |
|
|
518
|
+
| `flattenJson` | Utility | Flatten nested JSON to dot-notation keys |
|
|
519
|
+
| `createI18nRecord` | Utility | Create empty i18n records |
|
|
520
|
+
| `toFullLangCode` / `toShortLangCode` | Utility | Language code conversion |
|
|
521
|
+
| `buildReverseMapping` | Utility | Reverse a locale mapping |
|
|
522
|
+
| `provideBlocksLocalizationTesting` | Provider | In-memory store for tests |
|
|
523
|
+
|
|
524
|
+
---
|
|
525
|
+
|
|
526
|
+
## Testing
|
|
527
|
+
|
|
528
|
+
### Test helper
|
|
529
|
+
|
|
530
|
+
The SDK provides `provideBlocksLocalizationTesting` for unit tests - no HTTP calls, no UILM API. Translations are loaded directly into the store.
|
|
531
|
+
|
|
532
|
+
```typescript
|
|
533
|
+
import { provideBlocksLocalizationTesting } from '@seliseblocks/blocks-angular-localization';
|
|
534
|
+
|
|
535
|
+
TestBed.configureTestingModule({
|
|
536
|
+
providers: [
|
|
537
|
+
provideBlocksLocalizationTesting({
|
|
538
|
+
en: { 'dashboard.LABEL.HELLO': 'Hello', 'dashboard.LABEL.WORLD': 'World' },
|
|
539
|
+
de: { 'dashboard.LABEL.HELLO': 'Hallo', 'dashboard.LABEL.WORLD': 'Welt' },
|
|
540
|
+
}),
|
|
541
|
+
],
|
|
542
|
+
});
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
You can override any config option:
|
|
546
|
+
|
|
547
|
+
```typescript
|
|
548
|
+
provideBlocksLocalizationTesting(
|
|
549
|
+
{ en: {}, de: {} },
|
|
550
|
+
{ defaultLang: 'de' }, // start in German
|
|
551
|
+
)
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
### Testing components that use translations
|
|
555
|
+
|
|
556
|
+
```typescript
|
|
557
|
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
558
|
+
import { provideBlocksLocalizationTesting, UilmTranslateDirective } from '@seliseblocks/blocks-angular-localization';
|
|
559
|
+
|
|
560
|
+
import { MyComponent } from './my.component';
|
|
561
|
+
|
|
562
|
+
describe('MyComponent', () => {
|
|
563
|
+
let fixture: ComponentFixture<MyComponent>;
|
|
564
|
+
|
|
565
|
+
beforeEach(() => {
|
|
566
|
+
TestBed.configureTestingModule({
|
|
567
|
+
imports: [MyComponent],
|
|
568
|
+
providers: [
|
|
569
|
+
provideBlocksLocalizationTesting({
|
|
570
|
+
en: {
|
|
571
|
+
'dashboard.LABEL.TITLE': 'Dashboard',
|
|
572
|
+
'dashboard.HINT.WELCOME': 'Welcome {{ name }}',
|
|
573
|
+
},
|
|
574
|
+
}),
|
|
575
|
+
],
|
|
576
|
+
});
|
|
577
|
+
fixture = TestBed.createComponent(MyComponent);
|
|
578
|
+
fixture.detectChanges();
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
it('should render translated title', () => {
|
|
582
|
+
expect(fixture.nativeElement.textContent).toContain('Dashboard');
|
|
583
|
+
});
|
|
584
|
+
});
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
### Testing language switching
|
|
588
|
+
|
|
589
|
+
```typescript
|
|
590
|
+
import { UilmStore } from '@seliseblocks/blocks-angular-localization';
|
|
591
|
+
|
|
592
|
+
it('should update when language changes', () => {
|
|
593
|
+
const store = TestBed.inject(UilmStore);
|
|
594
|
+
|
|
595
|
+
expect(fixture.nativeElement.textContent).toContain('Hello');
|
|
596
|
+
|
|
597
|
+
store.setActiveLang('de');
|
|
598
|
+
fixture.detectChanges();
|
|
599
|
+
|
|
600
|
+
expect(fixture.nativeElement.textContent).toContain('Hallo');
|
|
601
|
+
});
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
### Testing the UilmLoader with HTTP mocks
|
|
605
|
+
|
|
606
|
+
```typescript
|
|
607
|
+
import { provideHttpClient } from '@angular/common/http';
|
|
608
|
+
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
|
|
609
|
+
import { BLOCKS_LOCALIZATION_CONFIG, UilmIndexedDbCache, UilmLoader } from '@seliseblocks/blocks-angular-localization';
|
|
610
|
+
|
|
611
|
+
// Mock IndexedDB cache for tests (jsdom has no IndexedDB)
|
|
612
|
+
class FakeIndexedDbCache {
|
|
613
|
+
private store = new Map<string, { data: Record<string, string>; timestamp: number }>();
|
|
614
|
+
|
|
615
|
+
async get(key: string, ttl: number) {
|
|
616
|
+
const entry = this.store.get(key);
|
|
617
|
+
if (!entry) return null;
|
|
618
|
+
if (ttl > 0 && Date.now() - entry.timestamp >= ttl) return null;
|
|
619
|
+
return entry.data;
|
|
620
|
+
}
|
|
621
|
+
async getStale(key: string) { return this.store.get(key)?.data ?? null; }
|
|
622
|
+
async set(key: string, data: Record<string, string>) {
|
|
623
|
+
this.store.set(key, { data, timestamp: Date.now() });
|
|
624
|
+
}
|
|
625
|
+
async clear() { this.store.clear(); }
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
beforeEach(() => {
|
|
629
|
+
TestBed.configureTestingModule({
|
|
630
|
+
providers: [
|
|
631
|
+
provideHttpClient(),
|
|
632
|
+
provideHttpClientTesting(),
|
|
633
|
+
{ provide: BLOCKS_LOCALIZATION_CONFIG, useValue: { /* config */ } },
|
|
634
|
+
{ provide: UilmIndexedDbCache, useValue: new FakeIndexedDbCache() },
|
|
635
|
+
],
|
|
636
|
+
});
|
|
637
|
+
});
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
### Running the library's own tests
|
|
641
|
+
|
|
642
|
+
```bash
|
|
643
|
+
npx nx test blocks-localization
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
### Test suite overview
|
|
647
|
+
|
|
648
|
+
| Spec file | Tests | What it covers |
|
|
649
|
+
|---|---|---|
|
|
650
|
+
| `flatten-json.spec.ts` | 13 | Nesting depth, custom separators, null/array edge cases, parent key prefix, circular reference protection |
|
|
651
|
+
| `lang-codes.spec.ts` | 8 | Short-to-full mapping, full-to-short extraction, reverse mapping |
|
|
652
|
+
| `i18n-record.spec.ts` | 2 | Empty record creation from language array |
|
|
653
|
+
| `uilm-store.spec.ts` | 30 | Translation lookup, `{{ param }}` interpolation (including array intermediates), merge (not overwrite), `has()`, `ready()` signal, version bumping, localStorage persistence + restore, invalid lang fallback, storage error fallback, key mode toggle via postMessage |
|
|
654
|
+
| `uilm-indexeddb-cache.spec.ts` | 21 | Graceful degradation when IndexedDB unavailable (no throws), TTL expiry logic unit tests |
|
|
655
|
+
| `uilm-loader.spec.ts` | 36 | L1 cache hit, L2 (IndexedDB) hit/miss/stale, in-flight dedup, key prefixing (module/alias/empty), full locale in URL, metadata fetch + no-op, clearCache L1+L2, fallbackToLocal disabled, Authorization header, local JSON fallback + flatten, stale-while-revalidate (background refresh, shallow equality, API error silence), API response validation (null/array sanitization) |
|
|
656
|
+
| `uilm-translate.service.spec.ts` | 8 | `t()` signal, `translate()` sync, `tMany()` / `translateMany()` batch, interpolation, `activeLang`, `setActiveLang()` |
|
|
657
|
+
| `provide-blocks-localization-testing.spec.ts` | 4 | Translation injection, default lang, config overrides, empty translations |
|
|
658
|
+
|
|
659
|
+
### Test infrastructure
|
|
660
|
+
|
|
661
|
+
| File | Purpose |
|
|
662
|
+
|---|---|
|
|
663
|
+
| `vite.config.mts` | Vitest configuration (jsdom environment, Angular plugin) |
|
|
664
|
+
| `tsconfig.spec.json` | TypeScript config for test files |
|
|
665
|
+
| `src/test-setup.ts` | Angular compiler + TestBed initialization |
|
|
666
|
+
| `project.json` | Nx `test` target (`@nx/vitest:test` executor) |
|
|
667
|
+
|
|
668
|
+
---
|
|
669
|
+
|
|
670
|
+
## Further reading
|
|
671
|
+
|
|
672
|
+
| Document | What it covers |
|
|
673
|
+
|----------|---------------|
|
|
674
|
+
| **[DEPLOYMENT.md](./DEPLOYMENT.md)** | CI/CD pipeline, npm publishing, versioning, troubleshooting |
|
|
675
|
+
| **[CONTRIBUTING.md](./CONTRIBUTING.md)** | Development workflow, code standards, PR process |
|
|
676
|
+
| **[CHANGELOG.md](./CHANGELOG.md)** | Version history and release notes |
|