@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/LICENSE +21 -21
- package/README.md +547 -349
- package/fesm2022/mmstack-translate.mjs +198 -33
- package/fesm2022/mmstack-translate.mjs.map +1 -1
- package/package.json +1 -1
- package/types/mmstack-translate.d.ts +30 -3
package/README.md
CHANGED
|
@@ -1,349 +1,547 @@
|
|
|
1
|
-
# @mmstack/translate
|
|
2
|
-
|
|
3
|
-
**Type-Safe & modular localization for Modern Angular.**
|
|
4
|
-
|
|
5
|
-
[](https://badge.fury.io/js/%40mmstack%2Ftranslate)
|
|
6
|
-
[](https://github.com/mihajm/mmstack/blob/master/LICENSE)
|
|
7
|
-
[](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. **
|
|
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
|
-
- 🚀 **
|
|
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
|
-
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
|
39
|
-
|
|
|
40
|
-
| **
|
|
41
|
-
| **
|
|
42
|
-
| **Type Safety (
|
|
43
|
-
| **
|
|
44
|
-
| **
|
|
45
|
-
| **
|
|
46
|
-
| **
|
|
47
|
-
| **
|
|
48
|
-
| **
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
},
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
import { type Routes } from '@angular/router';
|
|
196
|
-
import { resolveQuoteTranslations } from './quote.t';
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
resolve: {
|
|
203
|
-
resolveQuoteTranslations,
|
|
204
|
-
},
|
|
205
|
-
},
|
|
206
|
-
];
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
#### 2b. [OPTIONAL] Configure
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
<
|
|
243
|
-
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
1
|
+
# @mmstack/translate
|
|
2
|
+
|
|
3
|
+
**Type-Safe & modular localization for Modern Angular.**
|
|
4
|
+
|
|
5
|
+
[](https://badge.fury.io/js/%40mmstack%2Ftranslate)
|
|
6
|
+
[](https://github.com/mihajm/mmstack/blob/master/LICENSE)
|
|
7
|
+
[](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)
|