@mmstack/translate 21.0.1 → 21.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,349 +1,547 @@
1
- # @mmstack/translate
2
-
3
- **Type-Safe & modular localization for Modern Angular.**
4
-
5
- [![npm version](https://badge.fury.io/js/%40mmstack%2Ftranslate.svg)](https://badge.fury.io/js/%40mmstack%2Ftranslate)
6
- [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/mihajm/mmstack/blob/master/LICENSE)
7
- [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](CONTRIBUTING.md)
8
-
9
- `@mmstack/translate` is an opinionated internationalization (i18n) library for Angular applications built with three core priorities:
10
-
11
- 1. **Maximum Type Safety:** Catch errors related to missing keys or incorrect/missing parameters at compile time.
12
- 2. **Simplified Build Process:** Lazily load translations at runtime, requiring only a **single application build** regardless of the number of supported locales.
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.
16
-
17
- ## Features
18
-
19
- - ✅ **End-to-End Type Safety:** Compile-time checks for:
20
- - Translation key existence (within a namespace).
21
- - Correct parameter names and types.
22
- - Required vs. optional parameters based on ICU message.
23
- - Structural consistency check when defining non-default locales.
24
- - 🚀 **Single Build Artifact:** Runtime translation loading.
25
- - 📦 **Namespacing:** Organize translations by feature/library (e.g., 'quotes', 'userProfile', 'common').
26
- - 🔄 **Dynamic Language Switching:** Change locales at runtime with automatic translation loading.
27
- - **Lazy Loading:** Load namespaced translations on demand using Route Resolvers.
28
- - **Reactive API:** Includes `t.asSignal()` for creating computed translation signals based on signal parameters.
29
- - 🌍 **ICU Message Syntax:** Uses FormatJS runtime for robust support of variables (`{name}`), `plural`, `select`, and `selectordinal`. (Note: Complex inline date/number formats are not the focus; use Angular's built in Pipes/format functions & use the result as variables in your translation.)
30
- - 🔗 **Shared Namespace Support:** Define common translations (e.g., 'Save', 'Cancel') in one namespace and make them type-safely accessible from others.
31
- - 🛠️ **Template Helpers:** Includes abstract `Translator` pipe and `Translate` directive for easy, type-safe templating.
32
-
33
- ### Comparison
34
-
35
- While Angular offers excellent i18n solutions like `@angular/localize` and `transloco`, `@mmstack/translate` aims to fill a specific niche.
36
-
37
- | Feature | `@mmstack/translate` | `@angular/localize` | `transloco` | `ngx-translate` |
38
- | :----------------------- | :----------------------------------: | :----------------------: | :---------------------------: | :------------------------: |
39
- | **Build Process** | Single Build | Multi-Build (Typical) | Single Build | Single Build |
40
- | **Translation Timing** | Runtime | Compile Time | Runtime | Runtime |
41
- | **Type Safety (Keys)** | Strong (Inferred from structure) | 🟡 via extraction | 🟡 Tooling/TS Files | 🟡 OK Manual/Tooling |
42
- | **Type Safety (Params)** | ✅ Strong (Inferred from ICU) | None | 🟡 Manual | 🟡 Manual |
43
- | **Locale Switching** | Dynamic (Runtime) or Page refresh | 🔄 Page Refresh Required | Dynamic (Runtime) | Dynamic (Runtime) |
44
- | **Lazy Loading** | Built-in (Namespaces/Resolvers) | N/A (Compile Time) | ✅ Built-in (Scopes) | Yes (Custom Loaders) |
45
- | **Namespacing/Scopes** | ✅ Built-in | None | ✅ Built-in (Scopes) | 🟡 Manual (File Structure) |
46
- | **ICU Support** | Subset (via FormatJS Runtime) | Yes (Compile Time) | Yes (Runtime Intl/Plugins) | 🟡 Via Extensions |
47
- | **Signal Integration** | Good (`t.asSignal()`) | N/A |Good (`translateSignal()`) | ❌ Minimal/None |
48
- | **Maturity / Community** | ✨ New | Core Angular | Mature / Active | Mature |
49
-
50
- ## Installation & Configuration
51
-
52
- Install the library & its peer dependency, `@formatjs/intl`.
53
-
54
- ```bash
55
- npm install @mmstack/translate @formatjs/intl
56
- ```
57
-
58
- If you'd like to setup @formatjs with custom properties & setup a different default locale then 'en-US' you can define these options at the root of your application.
59
-
60
- ```typescript
61
- import { provideIntlConfig } from '@mmstack/translate';
62
-
63
- const appConfig: Providers = [
64
- provideIntlConfig({
65
- defaultLocale: 'en-US', // defaults to 'en-US' if nothing is provided
66
- }),
67
- ];
68
- ```
69
-
70
- ```typescript
71
- // DEMO impl, how you actually provide LOCALE_ID & what it's based on is up to you, it just has to be available when the resolvers are called
72
-
73
- import { Component, LOCALE_ID } from '@angular/core';
74
-
75
- @Component({
76
- selector: 'app-locale-shell',
77
- template: `<router-outlet />`,
78
- })
79
- export class LocaleShellComponent {}
80
-
81
- // app.routes.ts
82
- import { Routes, ActivatedRouteSnapshot } from '@angular/router';
83
- import { Injectable, LOCALE_ID } from '@angular/core';
84
- import { QuoteComponent } from './quote.component';
85
-
86
- @Injectable({
87
- providedIn: 'root',
88
- })
89
- export class LocaleStore {
90
- locale = 'en-US';
91
- }
92
-
93
- export const routes: Routes = [
94
- {
95
- path: ':locale',
96
- component: LocaleShellComponent,
97
- resolve: {
98
- localeId: (route: ActivatedRouteSnapshot) => {
99
- const locale = route.params['locale'] || 'en-US';
100
-
101
- return route.params['locale'] || 'en-US';
102
- },
103
- },
104
- providers: [
105
- {
106
- provide: LOCALE_ID,
107
- useFactory: (store: LocaleStore) => {
108
- return store.locale;
109
- },
110
- deps: [LocaleStore],
111
- },
112
- ],
113
- loadChildren: () => import('./quote.routes').then((m) => m.QUOTE_ROUTES),
114
- },
115
- ];
116
- ```
117
-
118
- _Note:_ `@mmstack/translate` relies on Angular's `LOCALE_ID` provider for its default. You should provide a value for it. How you determine this value (e.g., hardcoded, from server config, from URL segment via a factory provider) is up to your application's architecture.
119
-
120
- By default the library assumes this `LOCALE_ID` is static for the duration of the application session and requires a page refresh to change. You can however opt in to dynamic locale switching via `injectDynamicLocale`. see usage note 5 for details :)
121
-
122
- ## Usage
123
-
124
- The core workflow involves defining namespaces, registering them (often via lazy loading), and then using the injected translation function (t), pipe, or directive.
125
-
126
- ### 1. Define Namespace & Translations
127
-
128
- Define your default locale translations (e.g., 'en-US') as a `const` TypeScript object. Use createNamespace to process it and generate helpers.
129
-
130
- ```typescript
131
- // Example: packages/quote/src/lib/quote.namespace.ts
132
- import { createNamespace } from '@mmstack/translate';
133
-
134
- // Create the namespace definition object
135
- const ns = createNamespace('quote', {
136
- pageTitle: 'Famous Quotes',
137
- greeting: 'Hello {name}!',
138
- detail: {
139
- authorLabel: 'Author',
140
- },
141
- errors: {
142
- minLength: 'Quote must be at least {min} characters long.',
143
- },
144
- stats: '{count, plural, one {# quote} other {# quotes}} available',
145
- });
146
-
147
- export default ns.translation;
148
-
149
- export type QuoteLocale = (typeof ns)['translation'];
150
-
151
- export const createQuoteTranslation = ns.createTranslation;
152
-
153
- // Note the translations should be in separate files, if you are using import() to lazy load them.
154
- // packages/quote/src/lib/quote-sl.translation.ts
155
-
156
- import { createQuoteTranslation } from './quote.namespace';
157
-
158
- // shape is typesafe (errors if you have missing or additional keys)
159
- export default createQuoteTranslation('sl-SI', {
160
- pageTitle: 'Znani Citati',
161
- greeting: 'Zdravo {name}!',
162
- detail: {
163
- authorLabel: 'Avtor',
164
- },
165
- errors: {
166
- minLength: 'Citat mora imeti vsaj {min} znakov.', // If original has variables, the translation must contain a subset of used variables (min 1)
167
- },
168
- stats: '{count, plural, =1 {# citat} =2 {# citata} few {# citati} other {# citatov}} na voljo', // also guarenteed for "complex" variables, so {count} must be used in this translation
169
- });
170
- ```
171
-
172
- ### 2. Register the namespace & load
173
-
174
- Use registerNamespace to prepare your namespace definition and obtain the injectNamespaceT function and the resolveNamespaceTranslation resolver function. Use the resolver in your Angular routes.
175
-
176
- ```typescript
177
- import { registerNamespace } from '@mmstack/translate';
178
-
179
- // Register the namespace
180
- // Example: packages/quote/src/lib/quote.t.ts
181
- const r = registerNamespace(
182
- // you can also fetch from a server if you prefer, as long as the shape is the same
183
- () => import('./quote.namespace').then((m) => m.default), // Default locale's compiled translation (functions as fallback if no locale of type provided)
184
- {
185
- // Map other locales to promise factories (dynamic imports)
186
- 'sl-SI': () => import('./quote-sl.translation').then((m) => m.default),
187
- },
188
- );
189
-
190
- export const injectQuoteT = r.injectNamespaceT;
191
- export const resolveQuoteTranslations = r.resolveNamespaceTranslation;
192
-
193
- // in the main quote route add the provided resolver
194
-
195
- import { type Routes } from '@angular/router';
196
- import { resolveQuoteTranslations } from './quote.t';
197
-
198
- // quote.routes.ts
199
- export const QUOTE_ROUTES: Routes = [
200
- {
201
- // ... component at or above where the translations need to be available
202
- resolve: {
203
- resolveQuoteTranslations,
204
- },
205
- },
206
- ];
207
- ```
208
-
209
- #### 2b. [OPTIONAL] Configure the translation pipe and/or directive
210
-
211
- ```typescript
212
- import { Pipe, Directive } from '@angular/core';
213
- import { Translator, Translate } from '@mmstack/translate';
214
- import { type QuoteLocale } from './quote.namespace';
215
-
216
- @Pipe({
217
- name: 'translate',
218
- })
219
- export class QuoteTranslator extends Translator<QuoteLocale> {}
220
-
221
- @Directive({
222
- selector: '[translate]', // input in Translate is named 'translate'
223
- })
224
- // TInput is necessary to correctly infer the variables to the key
225
- export class QuoteTranslate<TInput extends string> extends Translate<TInput, QuoteLocale> {}
226
- ```
227
-
228
- ### 3. Have fun :)
229
-
230
- ```typescript
231
- @Component({
232
- selector: 'app-quote',
233
- imports: [QuoteTranslator, QuoteTranslate],
234
- template: `
235
- <!-- Pipe validates key & variables match -->
236
- <h1>{{ 'quote.pageTitle' | translate }}</h1>
237
- <!-- Non pluralized params must be string -->
238
- <span>{{ 'quote.errors.minLength' | translate: { min: '5' } }}</span>
239
-
240
- <!-- Directive replaces innerHTML of el -->
241
- <h1 translate="quote.pageTitle"></h1>
242
- <span [translate]="['quote.errors.minLength', { min: '5' }]"></span>
243
- `,
244
- })
245
- export class QuoteComponent {
246
- protected readonly count = signal(0);
247
- private readonly t = injectQuoteT();
248
-
249
- private readonly author = this.t('quote.detail.authorLabel'); // static translation
250
-
251
- private readonly stats = this.t.asSignal('quote.stats', () => ({
252
- count: this.count(), // must be object with count parameter & type number
253
- }));
254
- }
255
- ```
256
-
257
- ### 4. [OPTIONAL] - Creating a shared/common namespace
258
-
259
- _Note:_ A shared namespace allows you to use it within other `t` functions. You are however responsible for loading it before those `t` functions initialize, usually for a shared namespace that would be at the top route.
260
-
261
- #### 4a. Define a shared namespace
262
-
263
- Same as quote example, but this time also export the `createMergedNamespace` function. This will be your new factory for other namespaces.
264
-
265
- ```typescript
266
- // Example: packages/common-locales/src/lib/common.namespace.ts
267
- import { createNamespace } from '@mmstack/translate';
268
-
269
- const ns = createNamespace('common', {
270
- yes: 'Yes',
271
- no: 'No',
272
- });
273
-
274
- // ... rest
275
-
276
- export const createAppNamespace = ns.createMergedNamespace;
277
- ```
278
-
279
- ### 4b. Use the common namespace factory instead of the library one
280
-
281
- ```typescript
282
- // Example: packages/quote/src/lib/quote.namespace.ts
283
- import { createAppNamespace } from '@org/common/locale'; // replace with your library import path
284
-
285
- // Create the namespace definition object
286
- const ns = createAppNamespace('quote', {
287
- pageTitle: 'Famous Quotes',
288
- });
289
-
290
- // registration & other stuff remains the same
291
- ```
292
-
293
- ### 4c. Have even more fun!
294
-
295
- ```typescript
296
- @Component({
297
- //...
298
- })
299
- export class QuoteComponent {
300
- private readonly t = injectQuoteT();
301
-
302
- // t function is now 'aware' of the common namespace & its translations
303
- private readonly yes = this.t('common.yes');
304
-
305
- // quote translations also work the same
306
- private readonly author = this.t('quote.detail.authorLabel');
307
- }
308
- ```
309
-
310
- ### 5 [OPTIONAl] - Add dynamic language switching
311
-
312
- `LOCALE_ID` is always used as the initial value, but you can dynamically switch that & `@mmstack/translate` will load the new translations on demand, updating as necessary.
313
-
314
- ```typescript
315
- import { injectDynamicLocale } from '@mmstack/translate';
316
-
317
- @Component({...})
318
- export class LanguageSwitcher {
319
- private readonly locale = injectDynamicLocale();
320
-
321
- switchToSlovene() {
322
- this.locale.set('sl-SI'); // Automatically loads missing translations
323
- }
324
-
325
- // Template can react to loading state
326
- @if (locale.isLoading()) {
327
- <div>Loading translations...</div>
328
- }
329
- }
330
- ```
331
-
332
- _Note:_ Due to Angular memo-izing the transform function, custom Pipe's used will not update on dynamic local switches automatically. To resolve this you can:
333
-
334
- ```typescript
335
-
336
- // Add the locale as the last parameter when calling the pipe
337
- {{'common.yes' | translate : locale()}}
338
-
339
- // or set pure to false (not recommended)
340
- @Pipe({
341
- name: 'translate',
342
- pure: false
343
- })
344
- export class QuoteTranslator extends Translator<QuoteLocale> {}
345
- ```
346
-
347
- ## Contributing
348
-
349
- Contributions, issues, and feature requests are welcome!
1
+ # @mmstack/translate
2
+
3
+ **Type-Safe & modular localization for Modern Angular.**
4
+
5
+ [![npm version](https://badge.fury.io/js/%40mmstack%2Ftranslate.svg)](https://badge.fury.io/js/%40mmstack%2Ftranslate)
6
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/mihajm/mmstack/blob/master/LICENSE)
7
+ [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](CONTRIBUTING.md)
8
+
9
+ `@mmstack/translate` is an opinionated internationalization (i18n) library for Angular applications built with three core priorities:
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.
16
+
17
+ ## Features
18
+
19
+ - ✅ **End-to-End Type Safety:** Compile-time checks for:
20
+ - Translation key existence (within a namespace).
21
+ - Correct parameter names and types.
22
+ - Required vs. optional parameters based on ICU message.
23
+ - Structural consistency check when defining non-default locales.
24
+ - 🚀 **Flexible Deployment:** Support both multi-build (traditional) and single-build (runtime) scenarios.
25
+ - 📦 **Namespacing:** Organize translations by feature/library (e.g., 'quotes', 'userProfile', 'common').
26
+ - 🔄 **Dynamic Language Switching (Optional):** Change locales at runtime with automatic translation loading.
27
+ - 🛣️ **Route-Based Locale Support (Optional):** Automatic locale detection and switching based on route parameters.
28
+ - **Lazy Loading:** Load namespaced translations on demand using Route Resolvers.
29
+ - **Reactive API:** Includes `t.asSignal()` for creating computed translation signals based on signal parameters.
30
+ - 🌍 **ICU Message Syntax:** Uses FormatJS runtime for robust support of variables (`{name}`), `plural`, `select`, and `selectordinal`. (Note: Complex inline date/number formats are not the focus; use Angular's built-in Pipes/format functions & use the result as variables in your translation.)
31
+ - 🔗 **Shared Namespace Support:** Define common translations (e.g., 'Save', 'Cancel') in one namespace and make them type-safely accessible from others.
32
+ - 🛠️ **Template Helpers:** Includes abstract `Translator` pipe and `Translate` directive for easy, type-safe templating.
33
+
34
+ ### Comparison
35
+
36
+ While Angular offers excellent i18n solutions like `@angular/localize` and `transloco`, `@mmstack/translate` aims to fill a specific niche by supporting **both** traditional multi-build and modern single-build approaches.
37
+
38
+ | Feature | `@mmstack/translate` | `@angular/localize` | `transloco` | `ngx-translate` |
39
+ | :----------------------- | :----------------------------------: | :----------------------: | :---------------------------: | :------------------------: |
40
+ | **Build Process** | Single or Multi-Build | ❌ Multi-Build (Typical) | ✅ Single Build | ✅ Single Build |
41
+ | **Translation Timing** | Runtime or Build Time | Compile Time | Runtime | Runtime |
42
+ | **Type Safety (Keys)** | ✅ Strong (Inferred from structure) | 🟡 via extraction | 🟡 Tooling/TS Files | 🟡 OK Manual/Tooling |
43
+ | **Type Safety (Params)** | Strong (Inferred from ICU) | None | 🟡 Manual | 🟡 Manual |
44
+ | **Locale Switching** | Dynamic (Runtime) or Page refresh | 🔄 Page Refresh Required | ✅ Dynamic (Runtime) | Dynamic (Runtime) |
45
+ | **Lazy Loading** | ✅ Built-in (Namespaces/Resolvers) | N/A (Compile Time) | ✅ Built-in (Scopes) | Yes (Custom Loaders) |
46
+ | **Namespacing/Scopes** | Built-in | ❌ None | Built-in (Scopes) | 🟡 Manual (File Structure) |
47
+ | **ICU Support** | ✅ Subset (via FormatJS Runtime) | Yes (Compile Time) | ✅ Yes (Runtime Intl/Plugins) | 🟡 Via Extensions |
48
+ | **Signal Integration** | Good (`t.asSignal()`) | N/A | Good (`translateSignal()`) | Minimal/None |
49
+ | **Maturity / Community** | ✨ New | Core Angular | ✅ Mature / Active | ✅ Mature |
50
+
51
+ ## Installation
52
+
53
+ Install the library & its peer dependency, `@formatjs/intl`.
54
+
55
+ ```bash
56
+ npm install @mmstack/translate @formatjs/intl
57
+ ```
58
+
59
+ ## Configuration
60
+
61
+ ### Default: Multi-Build Scenario (like @angular/localize)
62
+
63
+ By default, `@mmstack/translate` works like `@angular/localize` - it uses Angular's `LOCALE_ID` token and expects a page refresh for locale changes. This is ideal for traditional multi-build deployments where each locale has its own build artifact.
64
+
65
+ **No special configuration needed!** Just provide `LOCALE_ID`:
66
+
67
+ ```typescript
68
+ // app.config.ts
69
+ import { ApplicationConfig, LOCALE_ID } from '@angular/core';
70
+
71
+ export const appConfig: ApplicationConfig = {
72
+ providers: [
73
+ { provide: LOCALE_ID, useValue: 'en-US' }, // Set your locale
74
+ // ... other providers
75
+ ],
76
+ };
77
+ ```
78
+
79
+ The library will use this `LOCALE_ID` value and work exactly like `@angular/localize` - requiring a full page refresh to change locales.
80
+
81
+ ### Single-Build with Runtime Translation Loading
82
+
83
+ If you want a **single build** that loads translations at runtime, use `provideIntlConfig()`:
84
+
85
+ ```typescript
86
+ // app.config.ts
87
+ import { ApplicationConfig, LOCALE_ID } from '@angular/core';
88
+ import { provideIntlConfig } from '@mmstack/translate';
89
+
90
+ export const appConfig: ApplicationConfig = {
91
+ providers: [
92
+ { provide: LOCALE_ID, useValue: 'en-US' }, // Initial/fallback locale
93
+ provideIntlConfig({
94
+ defaultLocale: 'en-US',
95
+ supportedLocales: ['en-US', 'sl-SI', 'de-DE', 'fr-FR'], // Validates locale switches
96
+ }),
97
+ // ... other providers
98
+ ],
99
+ };
100
+ ```
101
+
102
+ **Optional Configuration:**
103
+
104
+ ```typescript
105
+ provideIntlConfig({
106
+ defaultLocale: 'en-US',
107
+ supportedLocales: ['en-US', 'sl-SI', 'de-DE'],
108
+
109
+ // Automatically detect and respond to locale route parameter changes, should correspond with actual param name example bellow
110
+ localeParamName: 'locale',
111
+
112
+ // Preload default locale for synchronous fallback (rarely needed)
113
+ preloadDefaultLocale: true,
114
+ });
115
+ ```
116
+
117
+ ## Usage
118
+
119
+ The core workflow involves defining namespaces, registering them (often via lazy loading), and then using the injected translation function (t), pipe, or directive.
120
+
121
+ ### 1. Define Namespace & Translations
122
+
123
+ Define your default locale translations (e.g., 'en-US') as a `const` TypeScript object. Use `createNamespace` to process it and generate helpers.
124
+
125
+ ```typescript
126
+ // Example: packages/quote/src/lib/quote.namespace.ts
127
+ import { createNamespace } from '@mmstack/translate';
128
+
129
+ // Create the namespace definition object
130
+ const ns = createNamespace('quote', {
131
+ pageTitle: 'Famous Quotes',
132
+ greeting: 'Hello {name}!',
133
+ detail: {
134
+ authorLabel: 'Author',
135
+ },
136
+ errors: {
137
+ minLength: 'Quote must be at least {min} characters long.',
138
+ },
139
+ stats: '{count, plural, one {# quote} other {# quotes}} available',
140
+ });
141
+
142
+ export default ns.translation;
143
+
144
+ export type QuoteLocale = (typeof ns)['translation'];
145
+
146
+ export const createQuoteTranslation = ns.createTranslation;
147
+ ```
148
+
149
+ **Define other locales in separate files** (for lazy loading):
150
+
151
+ ```typescript
152
+ // packages/quote/src/lib/quote-sl.translation.ts
153
+ import { createQuoteTranslation } from './quote.namespace';
154
+
155
+ // Shape is type-safe (errors if you have missing or additional keys)
156
+ export default createQuoteTranslation('sl-SI', {
157
+ pageTitle: 'Znani Citati',
158
+ greeting: 'Zdravo {name}!',
159
+ detail: {
160
+ authorLabel: 'Avtor',
161
+ },
162
+ errors: {
163
+ minLength: 'Citat mora imeti vsaj {min} znakov.', // Variables must match original
164
+ },
165
+ stats: '{count, plural, =1 {# citat} =2 {# citata} few {# citati} other {# citatov}} na voljo',
166
+ });
167
+ ```
168
+
169
+ ### 2. Register the Namespace & Load Translations
170
+
171
+ Use `registerNamespace` to prepare your namespace definition and obtain the `injectNamespaceT` function and the `resolveNamespaceTranslation` resolver function.
172
+
173
+ ```typescript
174
+ // Example: packages/quote/src/lib/quote.t.ts
175
+ import { registerNamespace } from '@mmstack/translate';
176
+
177
+ const r = registerNamespace(
178
+ // Default locale's compiled translation (functions as fallback)
179
+ () => import('./quote.namespace').then((m) => m.default),
180
+ {
181
+ // Map other locales to promise factories (dynamic imports)
182
+ 'sl-SI': () => import('./quote-sl.translation').then((m) => m.default),
183
+ // Add more locales as needed...
184
+ },
185
+ );
186
+
187
+ export const injectQuoteT = r.injectNamespaceT;
188
+ export const resolveQuoteTranslations = r.resolveNamespaceTranslation;
189
+ ```
190
+
191
+ **Add the resolver to your routes:**
192
+
193
+ ```typescript
194
+ // quote.routes.ts
195
+ import { type Routes } from '@angular/router';
196
+ import { resolveQuoteTranslations } from './quote.t';
197
+
198
+ export const QUOTE_ROUTES: Routes = [
199
+ {
200
+ path: '',
201
+ component: QuoteComponent,
202
+ resolve: {
203
+ translations: resolveQuoteTranslations, // Loads translations before component
204
+ },
205
+ },
206
+ ];
207
+ ```
208
+
209
+ #### 2b. [OPTIONAL] Configure Type-Safe Pipe and/or Directive
210
+
211
+ ```typescript
212
+ import { Pipe, Directive } from '@angular/core';
213
+ import { Translator, Translate } from '@mmstack/translate';
214
+ import { type QuoteLocale } from './quote.namespace';
215
+
216
+ @Pipe({
217
+ name: 'translate',
218
+ standalone: true,
219
+ })
220
+ export class QuoteTranslator extends Translator<QuoteLocale> {}
221
+
222
+ @Directive({
223
+ selector: '[translate]', // Input in Translate is named 'translate'
224
+ standalone: true,
225
+ })
226
+ export class QuoteTranslate<TInput extends string> extends Translate<TInput, QuoteLocale> {}
227
+ ```
228
+
229
+ ### 3. Use Translations in Components
230
+
231
+ ```typescript
232
+ import { Component, signal } from '@angular/core';
233
+ import { injectQuoteT } from './quote.t';
234
+ import { QuoteTranslator, QuoteTranslate } from './quote.helpers';
235
+
236
+ @Component({
237
+ selector: 'app-quote',
238
+ standalone: true,
239
+ imports: [QuoteTranslator, QuoteTranslate],
240
+ template: `
241
+ <!-- Pipe validates key & variables match -->
242
+ <h1>{{ 'quote.pageTitle' | translate }}</h1>
243
+ <!-- Non-pluralized params must be string -->
244
+ <span>{{ 'quote.errors.minLength' | translate: { min: '5' } }}</span>
245
+
246
+ <!-- Directive replaces textContent of element -->
247
+ <h1 translate="quote.pageTitle"></h1>
248
+ <span [translate]="['quote.errors.minLength', { min: '5' }]"></span>
249
+ `,
250
+ })
251
+ export class QuoteComponent {
252
+ protected readonly count = signal(0);
253
+ private readonly t = injectQuoteT();
254
+
255
+ // Static translation
256
+ private readonly author = this.t('quote.detail.authorLabel');
257
+
258
+ // Reactive translation with signal parameters
259
+ private readonly stats = this.t.asSignal('quote.stats', () => ({
260
+ count: this.count(), // Must match ICU parameter (type: number)
261
+ }));
262
+ }
263
+ ```
264
+
265
+ ### 4. [OPTIONAL] Route-Based Locale Detection
266
+
267
+ For applications with locale-based routing (e.g., `/en-US/quotes`, `/sl-SI/quotes`), the library can automatically detect and switch locales.
268
+
269
+ **Step 1: Configure locale parameter name**
270
+
271
+ ```typescript
272
+ // app.config.ts
273
+ import { provideIntlConfig } from '@mmstack/translate';
274
+
275
+ export const appConfig: ApplicationConfig = {
276
+ providers: [
277
+ provideIntlConfig({
278
+ defaultLocale: 'en-US',
279
+ supportedLocales: ['en-US', 'sl-SI', 'de-DE'],
280
+ localeParamName: 'locale', // Track this route parameter automatically
281
+ }),
282
+ ],
283
+ };
284
+ ```
285
+
286
+ **Step 2: Add route guard for validation**
287
+
288
+ ```typescript
289
+ // app.routes.ts
290
+ import { Routes } from '@angular/router';
291
+ import { canMatchLocale } from '@mmstack/translate';
292
+
293
+ export const routes: Routes = [
294
+ {
295
+ path: ':locale',
296
+ canMatch: [canMatchLocale()], // Validates & redirects invalid locales
297
+ children: [
298
+ {
299
+ path: 'quotes',
300
+ loadChildren: () => import('./quote/quote.routes').then((m) => m.QUOTE_ROUTES),
301
+ },
302
+ // ... other routes
303
+ ],
304
+ },
305
+ ];
306
+ ```
307
+
308
+ **That's it!** The library will:
309
+
310
+ - Detect locale changes from route parameters
311
+ - Load translations on demand for the new locale
312
+ - Update all translation outputs reactively
313
+ - Redirect invalid locales to the default
314
+
315
+ **With prefix segments:**
316
+
317
+ If your locale parameter isn't the first segment (e.g., `/app/:locale/...`):
318
+
319
+ ```typescript
320
+ {
321
+ path: 'app/:locale',
322
+ canMatch: [canMatchLocale(['app'])], // Validates second segment
323
+ children: [...]
324
+ }
325
+ ```
326
+
327
+ ### 5. [OPTIONAL] Dynamic Language Switching
328
+
329
+ For applications that need runtime language switching without page refreshes (e.g., language selector in header), use `injectDynamicLocale()`:
330
+
331
+ ```typescript
332
+ import { Component } from '@angular/core';
333
+ import { injectDynamicLocale } from '@mmstack/translate';
334
+
335
+ @Component({
336
+ selector: 'app-language-switcher',
337
+ template: `
338
+ <select [value]="locale()" (change)="changeLanguage($event)">
339
+ <option value="en-US">English</option>
340
+ <option value="sl-SI">Slovenščina</option>
341
+ <option value="de-DE">Deutsch</option>
342
+ </select>
343
+
344
+ @if (locale.isLoading()) {
345
+ <div class="spinner">Loading translations...</div>
346
+ }
347
+ `,
348
+ })
349
+ export class LanguageSwitcherComponent {
350
+ protected readonly locale = injectDynamicLocale();
351
+
352
+ changeLanguage(event: Event) {
353
+ const target = event.target as HTMLSelectElement;
354
+ this.locale.set(target.value); // Automatically loads missing translations
355
+ }
356
+ }
357
+ ```
358
+
359
+ **Features:**
360
+
361
+ - Validates against `supportedLocales` (if configured)
362
+ - Automatically loads missing namespace translations
363
+ - Provides `isLoading()` signal for UI feedback
364
+ - Works with route-based locales
365
+
366
+ **Important Note for Pure Pipes:**
367
+
368
+ Due to Angular's memoization, pure pipes don't automatically react to locale changes. Solutions:
369
+
370
+ ```typescript
371
+ // Option 1: Pass locale as parameter (recommended)
372
+ {{ 'common.yes' | translate : locale() }}
373
+
374
+ // Option 2: Make pipe impure (not recommended for performance)
375
+ @Pipe({
376
+ name: 'translate',
377
+ pure: false,
378
+ })
379
+ export class QuoteTranslator extends Translator<QuoteLocale> {}
380
+ ```
381
+
382
+ ### 6. [OPTIONAL] Creating a Shared/Common Namespace
383
+
384
+ 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.
385
+
386
+ **Step 1: Define a shared namespace**
387
+
388
+ ```typescript
389
+ // packages/common/src/lib/common.namespace.ts
390
+ import { createNamespace } from '@mmstack/translate';
391
+
392
+ const ns = createNamespace('common', {
393
+ yes: 'Yes',
394
+ no: 'No',
395
+ save: 'Save',
396
+ cancel: 'Cancel',
397
+ delete: 'Delete',
398
+ });
399
+
400
+ export default ns.translation;
401
+ export type CommonLocale = (typeof ns)['translation'];
402
+ export const createCommonTranslation = ns.createTranslation;
403
+
404
+ // Export this for other namespaces to use
405
+ export const createAppNamespace = ns.createMergedNamespace;
406
+ ```
407
+
408
+ **Step 2: Register the common namespace at the top level**
409
+
410
+ ```typescript
411
+ // common.t.ts
412
+ import { registerNamespace } from '@mmstack/translate';
413
+
414
+ const r = registerNamespace(() => import('./common.namespace').then((m) => m.default), {
415
+ 'sl-SI': () => import('./common-sl.translation').then((m) => m.default),
416
+ });
417
+
418
+ export const injectCommonT = r.injectNamespaceT;
419
+ export const resolveCommonTranslations = r.resolveNamespaceTranslation;
420
+ ```
421
+
422
+ ```typescript
423
+ // app.routes.ts - resolve at top level
424
+ export const routes: Routes = [
425
+ {
426
+ path: '',
427
+ resolve: {
428
+ common: resolveCommonTranslations, // Load common translations first
429
+ },
430
+ children: [
431
+ // ... other routes
432
+ ],
433
+ },
434
+ ];
435
+ ```
436
+
437
+ **Step 3: Use the shared namespace factory in other namespaces**
438
+
439
+ ```typescript
440
+ // packages/quote/src/lib/quote.namespace.ts
441
+ import { createAppNamespace } from '@org/common'; // Your import path
442
+
443
+ const ns = createAppNamespace('quote', {
444
+ pageTitle: 'Famous Quotes',
445
+ // ... other translations
446
+ });
447
+
448
+ export default ns.translation;
449
+ // ... rest remains the same
450
+ ```
451
+
452
+ **Step 4: Access both namespaces in components**
453
+
454
+ ```typescript
455
+ @Component({...})
456
+ export class QuoteComponent {
457
+ private readonly t = injectQuoteT();
458
+
459
+ // Access common namespace translations
460
+ private readonly yesLabel = this.t('common.yes');
461
+ private readonly saveLabel = this.t('common.save');
462
+
463
+ // Access quote namespace translations
464
+ private readonly title = this.t('quote.pageTitle');
465
+ }
466
+ ```
467
+
468
+ ## Helper Functions
469
+
470
+ ### Core Injection Functions
471
+
472
+ **`injectDefaultLocale(): string`**
473
+ Returns the configured default locale or falls back to `LOCALE_ID`.
474
+
475
+ **`injectSupportedLocales(): string[]`**
476
+ Returns the array of supported locales or defaults to `[defaultLocale]`.
477
+
478
+ **`injectIntl(): Signal<IntlShape>`**
479
+ Directly access the FormatJS `Intl` instance for advanced formatting needs.
480
+
481
+ ```typescript
482
+ import { injectIntl } from '@mmstack/translate';
483
+
484
+ const intl = injectIntl();
485
+ const formatted = intl().formatNumber(1234.56, {
486
+ style: 'currency',
487
+ currency: 'EUR',
488
+ });
489
+ ```
490
+
491
+ **`injectDynamicLocale(): WritableSignal<string> & { isLoading: Signal<boolean> }`**
492
+ Inject a dynamic locale signal for runtime language switching.
493
+
494
+ ### Route Utilities
495
+
496
+ **`canMatchLocale(prefixSegments?: string[]): CanMatchFn`**
497
+ Route guard that validates locale parameters against `supportedLocales` and redirects invalid locales to the default.
498
+
499
+ ## Advanced: Architecture & Performance
500
+
501
+ ### Resource-Based Translation Loading
502
+
503
+ The library uses Angular's `resource()` API for efficient, reactive translation loading:
504
+
505
+ - Automatic request deduplication
506
+ - Built-in loading states
507
+ - Cancellation support via `AbortSignal`
508
+ - Better error handling
509
+
510
+ ### On-Demand Translation Loading
511
+
512
+ When switching locales dynamically, the library:
513
+
514
+ 1. Checks which namespaces need translations for the new locale
515
+ 2. Loads only the missing translations in parallel
516
+ 3. Updates all reactive outputs automatically
517
+ 4. Falls back to the default locale if unavailable
518
+
519
+ ## Migration from Other Libraries
520
+
521
+ ### From @angular/localize
522
+
523
+ `@mmstack/translate` can work exactly like `@angular/localize` by default - no migration needed for the build process! Simply:
524
+
525
+ 1. Define your translations using `createNamespace`
526
+ 2. Register namespaces with resolvers
527
+ 3. Use the translation functions/pipes/directives
528
+
529
+ The main difference is the namespace organization and type safety.
530
+
531
+ ### From transloco/ngx-translate
532
+
533
+ If you're migrating from a runtime-only solution:
534
+
535
+ 1. Configure `provideIntlConfig()` with your supported locales
536
+ 2. Use `localeParamName` if you have route-based locales
537
+ 3. Use `injectDynamicLocale()` for programmatic locale switching
538
+ 4. Convert your translation JSON files to TypeScript using `createNamespace`
539
+ 5. Update component/template usage to use the type-safe APIs
540
+
541
+ ## Contributing
542
+
543
+ Contributions, issues, and feature requests are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details.
544
+
545
+ ## License
546
+
547
+ MIT © [Mihael Mulec](https://github.com/mihajm)