@mmstack/translate 20.5.12 β 20.5.13
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 +232 -165
- package/fesm2022/mmstack-translate.mjs +85 -22
- package/fesm2022/mmstack-translate.mjs.map +1 -1
- package/index.d.ts +24 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,13 +6,7 @@
|
|
|
6
6
|
[](https://github.com/mihajm/mmstack/blob/master/LICENSE)
|
|
7
7
|
[](CONTRIBUTING.md)
|
|
8
8
|
|
|
9
|
-
`@mmstack/translate` is an opinionated internationalization (i18n) library for Angular applications
|
|
10
|
-
|
|
11
|
-
1. **Maximum Type Safety:** Catch errors related to missing keys or incorrect/missing parameters at compile time.
|
|
12
|
-
2. **Flexible Build Process:** Works as a traditional multi-build solution (like `@angular/localize`) **OR** as a single-build runtime solution.
|
|
13
|
-
3. **Scalable Modularity:** Organize translations into **namespaces** (typically aligned with feature libraries) and load them on demand.
|
|
14
|
-
|
|
15
|
-
It uses the robust **FormatJS** Intl runtime (`@formatjs/intl`) for ICU message formatting, and integrates with Angular's dependency injection and routing.
|
|
9
|
+
`@mmstack/translate` is an opinionated internationalization (i18n) library for Angular applications. It uses the **FormatJS** Intl runtime (`@formatjs/intl`) for ICU message formatting and integrates with Angular's dependency injection, routing, and signals.
|
|
16
10
|
|
|
17
11
|
## Features
|
|
18
12
|
|
|
@@ -32,24 +26,6 @@ It uses the robust **FormatJS** Intl runtime (`@formatjs/intl`) for ICU message
|
|
|
32
26
|
- π οΈ **Template Helpers:** Includes abstract `Translator` pipe and `Translate` directive for easy, type-safe templating.
|
|
33
27
|
- π’ **Reactive Formatters:** First-class, Intl-based, locale-aware formatters for dates, numbers, currencies, percentages, lists, and relative time β all automatically reactive to locale changes via signals, no zone or common/locale dependency required.
|
|
34
28
|
|
|
35
|
-
### Comparison
|
|
36
|
-
|
|
37
|
-
While Angular offers excellent i18n solutions like `@angular/localize` and `@jsverse/transloco`, `@mmstack/translate` aims to fill a specific niche by supporting **both** traditional multi-build and modern single-build approaches with a typesafe & modular approach, perfect for nx-based environments.
|
|
38
|
-
|
|
39
|
-
| Feature | `@mmstack/translate` | `@angular/localize` | `@jsverse/transloco` | `ngx-translate` |
|
|
40
|
-
| :----------------------- | :----------------------------------: | :---------------------------: | :--------------------------------------------------------------------------------------------------: | :-------------------------: |
|
|
41
|
-
| **Build Process** | β
Single or Multi-Build | β Multi-Build (Typical) | β
Single Build | β
Single Build |
|
|
42
|
-
| **Translation Timing** | Runtime or Build Time | Compile Time | Runtime | Runtime |
|
|
43
|
-
| **Type Safety (Keys)** | β
Strong (Inferred from structure) | π‘ via extraction | π‘ Tooling/TS Files | π‘ OK Manual/Tooling |
|
|
44
|
-
| **Type Safety (Params)** | β
Strong (Inferred from ICU) | β None | π‘ Manual | π‘ Manual |
|
|
45
|
-
| **Locale Switching** | β
Dynamic (Runtime) or Page refresh | π Page Refresh Required | β
Dynamic (Runtime) | β
Dynamic (Runtime) |
|
|
46
|
-
| **Lazy Loading** | β
Built-in (Namespaces/Resolvers) | N/A (Compile Time) | β
Built-in (Scopes) | β
Yes (Custom Loaders) |
|
|
47
|
-
| **Namespacing/Scopes** | β
Built-in | β None | β
Built-in (Scopes) | π‘ Manual (File Structure) |
|
|
48
|
-
| **ICU Support** | β
Subset (via FormatJS Runtime) | β
Yes (Compile Time) | β
Yes (Runtime Intl/Plugins) | π‘ Via Extensions |
|
|
49
|
-
| **Signal Integration** | β
Great (fully reactive) | N/A | β
Good (`translateSignal()`, `activeLang` signal) | β Minimal/NoneΒΉ |
|
|
50
|
-
| **Reactive Formatters** | β
Built-in Intl integration | π‘ Angular pipes (zone-based) | β
@jsverse/transloco-locale(`transloco-locale`Β²: date/number/currency/percent, not signal-reactive) | β None (use Angular pipes) |
|
|
51
|
-
| **Maturity / Community** | π‘ Less mature, but battle tested | Core Angular | β
Mature / Active | β
Mature |
|
|
52
|
-
|
|
53
29
|
## Installation
|
|
54
30
|
|
|
55
31
|
Install the library & its peer dependency, `@formatjs/intl`.
|
|
@@ -58,6 +34,20 @@ Install the library & its peer dependency, `@formatjs/intl`.
|
|
|
58
34
|
npm install @mmstack/translate @formatjs/intl
|
|
59
35
|
```
|
|
60
36
|
|
|
37
|
+
## Table of contents
|
|
38
|
+
|
|
39
|
+
- [Configuration](#configuration) β multi-build (default) or single-build with `provideIntlConfig`
|
|
40
|
+
- [Usage](#usage) β defining namespaces, registering them, reading translations
|
|
41
|
+
- [Example configurations](#example-configurations) β full configs for the two most common scenarios
|
|
42
|
+
- [Helper functions](#helper-functions) β `injectDefaultLocale`, `injectIntl`, `injectDynamicLocale`, `canMatchLocale`
|
|
43
|
+
- [Formatters](#formatters) β reactive `Intl.*` wrappers for date, number, currency, percent, list, relative time, display name
|
|
44
|
+
- [Remote / unsafe namespaces](#remote--unsafe-namespaces) β for translations whose keys aren't known at build time
|
|
45
|
+
- [Escape hatches](#escape-hatches) β `withParams`, `injectAddTranslations`, `injectUnsafeT`
|
|
46
|
+
- [Testing](#testing) β `provideMockTranslations`
|
|
47
|
+
- [Architecture & performance](#advanced-architecture--performance)
|
|
48
|
+
- [Migration from other libraries](#migration-from-other-libraries)
|
|
49
|
+
- [Alternatives & comparison](#alternatives--comparison)
|
|
50
|
+
|
|
61
51
|
## Configuration
|
|
62
52
|
|
|
63
53
|
### Default: Multi-Build Scenario (like @angular/localize)
|
|
@@ -101,20 +91,11 @@ export const appConfig: ApplicationConfig = {
|
|
|
101
91
|
};
|
|
102
92
|
```
|
|
103
93
|
|
|
104
|
-
**
|
|
105
|
-
|
|
106
|
-
```typescript
|
|
107
|
-
provideIntlConfig({
|
|
108
|
-
defaultLocale: 'en-US',
|
|
109
|
-
supportedLocales: ['en-US', 'sl-SI', 'de-DE'],
|
|
110
|
-
|
|
111
|
-
// Automatically detect and respond to locale route parameter changes, should correspond with actual param name example bellow
|
|
112
|
-
localeParamName: 'locale',
|
|
94
|
+
**Additional options:**
|
|
113
95
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
```
|
|
96
|
+
- **`localeParamName`** β drive the active locale from a route parameter. See [Route-based locale detection](#4-optional-route-based-locale-detection) and [Scenario A](#scenario-a-route-based-locale).
|
|
97
|
+
- **`localeStorage`** β persist the user-selected locale across reloads via a `read` / `write` adapter. See [Dynamic language switching](#5-optional-dynamic-language-switching) and [Scenario B](#scenario-b-dynamic-locale-with-localstorage-persistence). Mutually exclusive with `localeParamName`.
|
|
98
|
+
- **`preloadDefaultLocale: true`** β eagerly load the default-locale bundle so it's available as a synchronous fallback. Rarely needed.
|
|
118
99
|
|
|
119
100
|
## Usage
|
|
120
101
|
|
|
@@ -178,11 +159,11 @@ Use `registerNamespace` to prepare your namespace definition and obtain the `inj
|
|
|
178
159
|
import { registerNamespace } from '@mmstack/translate';
|
|
179
160
|
|
|
180
161
|
const r = registerNamespace(
|
|
181
|
-
// Default locale
|
|
182
|
-
() => import('./quote.namespace')
|
|
162
|
+
// Default locale (also acts as the fallback).
|
|
163
|
+
() => import('./quote.namespace'),
|
|
183
164
|
{
|
|
184
|
-
//
|
|
185
|
-
'sl-SI': () => import('./quote-sl.translation')
|
|
165
|
+
// Other locales β each value is a `() => Promise<...>` factory.
|
|
166
|
+
'sl-SI': () => import('./quote-sl.translation'),
|
|
186
167
|
// Add more locales as needed...
|
|
187
168
|
},
|
|
188
169
|
);
|
|
@@ -191,6 +172,16 @@ export const injectQuoteT = r.injectNamespaceT;
|
|
|
191
172
|
export const resolveQuoteTranslations = r.resolveNamespaceTranslation;
|
|
192
173
|
```
|
|
193
174
|
|
|
175
|
+
Each loader can return either a `CompiledTranslation` directly, or an ES module exposing one as `default` or as a named `translation` export. So all three of these are equivalent:
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
() => import('./quote.namespace'), // ES module with `export default`
|
|
179
|
+
() => import('./quote.namespace').then((m) => m.default), // explicit unwrap (still supported)
|
|
180
|
+
() => import('./quote.namespace').then((m) => m.translation), // for files using `export const translation`
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Bare dynamic imports are the most ergonomic; the explicit forms continue to work and can be useful when a single module re-exports several namespaces.
|
|
184
|
+
|
|
194
185
|
**Add the resolver to your routes:**
|
|
195
186
|
|
|
196
187
|
```typescript
|
|
@@ -291,139 +282,48 @@ export class QuoteComponent {
|
|
|
291
282
|
|
|
292
283
|
### 4. [OPTIONAL] Route-Based Locale Detection
|
|
293
284
|
|
|
294
|
-
For applications with locale-based routing (e.g
|
|
295
|
-
|
|
296
|
-
**Step 1: Configure locale parameter name**
|
|
297
|
-
|
|
298
|
-
```typescript
|
|
299
|
-
// app.config.ts
|
|
300
|
-
import { provideIntlConfig } from '@mmstack/translate';
|
|
301
|
-
|
|
302
|
-
export const appConfig: ApplicationConfig = {
|
|
303
|
-
providers: [
|
|
304
|
-
provideIntlConfig({
|
|
305
|
-
defaultLocale: 'en-US',
|
|
306
|
-
supportedLocales: ['en-US', 'sl-SI', 'de-DE'],
|
|
307
|
-
localeParamName: 'locale', // Track this route parameter automatically
|
|
308
|
-
}),
|
|
309
|
-
],
|
|
310
|
-
};
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
**Step 2: Add route guard for validation**
|
|
314
|
-
|
|
315
|
-
```typescript
|
|
316
|
-
// app.routes.ts
|
|
317
|
-
import { Routes } from '@angular/router';
|
|
318
|
-
import { canMatchLocale } from '@mmstack/translate';
|
|
319
|
-
|
|
320
|
-
export const routes: Routes = [
|
|
321
|
-
{
|
|
322
|
-
path: ':locale',
|
|
323
|
-
canMatch: [canMatchLocale()], // Validates & redirects invalid locales
|
|
324
|
-
children: [
|
|
325
|
-
{
|
|
326
|
-
path: 'quotes',
|
|
327
|
-
loadChildren: () =>
|
|
328
|
-
import('./quote/quote.routes').then((m) => m.QUOTE_ROUTES),
|
|
329
|
-
},
|
|
330
|
-
// ... other routes
|
|
331
|
-
],
|
|
332
|
-
},
|
|
333
|
-
];
|
|
334
|
-
```
|
|
285
|
+
For applications with locale-based routing (e.g. `/en-US/quotes`, `/sl-SI/quotes`), the library can detect and switch locales automatically:
|
|
335
286
|
|
|
336
|
-
|
|
287
|
+
- Set `localeParamName` in `provideIntlConfig({ ... })` β the store will react to that route param.
|
|
288
|
+
- Wire `canMatchLocale()` as a `canMatch` guard on the route that owns the locale segment β it validates against `supportedLocales` and redirects invalid locales to the default.
|
|
337
289
|
|
|
338
|
-
|
|
339
|
-
- Load translations on demand for the new locale
|
|
340
|
-
- Update all translation outputs reactively
|
|
341
|
-
- Redirect invalid locales to the default
|
|
290
|
+
See [Scenario A](#scenario-a-route-based-locale) for a complete end-to-end config.
|
|
342
291
|
|
|
343
|
-
**
|
|
344
|
-
|
|
345
|
-
If your locale parameter isn't the first segment (e.g., `/app/:locale/...`):
|
|
292
|
+
**Locale parameter not in the first position?** Pass the leading static segments to `canMatchLocale`:
|
|
346
293
|
|
|
347
294
|
```typescript
|
|
348
295
|
{
|
|
349
296
|
path: 'app/:locale',
|
|
350
|
-
canMatch: [canMatchLocale(['app'])], //
|
|
297
|
+
canMatch: [canMatchLocale(['app'])], // matches the segment after 'app'
|
|
351
298
|
children: [...]
|
|
352
299
|
}
|
|
353
300
|
```
|
|
354
301
|
|
|
355
302
|
### 5. [OPTIONAL] Dynamic Language Switching
|
|
356
303
|
|
|
357
|
-
For
|
|
304
|
+
For runtime language switching without page refreshes (e.g. a language picker in the header), `injectDynamicLocale()` returns a `WritableSignal<string>` with an attached `isLoading: Signal<boolean>`. Setting it triggers automatic loading of any missing namespace translations for the new locale; setting it to a value not in `supportedLocales` is a no-op (with a dev-mode warning).
|
|
358
305
|
|
|
359
306
|
```typescript
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
@Component({
|
|
364
|
-
selector: 'app-language-switcher',
|
|
365
|
-
template: `
|
|
366
|
-
<select [value]="locale()" (change)="changeLanguage($event)">
|
|
367
|
-
<option value="en-US">English</option>
|
|
368
|
-
<option value="sl-SI">SlovenΕ‘Δina</option>
|
|
369
|
-
<option value="de-DE">Deutsch</option>
|
|
370
|
-
</select>
|
|
371
|
-
|
|
372
|
-
@if (locale.isLoading()) {
|
|
373
|
-
<div class="spinner">Loading translations...</div>
|
|
374
|
-
}
|
|
375
|
-
`,
|
|
376
|
-
})
|
|
377
|
-
export class LanguageSwitcherComponent {
|
|
378
|
-
protected readonly locale = injectDynamicLocale();
|
|
379
|
-
|
|
380
|
-
changeLanguage(event: Event) {
|
|
381
|
-
const target = event.target as HTMLSelectElement;
|
|
382
|
-
this.locale.set(target.value); // Automatically loads missing translations
|
|
383
|
-
}
|
|
384
|
-
}
|
|
307
|
+
const locale = injectDynamicLocale();
|
|
308
|
+
locale.set('sl-SI'); // missing translations load automatically
|
|
309
|
+
if (locale.isLoading()) { /* show a spinner */ }
|
|
385
310
|
```
|
|
386
311
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
- Validates against `supportedLocales` (if configured)
|
|
390
|
-
- Automatically loads missing namespace translations
|
|
391
|
-
- Provides `isLoading()` signal for UI feedback
|
|
392
|
-
- Works with route-based locales
|
|
312
|
+
Pair with `localeStorage` in `provideIntlConfig({ ... })` to persist the user's choice across reloads β `read()` runs once on init, `write()` fires on every successful change. Stored values are validated against `supportedLocales`; errors from `read()`/`write()` are swallowed (and dev-mode logged) so a misbehaving backend can't break the app. `localeStorage` is mutually exclusive with `localeParamName` at the type level.
|
|
393
313
|
|
|
394
|
-
|
|
314
|
+
See [Scenario B](#scenario-b-dynamic-locale-with-localstorage-persistence) for a complete app config plus a language-switcher component.
|
|
395
315
|
|
|
396
|
-
|
|
316
|
+
**Note for pure pipes:** Angular memoizes pure pipes by input identity, so they don't naturally re-evaluate when only the store's locale signal changes. Two ways out:
|
|
397
317
|
|
|
398
318
|
```typescript
|
|
399
|
-
//
|
|
319
|
+
// Recommended: pass locale as a pipe argument so the input identity changes.
|
|
400
320
|
{{ 'common.yes' | translate : locale() }}
|
|
401
321
|
|
|
402
|
-
//
|
|
403
|
-
@Pipe({
|
|
404
|
-
name: 'translate',
|
|
405
|
-
pure: false,
|
|
406
|
-
})
|
|
322
|
+
// Alternative: opt the pipe out of memoization (slower; re-runs every CD cycle).
|
|
323
|
+
@Pipe({ name: 'translate', pure: false })
|
|
407
324
|
export class QuoteTranslator extends Translator<QuoteLocale> {}
|
|
408
325
|
```
|
|
409
326
|
|
|
410
|
-
**Persisting the selected locale:**
|
|
411
|
-
|
|
412
|
-
If you want the user's last selected locale to survive page reloads, pass a `localeStorage` adapter to `provideIntlConfig()`. The library calls `read()` once on init to restore the previous selection, and `write()` whenever the active locale changes β you decide where it lives (localStorage, cookies, IndexedDB-with-sync-wrapper, etc.).
|
|
413
|
-
|
|
414
|
-
```typescript
|
|
415
|
-
provideIntlConfig({
|
|
416
|
-
defaultLocale: 'en-US',
|
|
417
|
-
supportedLocales: ['en-US', 'sl-SI', 'de-DE'],
|
|
418
|
-
localeStorage: {
|
|
419
|
-
read: () => localStorage.getItem('locale'),
|
|
420
|
-
write: (locale) => localStorage.setItem('locale', locale),
|
|
421
|
-
},
|
|
422
|
-
});
|
|
423
|
-
```
|
|
424
|
-
|
|
425
|
-
Stored values are validated against `supportedLocales` before being applied β anything unrecognized is ignored and the default applies. Errors thrown from `read()` / `write()` are swallowed (and logged in dev mode) so a misbehaving storage backend can't break the app. `localeStorage` is mutually exclusive with `localeParamName` at the type level β when the URL is the source of truth, persisting separately would just fight it.
|
|
426
|
-
|
|
427
327
|
### 6. [OPTIONAL] Creating a Shared/Common Namespace
|
|
428
328
|
|
|
429
329
|
A shared namespace allows you to define common translations (e.g., 'Save', 'Cancel', 'Yes', 'No') once and use them type-safely across all other namespaces.
|
|
@@ -457,9 +357,9 @@ export const createAppNamespace = ns.createMergedNamespace;
|
|
|
457
357
|
import { registerNamespace } from '@mmstack/translate';
|
|
458
358
|
|
|
459
359
|
const r = registerNamespace(
|
|
460
|
-
() => import('./common.namespace')
|
|
360
|
+
() => import('./common.namespace'),
|
|
461
361
|
{
|
|
462
|
-
'sl-SI': () => import('./common-sl.translation')
|
|
362
|
+
'sl-SI': () => import('./common-sl.translation'),
|
|
463
363
|
},
|
|
464
364
|
);
|
|
465
365
|
|
|
@@ -513,6 +413,111 @@ export class QuoteComponent {
|
|
|
513
413
|
}
|
|
514
414
|
```
|
|
515
415
|
|
|
416
|
+
## Example configurations
|
|
417
|
+
|
|
418
|
+
Two end-to-end app configs covering the most common single-build scenarios. Copy either as a starting point.
|
|
419
|
+
|
|
420
|
+
### Scenario A: Route-based locale
|
|
421
|
+
|
|
422
|
+
The locale lives in the URL (`/en-US/quotes`, `/sl-SI/quotes`), the router guard validates it, and the resolver picks it up automatically. Best when you want shareable, SEO-friendly locale URLs.
|
|
423
|
+
|
|
424
|
+
```typescript
|
|
425
|
+
// app.config.ts
|
|
426
|
+
import { ApplicationConfig, LOCALE_ID } from '@angular/core';
|
|
427
|
+
import { provideRouter } from '@angular/router';
|
|
428
|
+
import { provideIntlConfig } from '@mmstack/translate';
|
|
429
|
+
import { routes } from './app.routes';
|
|
430
|
+
|
|
431
|
+
export const appConfig: ApplicationConfig = {
|
|
432
|
+
providers: [
|
|
433
|
+
{ provide: LOCALE_ID, useValue: 'en-US' }, // initial / fallback locale
|
|
434
|
+
provideIntlConfig({
|
|
435
|
+
defaultLocale: 'en-US',
|
|
436
|
+
supportedLocales: ['en-US', 'sl-SI', 'de-DE'],
|
|
437
|
+
localeParamName: 'locale', // store reacts to this route param
|
|
438
|
+
}),
|
|
439
|
+
provideRouter(routes),
|
|
440
|
+
],
|
|
441
|
+
};
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
```typescript
|
|
445
|
+
// app.routes.ts
|
|
446
|
+
import { Routes } from '@angular/router';
|
|
447
|
+
import { canMatchLocale } from '@mmstack/translate';
|
|
448
|
+
|
|
449
|
+
export const routes: Routes = [
|
|
450
|
+
{
|
|
451
|
+
path: ':locale',
|
|
452
|
+
canMatch: [canMatchLocale()], // redirects invalid locales to default
|
|
453
|
+
children: [
|
|
454
|
+
{
|
|
455
|
+
path: 'quotes',
|
|
456
|
+
loadChildren: () =>
|
|
457
|
+
import('./quote/quote.routes').then((m) => m.QUOTE_ROUTES),
|
|
458
|
+
},
|
|
459
|
+
// ... other locale-scoped routes
|
|
460
|
+
],
|
|
461
|
+
},
|
|
462
|
+
];
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
Visiting `/sl-SI/quotes` triggers the resolver, which loads the `sl-SI` translation and switches the store's locale. Visiting `/zz-ZZ/quotes` is redirected to `/en-US/quotes` by the guard.
|
|
466
|
+
|
|
467
|
+
### Scenario B: Dynamic locale with localStorage persistence
|
|
468
|
+
|
|
469
|
+
The locale lives in a writable signal driven by the user (typically a language picker), and the choice survives reloads. Best when the locale isn't part of the URL.
|
|
470
|
+
|
|
471
|
+
```typescript
|
|
472
|
+
// app.config.ts
|
|
473
|
+
import { ApplicationConfig, LOCALE_ID } from '@angular/core';
|
|
474
|
+
import { provideRouter } from '@angular/router';
|
|
475
|
+
import { provideIntlConfig } from '@mmstack/translate';
|
|
476
|
+
import { routes } from './app.routes';
|
|
477
|
+
|
|
478
|
+
export const appConfig: ApplicationConfig = {
|
|
479
|
+
providers: [
|
|
480
|
+
{ provide: LOCALE_ID, useValue: 'en-US' }, // initial / fallback locale
|
|
481
|
+
provideIntlConfig({
|
|
482
|
+
defaultLocale: 'en-US',
|
|
483
|
+
supportedLocales: ['en-US', 'sl-SI', 'de-DE'],
|
|
484
|
+
localeStorage: {
|
|
485
|
+
read: () => localStorage.getItem('locale'),
|
|
486
|
+
write: (locale) => localStorage.setItem('locale', locale),
|
|
487
|
+
},
|
|
488
|
+
}),
|
|
489
|
+
provideRouter(routes),
|
|
490
|
+
],
|
|
491
|
+
};
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
```typescript
|
|
495
|
+
// language-switcher.component.ts
|
|
496
|
+
import { Component } from '@angular/core';
|
|
497
|
+
import { injectDynamicLocale } from '@mmstack/translate';
|
|
498
|
+
|
|
499
|
+
@Component({
|
|
500
|
+
selector: 'app-language-switcher',
|
|
501
|
+
template: `
|
|
502
|
+
<select [value]="locale()" (change)="onChange($event)">
|
|
503
|
+
<option value="en-US">English</option>
|
|
504
|
+
<option value="sl-SI">SlovenΕ‘Δina</option>
|
|
505
|
+
<option value="de-DE">Deutsch</option>
|
|
506
|
+
</select>
|
|
507
|
+
@if (locale.isLoading()) { <span>Loadingβ¦</span> }
|
|
508
|
+
`,
|
|
509
|
+
})
|
|
510
|
+
export class LanguageSwitcherComponent {
|
|
511
|
+
protected readonly locale = injectDynamicLocale();
|
|
512
|
+
|
|
513
|
+
protected onChange(event: Event) {
|
|
514
|
+
this.locale.set((event.target as HTMLSelectElement).value);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
`localeStorage.read()` runs once on init to restore the previous choice (silently ignored if not in `supportedLocales`); `write()` fires on every successful locale change. `localeStorage` and `localeParamName` are mutually exclusive at the type level β when the URL is the source of truth, persisting separately would just fight it.
|
|
520
|
+
|
|
516
521
|
## Helper Functions
|
|
517
522
|
|
|
518
523
|
### Core Injection Functions
|
|
@@ -552,7 +557,7 @@ The library uses Angular's `resource()` API for efficient, reactive translation
|
|
|
552
557
|
|
|
553
558
|
- Automatic request deduplication
|
|
554
559
|
- Built-in loading states
|
|
555
|
-
-
|
|
560
|
+
- Stale-result discarding via `AbortSignal` (the abort signal is checked after each load resolves; in-flight `fetch` cancellation isn't propagated to user-supplied loaders)
|
|
556
561
|
- Better error handling
|
|
557
562
|
|
|
558
563
|
### On-Demand Translation Loading
|
|
@@ -750,27 +755,71 @@ describe('MyComponent', () => {
|
|
|
750
755
|
|
|
751
756
|
### From @angular/localize
|
|
752
757
|
|
|
753
|
-
`@mmstack/translate`
|
|
758
|
+
You don't have to change your build pipeline β `@mmstack/translate` runs alongside (or in place of) `@angular/localize` in multi-build mode without rebuilds. The migration happens at the source level: replace `$localize` template tags and `i18n` attributes with namespace-based access.
|
|
759
|
+
|
|
760
|
+
| `@angular/localize` | `@mmstack/translate` |
|
|
761
|
+
| ---------------------------------------------- | --------------------------------------------------------------------------------- |
|
|
762
|
+
| ``$localize`Hello ${name}:name:` `` | `t('quote.greeting', { name })` |
|
|
763
|
+
| `<h1 i18n>Title</h1>` | `<h1 [translate]="'quote.title'">` / `{{ t('quote.title') }}` / pipe |
|
|
764
|
+
| `messages.xlf` extraction | TypeScript: `createNamespace('quote', { ... })` |
|
|
765
|
+
| `messages.<locale>.xlf` translation file | TS file: `createQuoteTranslation('sl-SI', { ... })` |
|
|
766
|
+
| `angular.json` `localize` config (multi-build) | Same multi-build still works; or switch to a single build with `provideIntlConfig` |
|
|
767
|
+
| `<my-cmp i18n-title title="Hi">` | Bind the title from a translation: `[title]="t('ns.greeting')"` |
|
|
768
|
+
|
|
769
|
+
**ICU plurals and selects use the same syntax** β no conversion. They're now type-checked end to end, which the `@angular/localize` extractor doesn't provide.
|
|
770
|
+
|
|
771
|
+
**No auto-extraction.** Translation files are authored as TypeScript, so the compiler enforces shape consistency and parameter coverage across locales β at the cost of losing the `ng extract-i18n` workflow. For greenfield namespaces this is usually a net win; for huge existing xlf catalogs, plan a one-time script to convert them.
|
|
772
|
+
|
|
773
|
+
### From @jsverse/transloco
|
|
774
|
+
|
|
775
|
+
Conceptually close β both are runtime, signal-aware, and namespace/scope-based. The main shift is from JSON files + a service API to TypeScript namespaces + an injected `t` function:
|
|
776
|
+
|
|
777
|
+
| transloco | `@mmstack/translate` |
|
|
778
|
+
| -------------------------------------------------------- | --------------------------------------------------------------------- |
|
|
779
|
+
| `provideTransloco({ ... })` | `provideIntlConfig({ ... })` |
|
|
780
|
+
| `scope` (e.g. `'lazy-page'`) | `namespace` (first arg to `createNamespace` / `registerNamespace`) |
|
|
781
|
+
| JSON translation files | TS namespace files (default + one per other locale) |
|
|
782
|
+
| `inject(TranslocoService).translate(key, params)` | `injectQuoteT()(...)` (typed `t` from `registerNamespace`) |
|
|
783
|
+
| `translateSignal(key, params)` / `*transloco` | `t.asSignal(key, () => params)` / `Translate` directive |
|
|
784
|
+
| `transloco` pipe | `Translator` pipe (typed subclass) |
|
|
785
|
+
| `TranslocoService.activeLang` signal | `injectDynamicLocale()` (writable signal) |
|
|
786
|
+
| `TranslocoService.langChanges$` | `effect(() => locale())` |
|
|
787
|
+
| HTTP-loader-based lazy scope | Dynamic `import()` factory passed to `registerNamespace` |
|
|
788
|
+
| `*translocoLoading` | `locale.isLoading()` signal on `injectDynamicLocale` |
|
|
754
789
|
|
|
755
|
-
|
|
756
|
-
2. Register namespaces with resolvers
|
|
757
|
-
3. Use the translation functions/pipes/directives
|
|
790
|
+
Migration sketch:
|
|
758
791
|
|
|
759
|
-
|
|
792
|
+
1. Pick a locale strategy and configure `provideIntlConfig` accordingly (see [Example configurations](#example-configurations)).
|
|
793
|
+
2. For each transloco scope: convert the JSON into a `createNamespace('<name>', defaultTranslations)` file plus one `createXTranslation('<locale>', ...)` file per non-default locale.
|
|
794
|
+
3. In each scope's loader module, call `registerNamespace(() => import('./<ns>.namespace'), { ... })` and export the resulting `injectNamespaceT` / `resolveNamespaceTranslation`. Wire the resolver into the matching route.
|
|
795
|
+
4. Replace `TranslocoService.translate` / `translateSignal` calls with the injected typed `t`. Replace the `transloco` pipe and `*transloco` directive with typed subclasses of `Translator` and `Translate`.
|
|
760
796
|
|
|
761
|
-
### From
|
|
797
|
+
### From ngx-translate
|
|
762
798
|
|
|
763
|
-
|
|
799
|
+
Same source-level shape change as transloco but with more legacy API surface to replace:
|
|
764
800
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
801
|
+
| ngx-translate | `@mmstack/translate` |
|
|
802
|
+
| --------------------------------------------------- | --------------------------------------------------------------------------------- |
|
|
803
|
+
| `TranslateModule.forRoot({ loader: ... })` | `provideIntlConfig` + per-namespace `registerNamespace` |
|
|
804
|
+
| `TranslateService.get(key, params)` (Observable) | `t('ns.key', params)` (plain string) or `t.asSignal(...)` (Signal) |
|
|
805
|
+
| `TranslateService.instant(key, params)` | `t('ns.key', params)` |
|
|
806
|
+
| `TranslateService.use('locale')` | `injectDynamicLocale().set('locale')` |
|
|
807
|
+
| `TranslateService.onLangChange.subscribe(...)` | `effect(() => locale())` |
|
|
808
|
+
| `TranslateHttpLoader` | Dynamic `import()` factory: `() => import('./ns.<locale>').then((m) => m.default)`|
|
|
809
|
+
| `translate` pipe | Typed `Translator` pipe subclass |
|
|
810
|
+
| `MissingTranslationHandler` | Falls back to the default-locale message; dev-mode `console.warn` |
|
|
811
|
+
|
|
812
|
+
Migration sketch:
|
|
813
|
+
|
|
814
|
+
1. Convert each JSON translation file to TypeScript with `createNamespace` (default locale) + `createXTranslation` (other locales). The compiler enforces parameter and shape consistency.
|
|
815
|
+
2. Replace `TranslateService` usage with the typed `t` from `registerNamespace`. RxJS observables become plain strings (eager) or signals (`t.asSignal(...)`).
|
|
816
|
+
3. Replace `TranslateHttpLoader` with dynamic `import()` factories β your bundler code-splits each locale automatically; no HTTP fetch needed.
|
|
817
|
+
4. Pick a locale strategy: [route-based](#scenario-a-route-based-locale) (`canMatchLocale` + `localeParamName`) or [dynamic with persistence](#scenario-b-dynamic-locale-with-localstorage-persistence) (`injectDynamicLocale` + `localeStorage`).
|
|
818
|
+
5. Swap the `translate` pipe usage for a typed `Translator` pipe subclass (per namespace). Pure-pipe locale memoization needs the `locale()` argument trick β see [Step 5](#5-optional-dynamic-language-switching).
|
|
770
819
|
|
|
771
820
|
## Escape Hatches
|
|
772
821
|
|
|
773
|
-
Sometimes we all hit the limit of an api & need
|
|
822
|
+
Sometimes we all hit the limit of an api & need imperative escape hatches for those edge cases. These are the ones mmstack/translate currently provides:
|
|
774
823
|
|
|
775
824
|
**`withParams()`**
|
|
776
825
|
|
|
@@ -826,6 +875,24 @@ const signalGreeting = t.asSignal('dynamicNs.greeting', () => ({
|
|
|
826
875
|
}));
|
|
827
876
|
```
|
|
828
877
|
|
|
878
|
+
## Alternatives & comparison
|
|
879
|
+
|
|
880
|
+
`@mmstack/translate` fills a specific niche: supporting **both** traditional multi-build and modern single-build approaches with a typesafe & modular API, well-suited to nx-based environments. The table below positions it relative to the main alternatives.
|
|
881
|
+
|
|
882
|
+
| Feature | `@mmstack/translate` | `@angular/localize` | `@jsverse/transloco` | `ngx-translate` |
|
|
883
|
+
| :----------------------- | :----------------------------------: | :---------------------------: | :--------------------------------------------------------------------------------------------------: | :-------------------------: |
|
|
884
|
+
| **Build Process** | β
Single or Multi-Build | β Multi-Build (Typical) | β
Single Build | β
Single Build |
|
|
885
|
+
| **Translation Timing** | Runtime or Build Time | Compile Time | Runtime | Runtime |
|
|
886
|
+
| **Type Safety (Keys)** | β
Strong (Inferred from structure) | π‘ via extraction | π‘ Tooling/TS Files | π‘ OK Manual/Tooling |
|
|
887
|
+
| **Type Safety (Params)** | β
Strong (Inferred from ICU) | β None | π‘ Manual | π‘ Manual |
|
|
888
|
+
| **Locale Switching** | β
Dynamic (Runtime) or Page refresh | π Page Refresh Required | β
Dynamic (Runtime) | β
Dynamic (Runtime) |
|
|
889
|
+
| **Lazy Loading** | β
Built-in (Namespaces/Resolvers) | N/A (Compile Time) | β
Built-in (Scopes) | β
Yes (Custom Loaders) |
|
|
890
|
+
| **Namespacing/Scopes** | β
Built-in | β None | β
Built-in (Scopes) | π‘ Manual (File Structure) |
|
|
891
|
+
| **ICU Support** | β
Subset (via FormatJS Runtime) | β
Yes (Compile Time) | β
Yes (Runtime Intl/Plugins) | π‘ Via Extensions |
|
|
892
|
+
| **Signal Integration** | β
Great (fully reactive) | N/A | β
Good (`translateSignal()`, `activeLang` signal) | β Minimal/NoneΒΉ |
|
|
893
|
+
| **Reactive Formatters** | β
Built-in Intl integration | π‘ Angular pipes (zone-based) | β
@jsverse/transloco-locale(`transloco-locale`Β²: date/number/currency/percent, not signal-reactive) | β None (use Angular pipes) |
|
|
894
|
+
| **Maturity / Community** | π‘ Less mature, but battle tested | Core Angular | β
Mature / Active | β
Mature |
|
|
895
|
+
|
|
829
896
|
## Contributing
|
|
830
897
|
|
|
831
898
|
Contributions, issues, and feature requests are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|