@relax.js/core 1.0.2 → 1.0.4
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 +194 -188
- package/dist/DependencyInjection.d.ts +42 -24
- package/dist/collections/Index.d.ts +2 -0
- package/dist/collections/index.js +1 -1
- package/dist/collections/index.js.map +4 -4
- package/dist/collections/index.mjs +1 -1
- package/dist/collections/index.mjs.map +4 -4
- package/dist/di/index.js +1 -1
- package/dist/di/index.js.map +3 -3
- package/dist/di/index.mjs +1 -1
- package/dist/di/index.mjs.map +3 -3
- package/dist/errors.d.ts +20 -0
- package/dist/forms/FormValidator.d.ts +1 -20
- package/dist/forms/ValidationRules.d.ts +2 -0
- package/dist/forms/index.js +1 -1
- package/dist/forms/index.js.map +4 -4
- package/dist/forms/index.mjs +1 -1
- package/dist/forms/index.mjs.map +4 -4
- package/dist/html/TableRenderer.d.ts +1 -0
- package/dist/html/index.js.map +2 -2
- package/dist/html/index.mjs.map +2 -2
- package/dist/html/template.d.ts +4 -0
- package/dist/http/http.d.ts +1 -0
- package/dist/http/index.js.map +2 -2
- package/dist/http/index.mjs.map +2 -2
- package/dist/index.d.ts +0 -2
- package/dist/index.js +3 -3
- package/dist/index.js.map +4 -4
- package/dist/index.mjs +3 -3
- package/dist/index.mjs.map +4 -4
- package/dist/routing/index.js +3 -3
- package/dist/routing/index.js.map +3 -3
- package/dist/routing/index.mjs +3 -3
- package/dist/routing/index.mjs.map +3 -3
- package/dist/routing/routeTargetRegistry.d.ts +1 -0
- package/dist/routing/types.d.ts +2 -1
- package/dist/templates/NodeTemplate.d.ts +2 -0
- package/dist/utils/index.js +1 -1
- package/dist/utils/index.js.map +2 -2
- package/dist/utils/index.mjs +1 -1
- package/dist/utils/index.mjs.map +2 -2
- package/docs/Architecture.md +333 -333
- package/docs/DependencyInjection.md +277 -237
- package/docs/Errors.md +87 -87
- package/docs/GettingStarted.md +231 -231
- package/docs/Pipes.md +5 -5
- package/docs/Translations.md +167 -312
- package/docs/WhyRelaxjs.md +336 -336
- package/docs/api/.nojekyll +1 -0
- package/docs/api/assets/hierarchy.js +1 -0
- package/docs/api/assets/highlight.css +120 -0
- package/docs/api/assets/icons.js +18 -0
- package/docs/api/assets/icons.svg +1 -0
- package/docs/api/assets/main.js +60 -0
- package/docs/api/assets/navigation.js +1 -0
- package/docs/api/assets/search.js +1 -0
- package/docs/api/assets/style.css +1633 -0
- package/docs/api/classes/http.WebSocketClient.html +26 -0
- package/docs/api/classes/i18n.LocaleChangeEvent.html +66 -0
- package/docs/api/classes/index.Blueprint.html +3 -0
- package/docs/api/classes/index.BoundNode.html +3 -0
- package/docs/api/classes/index.DigitsValidation.html +10 -0
- package/docs/api/classes/index.FormValidator.html +32 -0
- package/docs/api/classes/index.HttpError.html +13 -0
- package/docs/api/classes/index.LinkedList.html +26 -0
- package/docs/api/classes/index.NavigateRouteEvent.html +76 -0
- package/docs/api/classes/index.Node.html +15 -0
- package/docs/api/classes/index.PageSelectedEvent.html +61 -0
- package/docs/api/classes/index.Pager.html +4 -0
- package/docs/api/classes/index.RangeValidation.html +15 -0
- package/docs/api/classes/index.RelaxError.html +17 -0
- package/docs/api/classes/index.RequiredValidation.html +10 -0
- package/docs/api/classes/index.RouteError.html +11 -0
- package/docs/api/classes/index.RouteGuardError.html +12 -0
- package/docs/api/classes/index.RouteLink.html +779 -0
- package/docs/api/classes/index.RouteTarget.html +788 -0
- package/docs/api/classes/index.SSEClient.html +13 -0
- package/docs/api/classes/index.SSEDataEvent.html +63 -0
- package/docs/api/classes/index.ServiceCollection.html +28 -0
- package/docs/api/classes/index.ServiceContainer.html +24 -0
- package/docs/api/classes/index.SortChangeEvent.html +61 -0
- package/docs/api/classes/index.TableRenderer.html +5 -0
- package/docs/api/classes/index.TableSorter.html +4 -0
- package/docs/api/enums/index.GuardResult.html +9 -0
- package/docs/api/functions/elements.formError.html +6 -0
- package/docs/api/functions/elements.selectOne.html +6 -0
- package/docs/api/functions/i18n.getCurrentLocale.html +3 -0
- package/docs/api/functions/i18n.loadNamespace.html +7 -0
- package/docs/api/functions/i18n.loadNamespaces.html +6 -0
- package/docs/api/functions/i18n.onMissingTranslation.html +7 -0
- package/docs/api/functions/i18n.setLocale.html +7 -0
- package/docs/api/functions/i18n.setMessageFormatter.html +7 -0
- package/docs/api/functions/i18n.t.html +9 -0
- package/docs/api/functions/index.BooleanConverter.html +6 -0
- package/docs/api/functions/index.ContainerService.html +13 -0
- package/docs/api/functions/index.DateConverter.html +11 -0
- package/docs/api/functions/index.Inject.html +16 -0
- package/docs/api/functions/index.NumberConverter.html +5 -0
- package/docs/api/functions/index.RegisterValidator.html +7 -0
- package/docs/api/functions/index.applyPipes.html +17 -0
- package/docs/api/functions/index.asyncHandler.html +11 -0
- package/docs/api/functions/index.capitalizePipe.html +4 -0
- package/docs/api/functions/index.clearPendingNavigations.html +1 -0
- package/docs/api/functions/index.compileTemplate.html +26 -0
- package/docs/api/functions/index.configure.html +5 -0
- package/docs/api/functions/index.createBluePrint.html +1 -0
- package/docs/api/functions/index.createConverterFromDataType.html +4 -0
- package/docs/api/functions/index.createConverterFromInputType.html +5 -0
- package/docs/api/functions/index.createPipeRegistry.html +12 -0
- package/docs/api/functions/index.currencyPipe.html +9 -0
- package/docs/api/functions/index.datePipe.html +9 -0
- package/docs/api/functions/index.daysAgoPipe.html +8 -0
- package/docs/api/functions/index.defaultPipe.html +5 -0
- package/docs/api/functions/index.defineRoutes.html +8 -0
- package/docs/api/functions/index.del.html +8 -0
- package/docs/api/functions/index.findRouteByName.html +5 -0
- package/docs/api/functions/index.findRouteByUrl.html +4 -0
- package/docs/api/functions/index.firstPipe.html +4 -0
- package/docs/api/functions/index.generateSequentialId.html +21 -0
- package/docs/api/functions/index.get.html +9 -0
- package/docs/api/functions/index.getDataConverter.html +11 -0
- package/docs/api/functions/index.getParentComponent.html +18 -0
- package/docs/api/functions/index.getValidator.html +4 -0
- package/docs/api/functions/index.html.html +19 -0
- package/docs/api/functions/index.joinPipe.html +5 -0
- package/docs/api/functions/index.keysPipe.html +4 -0
- package/docs/api/functions/index.lastPipe.html +4 -0
- package/docs/api/functions/index.lowercasePipe.html +4 -0
- package/docs/api/functions/index.mapFormToClass.html +17 -0
- package/docs/api/functions/index.matchRoute.html +5 -0
- package/docs/api/functions/index.navigate.html +8 -0
- package/docs/api/functions/index.onError.html +8 -0
- package/docs/api/functions/index.piecesPipe.html +8 -0
- package/docs/api/functions/index.post.html +9 -0
- package/docs/api/functions/index.printRoutes.html +2 -0
- package/docs/api/functions/index.put.html +9 -0
- package/docs/api/functions/index.readData.html +17 -0
- package/docs/api/functions/index.registerRouteTarget.html +9 -0
- package/docs/api/functions/index.reportError.html +10 -0
- package/docs/api/functions/index.request.html +8 -0
- package/docs/api/functions/index.resolveValue.html +18 -0
- package/docs/api/functions/index.setFetch.html +6 -0
- package/docs/api/functions/index.setFormData.html +17 -0
- package/docs/api/functions/index.shortenPipe.html +5 -0
- package/docs/api/functions/index.startRouting.html +6 -0
- package/docs/api/functions/index.ternaryPipe.html +6 -0
- package/docs/api/functions/index.trimPipe.html +4 -0
- package/docs/api/functions/index.unregisterRouteTarget.html +3 -0
- package/docs/api/functions/index.uppercasePipe.html +4 -0
- package/docs/api/hierarchy.html +1 -0
- package/docs/api/index.html +323 -0
- package/docs/api/interfaces/http.SimpleDataEvent.html +3 -0
- package/docs/api/interfaces/http.WebSocketAbstraction.html +9 -0
- package/docs/api/interfaces/http.WebSocketCodec.html +4 -0
- package/docs/api/interfaces/http.WebSocketOptions.html +20 -0
- package/docs/api/interfaces/index.CompiledTemplate.html +10 -0
- package/docs/api/interfaces/index.DataLoader.html +19 -0
- package/docs/api/interfaces/index.EngineConfig.html +11 -0
- package/docs/api/interfaces/index.ErrorContext.html +4 -0
- package/docs/api/interfaces/index.FormReaderOptions.html +8 -0
- package/docs/api/interfaces/index.HttpOptions.html +16 -0
- package/docs/api/interfaces/index.HttpResponse.html +17 -0
- package/docs/api/interfaces/index.LoadRoute.html +7 -0
- package/docs/api/interfaces/index.NavigateOptions.html +7 -0
- package/docs/api/interfaces/index.PipeRegistry.html +12 -0
- package/docs/api/interfaces/index.RegistrationOptions.html +22 -0
- package/docs/api/interfaces/index.RenderTemplate.html +7 -0
- package/docs/api/interfaces/index.RequestOptions.html +11 -0
- package/docs/api/interfaces/index.Routable.html +10 -0
- package/docs/api/interfaces/index.Route.html +13 -0
- package/docs/api/interfaces/index.RouteGuard.html +2 -0
- package/docs/api/interfaces/index.RouteValue.html +6 -0
- package/docs/api/interfaces/index.SSEOptions.html +24 -0
- package/docs/api/interfaces/index.ValidationContext.html +8 -0
- package/docs/api/interfaces/index.ValidatorOptions.html +14 -0
- package/docs/api/media/Architecture.md +333 -0
- package/docs/api/media/DependencyInjection.md +277 -0
- package/docs/api/media/GettingStarted.md +231 -0
- package/docs/api/media/HttpClient.md +459 -0
- package/docs/api/media/Pipes.md +211 -0
- package/docs/api/media/Routing.md +332 -0
- package/docs/api/media/WhyRelaxjs.md +336 -0
- package/docs/api/media/forms.md +99 -0
- package/docs/api/media/html.md +175 -0
- package/docs/api/media/i18n.md +354 -0
- package/docs/api/media/utilities.md +143 -0
- package/docs/api/media/validation.md +351 -0
- package/docs/api/modules/collections_Index.html +1 -0
- package/docs/api/modules/di.html +1 -0
- package/docs/api/modules/elements.html +1 -0
- package/docs/api/modules/forms.html +1 -0
- package/docs/api/modules/html.html +1 -0
- package/docs/api/modules/http.html +1 -0
- package/docs/api/modules/i18n.html +1 -0
- package/docs/api/modules/index.html +1 -0
- package/docs/api/modules/routing.html +1 -0
- package/docs/api/modules/utils.html +1 -0
- package/docs/api/modules.html +1 -0
- package/docs/api/types/http.WebSocketFactory.html +2 -0
- package/docs/api/types/i18n.MessageFormatter.html +3 -0
- package/docs/api/types/i18n.MissingTranslationHandler.html +1 -0
- package/docs/api/types/index.Constructor.html +7 -0
- package/docs/api/types/index.ConverterFunc.html +2 -0
- package/docs/api/types/index.DataType.html +2 -0
- package/docs/api/types/index.InputType.html +2 -0
- package/docs/api/types/index.PipeFunction.html +6 -0
- package/docs/api/types/index.RouteData.html +1 -0
- package/docs/api/types/index.RouteMatchResult.html +9 -0
- package/docs/api/types/index.RouteParamType.html +1 -0
- package/docs/api/types/index.RouteSegmentType.html +2 -0
- package/docs/api/types/index.SSEEventFactory.html +5 -0
- package/docs/api/types/index.ServiceScope.html +10 -0
- package/docs/api/types/index.SortColumn.html +3 -0
- package/docs/api/variables/i18n.formatICU.html +3 -0
- package/docs/api/variables/index.container.html +6 -0
- package/docs/api/variables/index.defaultPipes.html +6 -0
- package/docs/api/variables/index.internalRoutes.html +1 -0
- package/docs/api/variables/index.serviceCollection.html +6 -0
- package/docs/api.json +93171 -0
- package/docs/elements/dom.md +102 -102
- package/docs/forms/creating-form-components.md +924 -924
- package/docs/forms/form-api.md +94 -94
- package/docs/forms/forms.md +99 -99
- package/docs/forms/patterns.md +311 -311
- package/docs/forms/reading-writing.md +365 -365
- package/docs/forms/validation.md +351 -351
- package/docs/html/TableRenderer.md +291 -291
- package/docs/html/html.md +175 -175
- package/docs/html/index.md +54 -54
- package/docs/html/template.md +422 -422
- package/docs/http/HttpClient.md +459 -459
- package/docs/http/ServerSentEvents.md +184 -184
- package/docs/http/index.md +109 -109
- package/docs/i18n/i18n.md +49 -4
- package/docs/i18n/intl-standard.md +178 -178
- package/docs/routing/RouteLink.md +98 -98
- package/docs/routing/Routing.md +332 -332
- package/docs/routing/layouts.md +207 -207
- package/docs/utilities.md +143 -143
- package/package.json +4 -3
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
# Relaxjs i18n
|
|
2
|
+
|
|
3
|
+
Namespace-based translations with ICU message format, lazy loading, and locale change events. Built on top of the [modern Intl standard](intl-standard.md).
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { setLocale, loadNamespaces, t } from '@relax.js/core/i18n';
|
|
9
|
+
|
|
10
|
+
await setLocale('sv');
|
|
11
|
+
await loadNamespaces(['r-pipes', 'r-validation']);
|
|
12
|
+
|
|
13
|
+
t('greeting', { name: 'Anna' }); // "Hej, Anna!"
|
|
14
|
+
t('items', { count: 3 }); // "3 saker"
|
|
15
|
+
t('r-pipes:daysAgo', { count: 2 }); // "2 dagar sedan"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Folder Structure
|
|
19
|
+
|
|
20
|
+
Translations live in `src/i18n/locales/{locale}/{namespace}.json`:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
src/i18n/locales/
|
|
24
|
+
├── en/
|
|
25
|
+
│ ├── r-common.json
|
|
26
|
+
│ ├── r-pipes.json
|
|
27
|
+
│ └── r-validation.json
|
|
28
|
+
└── sv/
|
|
29
|
+
├── r-common.json
|
|
30
|
+
├── r-pipes.json
|
|
31
|
+
└── r-validation.json
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Translation Keys
|
|
35
|
+
|
|
36
|
+
Use `t('namespace:key')` to translate. Omit the namespace to use `r-common`:
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
t('greeting', { name: 'Anna' }) // r-common:greeting
|
|
40
|
+
t('r-common:greeting', { name: 'Anna' }) // explicit namespace
|
|
41
|
+
t('r-pipes:today') // r-pipes namespace
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
If a key is not found, `t()` returns the key itself.
|
|
45
|
+
|
|
46
|
+
## Translation Files
|
|
47
|
+
|
|
48
|
+
### Simple Interpolation
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"greeting": "Hello, {name}!",
|
|
53
|
+
"welcome": "Welcome to {appName}"
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
t('greeting', { name: 'John' }); // "Hello, John!"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Pluralization (ICU)
|
|
62
|
+
|
|
63
|
+
Uses `Intl.PluralRules` for locale-aware category selection. The `#` token inserts the count.
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"items": "{count, plural, one {# item} other {# items}}",
|
|
68
|
+
"daysAgo": "{count, plural, one {# day ago} other {# days ago}}"
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
t('items', { count: 1 }); // "1 item"
|
|
74
|
+
t('items', { count: 5 }); // "5 items"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Exact Matches (`=N`)
|
|
78
|
+
|
|
79
|
+
Exact values take priority over plural categories:
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"pieces": "{count, plural, =0 {none} one {one} other {# pcs}}"
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
t('pieces', { count: 0 }); // "none" (exact =0 match)
|
|
89
|
+
t('pieces', { count: 1 }); // "one" (plural category)
|
|
90
|
+
t('pieces', { count: 5 }); // "5 pcs" (plural category)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Select (ICU)
|
|
94
|
+
|
|
95
|
+
Chooses a branch based on an exact string match. Always include an `other` fallback.
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"status": "{role, select, admin {Full access} editor {Can edit} other {Read only}}"
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
t('status', { role: 'admin' }); // "Full access"
|
|
105
|
+
t('status', { role: 'editor' }); // "Can edit"
|
|
106
|
+
t('status', { role: 'viewer' }); // "Read only" (other)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Number and Date Formatting
|
|
110
|
+
|
|
111
|
+
ICU format supports locale-aware number and date formatting:
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"price": "Price: {amount, number, currency}",
|
|
116
|
+
"date": "Date: {date, date, medium}"
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Example Translation Files
|
|
121
|
+
|
|
122
|
+
**`src/i18n/locales/en/r-common.json`**:
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"welcome": "Welcome to our site!",
|
|
127
|
+
"greeting": "Hello, {name}!",
|
|
128
|
+
"items": "{count, plural, =0 {No items} one {# item} other {# items}}",
|
|
129
|
+
"goodbye": "Goodbye!"
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**`src/i18n/locales/en/errors.json`**:
|
|
134
|
+
|
|
135
|
+
```json
|
|
136
|
+
{
|
|
137
|
+
"notFound": "Page not found",
|
|
138
|
+
"unauthorized": "You are not authorized to view this page",
|
|
139
|
+
"serverError": "An unexpected error occurred"
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**`src/i18n/locales/sv/r-common.json`**:
|
|
144
|
+
|
|
145
|
+
```json
|
|
146
|
+
{
|
|
147
|
+
"welcome": "Välkommen till vår sida!",
|
|
148
|
+
"greeting": "Hej, {name}!",
|
|
149
|
+
"items": "{count, plural, =0 {Inga föremål} one {# föremål} other {# föremål}}",
|
|
150
|
+
"goodbye": "Hejdå!"
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Built-in Namespaces
|
|
155
|
+
|
|
156
|
+
### r-common
|
|
157
|
+
|
|
158
|
+
General application strings. Loaded automatically with `setLocale()`.
|
|
159
|
+
|
|
160
|
+
| Key | Message (EN) |
|
|
161
|
+
|-----|-------------|
|
|
162
|
+
| `greeting` | `Hello, {name}!` |
|
|
163
|
+
| `items` | `{count, plural, one {# item} other {# items}}` |
|
|
164
|
+
|
|
165
|
+
### r-pipes
|
|
166
|
+
|
|
167
|
+
Translations for [locale-aware pipes](../Pipes.md). Load before using `daysAgo` or `pieces` pipes:
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
await loadNamespace('r-pipes');
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
| Key | Message (EN) |
|
|
174
|
+
|-----|-------------|
|
|
175
|
+
| `today` | `today` |
|
|
176
|
+
| `yesterday` | `yesterday` |
|
|
177
|
+
| `daysAgo` | `{count, plural, one {# day ago} other {# days ago}}` |
|
|
178
|
+
| `pieces` | `{count, plural, =0 {none} one {one} other {# pcs}}` |
|
|
179
|
+
|
|
180
|
+
### r-validation
|
|
181
|
+
|
|
182
|
+
Translations for [form validation](../forms/validation.md) error messages:
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
await loadNamespace('r-validation');
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
| Key | Message (EN) |
|
|
189
|
+
|-----|-------------|
|
|
190
|
+
| `required` | `This field is required.` |
|
|
191
|
+
| `range` | `Number must be between {min} and {max}, was {actual}.` |
|
|
192
|
+
| `digits` | `Please enter only digits.` |
|
|
193
|
+
|
|
194
|
+
## Locale Switching
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
await setLocale('sv'); // Clears translations, loads sv/r-common.json, dispatches event
|
|
198
|
+
await setLocale('en'); // Clears translations, loads en/r-common.json, dispatches event
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Locale codes are normalized: `en-US` becomes `en`, `sv-SE` becomes `sv`.
|
|
202
|
+
|
|
203
|
+
## LocaleChangeEvent
|
|
204
|
+
|
|
205
|
+
`setLocale()` dispatches a `LocaleChangeEvent` on `document` after loading translations. Components can listen for it to re-render:
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
document.addEventListener('localechange', (e) => {
|
|
209
|
+
console.log(`Switched to ${e.locale}`);
|
|
210
|
+
this.render();
|
|
211
|
+
});
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
The event has a typed `locale` property (the normalized locale code) and is registered on `DocumentEventMap` for type-safe listeners.
|
|
215
|
+
|
|
216
|
+
## Missing Translation Handler
|
|
217
|
+
|
|
218
|
+
Register a callback for when `t()` encounters a missing key. Useful for development tooling, logging, or collecting untranslated strings:
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
import { onMissingTranslation } from '@relax.js/core/i18n';
|
|
222
|
+
|
|
223
|
+
onMissingTranslation((key, namespace, locale) => {
|
|
224
|
+
console.warn(`Missing: ${namespace}:${key} [${locale}]`);
|
|
225
|
+
});
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Pass `null` to remove the handler:
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
onMissingTranslation(null);
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Fallback Behavior
|
|
235
|
+
|
|
236
|
+
1. If a namespace file doesn't exist for the current locale, loads from `en/`
|
|
237
|
+
2. If the key doesn't exist in the namespace, calls the missing handler (if set) and returns the key
|
|
238
|
+
3. If the namespace doesn't exist at all, logs a warning and returns the key
|
|
239
|
+
|
|
240
|
+
## Custom Message Formatter
|
|
241
|
+
|
|
242
|
+
The built-in formatter supports interpolation, pluralization, and select. For advanced ICU features (nested arguments, date/time formatting in messages), use an external library:
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
import { setMessageFormatter } from '@relax.js/core/i18n';
|
|
246
|
+
import { IntlMessageFormat } from 'intl-messageformat';
|
|
247
|
+
|
|
248
|
+
setMessageFormatter((message, values, locale) => {
|
|
249
|
+
const fmt = new IntlMessageFormat(message, locale);
|
|
250
|
+
return fmt.format(values) as string;
|
|
251
|
+
});
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## Integration with Pipes
|
|
255
|
+
|
|
256
|
+
Several [pipes](../Pipes.md) use the i18n system for localized output:
|
|
257
|
+
|
|
258
|
+
- `currency`: uses `getCurrentLocale()` for number formatting
|
|
259
|
+
- `date`: uses `getCurrentLocale()` for date formatting
|
|
260
|
+
- `daysAgo`: uses `t('r-pipes:...')` for translated text
|
|
261
|
+
- `pieces`: uses `t('r-pipes:...')` for translated text
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
await setLocale('sv');
|
|
265
|
+
await loadNamespace('r-pipes');
|
|
266
|
+
|
|
267
|
+
// Now pipes output Swedish
|
|
268
|
+
// {{createdAt | daysAgo}} → "idag", "igår", "3 dagar sedan"
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Adding a New Locale
|
|
272
|
+
|
|
273
|
+
1. Create `src/i18n/locales/{locale}/r-common.json`
|
|
274
|
+
2. Create `src/i18n/locales/{locale}/r-pipes.json`
|
|
275
|
+
3. Create `src/i18n/locales/{locale}/r-validation.json`
|
|
276
|
+
4. Add translations for all keys
|
|
277
|
+
|
|
278
|
+
Example for German:
|
|
279
|
+
|
|
280
|
+
```json
|
|
281
|
+
// src/i18n/locales/de/r-pipes.json
|
|
282
|
+
{
|
|
283
|
+
"today": "heute",
|
|
284
|
+
"yesterday": "gestern",
|
|
285
|
+
"daysAgo": "{count, plural, one {vor # Tag} other {vor # Tagen}}",
|
|
286
|
+
"pieces": "{count, plural, =0 {keine} one {eins} other {# Stück}}"
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## API Reference
|
|
291
|
+
|
|
292
|
+
### Functions
|
|
293
|
+
|
|
294
|
+
| Function | Description |
|
|
295
|
+
|----------|-------------|
|
|
296
|
+
| `setLocale(locale)` | Set locale, clear translations, load `r-common`, dispatch event |
|
|
297
|
+
| `loadNamespace(ns)` | Load a single translation namespace |
|
|
298
|
+
| `loadNamespaces(ns[])` | Load multiple namespaces in parallel |
|
|
299
|
+
| `t(key, values?)` | Translate a key with optional interpolation |
|
|
300
|
+
| `getCurrentLocale()` | Get the current normalized locale code |
|
|
301
|
+
| `onMissingTranslation(handler)` | Register/remove missing key handler |
|
|
302
|
+
| `setMessageFormatter(fn)` | Replace the default ICU formatter |
|
|
303
|
+
|
|
304
|
+
### Types
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
type MessageFormatter = (
|
|
308
|
+
message: string,
|
|
309
|
+
values?: Record<string, any>,
|
|
310
|
+
locale?: string,
|
|
311
|
+
) => string;
|
|
312
|
+
|
|
313
|
+
type MissingTranslationHandler = (
|
|
314
|
+
key: string,
|
|
315
|
+
namespace: string,
|
|
316
|
+
locale: string,
|
|
317
|
+
) => void;
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Events
|
|
321
|
+
|
|
322
|
+
| Event | Target | Property | Description |
|
|
323
|
+
|-------|--------|----------|-------------|
|
|
324
|
+
| `localechange` | `document` | `locale: string` | Fired after `setLocale()` completes |
|
|
325
|
+
|
|
326
|
+
## Example: Full Setup
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
import {
|
|
330
|
+
setLocale, loadNamespaces, t,
|
|
331
|
+
getCurrentLocale, onMissingTranslation,
|
|
332
|
+
} from '@relax.js/core/i18n';
|
|
333
|
+
|
|
334
|
+
async function initI18n() {
|
|
335
|
+
// Dev-time missing key logging
|
|
336
|
+
onMissingTranslation((key, ns, locale) => {
|
|
337
|
+
console.warn(`Missing: ${ns}:${key} [${locale}]`);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// Detect browser locale or use default
|
|
341
|
+
const browserLocale = navigator.language || 'en';
|
|
342
|
+
await setLocale(browserLocale);
|
|
343
|
+
|
|
344
|
+
// Load namespaces needed by the app
|
|
345
|
+
await loadNamespaces(['r-pipes', 'r-validation']);
|
|
346
|
+
|
|
347
|
+
console.log(`Locale: ${getCurrentLocale()}`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// React to locale changes
|
|
351
|
+
document.addEventListener('localechange', (e) => {
|
|
352
|
+
console.log(`Locale switched to ${e.locale}`);
|
|
353
|
+
});
|
|
354
|
+
```
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Utilities
|
|
2
|
+
|
|
3
|
+
Small helper functions and data structures exported from `@relax.js/core/utils`.
|
|
4
|
+
|
|
5
|
+
## generateSequentialId
|
|
6
|
+
|
|
7
|
+
Generates compact, time-ordered unique identifiers. IDs sort lexicographically in creation order, making them useful for ordered collections and databases.
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { generateSequentialId } from '@relax.js/core/utils';
|
|
11
|
+
|
|
12
|
+
const id = generateSequentialId(1);
|
|
13
|
+
// Returns a Base36 string like 'k2j8m3n5p'
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### Parameters
|
|
17
|
+
|
|
18
|
+
| Parameter | Type | Description |
|
|
19
|
+
|-----------|------|-------------|
|
|
20
|
+
| `baseId` | `number` | Unique identifier for the client/process (0 to 1,048,575) |
|
|
21
|
+
|
|
22
|
+
Use different `baseId` values for different servers or processes to avoid collisions:
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
const SERVER_ID = parseInt(process.env.SERVER_ID || '0');
|
|
26
|
+
const orderId = generateSequentialId(SERVER_ID);
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Structure
|
|
30
|
+
|
|
31
|
+
Each ID encodes 58 bits of data in Base36:
|
|
32
|
+
|
|
33
|
+
| Bits | Field | Range |
|
|
34
|
+
|------|-------|-------|
|
|
35
|
+
| 30 | Timestamp (seconds since 2025-01-01) | Until ~2059 |
|
|
36
|
+
| 8 | Per-second counter | 256 IDs/second |
|
|
37
|
+
| 20 | Client identifier | ~1M unique sources |
|
|
38
|
+
|
|
39
|
+
### Time-Sortable
|
|
40
|
+
|
|
41
|
+
IDs generated later sort after earlier IDs:
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
const id1 = generateSequentialId(0);
|
|
45
|
+
// ... wait ...
|
|
46
|
+
const id2 = generateSequentialId(0);
|
|
47
|
+
console.log(id1 < id2); // true
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Errors
|
|
51
|
+
|
|
52
|
+
- Throws if `baseId` is out of range (0 to 1,048,575)
|
|
53
|
+
- Throws if more than 256 IDs are generated in a single second
|
|
54
|
+
- Throws if timestamp exceeds range (after ~2059)
|
|
55
|
+
|
|
56
|
+
## resolveValue
|
|
57
|
+
|
|
58
|
+
Safely navigates nested object properties using a path array. Returns `undefined` if any segment is null or missing (no exceptions thrown).
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import { resolveValue } from '@relax.js/core/utils';
|
|
62
|
+
|
|
63
|
+
const user = { address: { city: 'Stockholm' } };
|
|
64
|
+
|
|
65
|
+
resolveValue(['address', 'city'], user);
|
|
66
|
+
// Returns: 'Stockholm'
|
|
67
|
+
|
|
68
|
+
resolveValue(['address', 'zip'], user);
|
|
69
|
+
// Returns: undefined
|
|
70
|
+
|
|
71
|
+
// Safe with null values
|
|
72
|
+
const data = { user: null };
|
|
73
|
+
resolveValue(['user', 'name'], data);
|
|
74
|
+
// Returns: undefined (no error)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Use with Dot-Notation Paths
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
const path = 'user.profile.avatar'.split('.');
|
|
81
|
+
const avatar = resolveValue(path, context);
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## LinkedList
|
|
85
|
+
|
|
86
|
+
A doubly-linked list with O(1) insertion and removal at both ends.
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
import { LinkedList } from '@relax.js/core/utils';
|
|
90
|
+
|
|
91
|
+
const list = new LinkedList<string>();
|
|
92
|
+
|
|
93
|
+
list.addFirst('A');
|
|
94
|
+
list.addLast('B');
|
|
95
|
+
list.addLast('C');
|
|
96
|
+
|
|
97
|
+
console.log(list.length); // 3
|
|
98
|
+
console.log(list.firstValue); // 'A'
|
|
99
|
+
console.log(list.lastValue); // 'C'
|
|
100
|
+
|
|
101
|
+
list.removeFirst(); // Returns 'A'
|
|
102
|
+
list.removeLast(); // Returns 'C'
|
|
103
|
+
console.log(list.length); // 1
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Node Access
|
|
107
|
+
|
|
108
|
+
Access the internal `Node` wrappers to traverse or remove specific nodes:
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
let node = list.first;
|
|
112
|
+
while (node) {
|
|
113
|
+
console.log(node.value);
|
|
114
|
+
node = node.next;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Remove a specific node
|
|
118
|
+
const node = list.first;
|
|
119
|
+
node.remove(); // Removes from list and updates length
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### API
|
|
123
|
+
|
|
124
|
+
| Method/Property | Description |
|
|
125
|
+
|-----------------|-------------|
|
|
126
|
+
| `addFirst(value)` | Insert at the beginning |
|
|
127
|
+
| `addLast(value)` | Insert at the end |
|
|
128
|
+
| `removeFirst()` | Remove and return the first value |
|
|
129
|
+
| `removeLast()` | Remove and return the last value |
|
|
130
|
+
| `length` | Number of nodes |
|
|
131
|
+
| `first` | First `Node` (or `undefined`) |
|
|
132
|
+
| `last` | Last `Node` (or `undefined`) |
|
|
133
|
+
| `firstValue` | Value of the first node |
|
|
134
|
+
| `lastValue` | Value of the last node |
|
|
135
|
+
|
|
136
|
+
### Node Properties
|
|
137
|
+
|
|
138
|
+
| Property | Description |
|
|
139
|
+
|----------|-------------|
|
|
140
|
+
| `value` | The stored value |
|
|
141
|
+
| `next` | Next node (or `null`) |
|
|
142
|
+
| `prev` | Previous node (or `null`) |
|
|
143
|
+
| `remove()` | Remove this node from the list |
|