@relax.js/core 1.0.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 -0
- package/README.md +188 -0
- package/dist/DataLoader.d.ts +51 -0
- package/dist/DependencyInjection.d.ts +271 -0
- package/dist/DependencyInjectionOld.d.ts +35 -0
- package/dist/Metadata.d.ts +8 -0
- package/dist/SequentialId.d.ts +47 -0
- package/dist/_alt/src/MustardEngine.d.ts +30 -0
- package/dist/_alt/src/MustardParser.d.ts +63 -0
- package/dist/_alt/src/MustardParser2.d.ts +35 -0
- package/dist/_alt/src/pipes.d.ts +93 -0
- package/dist/_alt/src/template.d.ts +166 -0
- package/dist/_alt/src/tools.d.ts +4 -0
- package/dist/_alt/tests/pipes.tests.d.ts +1 -0
- package/dist/_alt/tests/template.tests.d.ts +1 -0
- package/dist/_alt/vitest.config.d.ts +2 -0
- package/dist/collections/Index.d.ts +1 -0
- package/dist/collections/LinkedList.d.ts +75 -0
- package/dist/collections/Pager.d.ts +15 -0
- package/dist/collections/index.js +2 -0
- package/dist/collections/index.js.map +7 -0
- package/dist/collections/index.mjs +2 -0
- package/dist/collections/index.mjs.map +7 -0
- package/dist/components/Table.d.ts +13 -0
- package/dist/components/index.d.ts +4 -0
- package/dist/components/index.js +128 -0
- package/dist/components/index.js.map +7 -0
- package/dist/components/index.mjs +128 -0
- package/dist/components/index.mjs.map +7 -0
- package/dist/components/lists/Table.d.ts +59 -0
- package/dist/components/lists/TreeView.d.ts +67 -0
- package/dist/components/lists/index.d.ts +2 -0
- package/dist/components/loader.d.ts +60 -0
- package/dist/components/menus/MenuItem.d.ts +30 -0
- package/dist/components/menus/TopMenu.d.ts +16 -0
- package/dist/components/menus/index.d.ts +2 -0
- package/dist/components/panels/tabs.d.ts +15 -0
- package/dist/di/index.d.ts +1 -0
- package/dist/di/index.js +2 -0
- package/dist/di/index.js.map +7 -0
- package/dist/di/index.mjs +2 -0
- package/dist/di/index.mjs.map +7 -0
- package/dist/elements/CopyAttributes.d.ts +2 -0
- package/dist/elements/dom.d.ts +18 -0
- package/dist/elements/index.d.ts +2 -0
- package/dist/elements/index.js +2 -0
- package/dist/elements/index.js.map +7 -0
- package/dist/elements/index.mjs +2 -0
- package/dist/elements/index.mjs.map +7 -0
- package/dist/errors.d.ts +71 -0
- package/dist/forms/FormReader.d.ts +182 -0
- package/dist/forms/FormValidator.d.ts +114 -0
- package/dist/forms/ValidationRules.d.ts +103 -0
- package/dist/forms/index.d.ts +4 -0
- package/dist/forms/index.js +2 -0
- package/dist/forms/index.js.map +7 -0
- package/dist/forms/index.mjs +2 -0
- package/dist/forms/index.mjs.map +7 -0
- package/dist/forms/setFormData.d.ts +49 -0
- package/dist/getParentComponent.d.ts +43 -0
- package/dist/html/TableRenderer.d.ts +44 -0
- package/dist/html/TreeBinder.d.ts +9 -0
- package/dist/html/html.d.ts +55 -0
- package/dist/html/index.d.ts +5 -0
- package/dist/html/index.js +2 -0
- package/dist/html/index.js.map +7 -0
- package/dist/html/index.mjs +2 -0
- package/dist/html/index.mjs.map +7 -0
- package/dist/html/template.d.ts +167 -0
- package/dist/http/ServerSentEvents.d.ts +116 -0
- package/dist/http/SimpleWebSocket.d.ts +153 -0
- package/dist/http/http.d.ts +177 -0
- package/dist/http/index.d.ts +3 -0
- package/dist/http/index.js +2 -0
- package/dist/http/index.js.map +7 -0
- package/dist/http/index.mjs +2 -0
- package/dist/http/index.mjs.map +7 -0
- package/dist/i18n/i18n.d.ts +105 -0
- package/dist/i18n/icu.d.ts +64 -0
- package/dist/i18n/index.d.ts +2 -0
- package/dist/i18n/index.js +2 -0
- package/dist/i18n/index.js.map +7 -0
- package/dist/i18n/index.mjs +2 -0
- package/dist/i18n/index.mjs.map +7 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +7 -0
- package/dist/index.mjs +5 -0
- package/dist/index.mjs.map +7 -0
- package/dist/lib/DataLoader.d.ts +51 -0
- package/dist/lib/DependencyInjection.d.ts +271 -0
- package/dist/lib/InvokeParent.d.ts +10 -0
- package/dist/lib/Pipes.d.ts +236 -0
- package/dist/lib/SequentialId.d.ts +47 -0
- package/dist/lib/collections/Index.d.ts +1 -0
- package/dist/lib/collections/LinkedList.d.ts +75 -0
- package/dist/lib/collections/Pager.d.ts +15 -0
- package/dist/lib/collections/TableRenderer.d.ts +44 -0
- package/dist/lib/di/index.d.ts +1 -0
- package/dist/lib/elements/CopyAttributes.d.ts +2 -0
- package/dist/lib/elements/dom.d.ts +18 -0
- package/dist/lib/elements/index.d.ts +2 -0
- package/dist/lib/errors.d.ts +71 -0
- package/dist/lib/forms/FormReader.d.ts +182 -0
- package/dist/lib/forms/FormValidator.d.ts +114 -0
- package/dist/lib/forms/ValidationRules.d.ts +103 -0
- package/dist/lib/forms/index.d.ts +4 -0
- package/dist/lib/forms/setFormData.d.ts +49 -0
- package/dist/lib/getParentComponent.d.ts +43 -0
- package/dist/lib/html/TableRenderer.d.ts +44 -0
- package/dist/lib/html/TreeBinder.d.ts +9 -0
- package/dist/lib/html/html.d.ts +55 -0
- package/dist/lib/html/html2.d.ts +55 -0
- package/dist/lib/html/index.d.ts +5 -0
- package/dist/lib/html/m.d.ts +167 -0
- package/dist/lib/html/m2.d.ts +8 -0
- package/dist/lib/html/m3.d.ts +0 -0
- package/dist/lib/html/template.d.ts +167 -0
- package/dist/lib/http/HttpClient.d.ts +153 -0
- package/dist/lib/http/ServerSentEvents.d.ts +116 -0
- package/dist/lib/http/SimpleWebSocket.d.ts +153 -0
- package/dist/lib/http/http.d.ts +177 -0
- package/dist/lib/http/index.d.ts +3 -0
- package/dist/lib/i18n/i18n.d.ts +105 -0
- package/dist/lib/i18n/icu.d.ts +64 -0
- package/dist/lib/i18n/index.d.ts +2 -0
- package/dist/lib/index.d.ts +16 -0
- package/dist/lib/routing/NavigateRouteEvent.d.ts +52 -0
- package/dist/lib/routing/RouteLink.d.ts +7 -0
- package/dist/lib/routing/Routing.d.ts +270 -0
- package/dist/lib/routing/RoutingTarget.d.ts +22 -0
- package/dist/lib/routing/index.d.ts +7 -0
- package/dist/lib/routing/navigation.d.ts +70 -0
- package/dist/lib/routing/routeMatching.d.ts +21 -0
- package/dist/lib/routing/routeTargetRegistry.d.ts +23 -0
- package/dist/lib/routing/types.d.ts +130 -0
- package/dist/lib/templates/NodeTemplate.d.ts +38 -0
- package/dist/lib/templates/accessorParser.d.ts +87 -0
- package/dist/lib/templates/parseTemplate.d.ts +6 -0
- package/dist/lib/templates/tokenizer.d.ts +76 -0
- package/dist/lib/tools.d.ts +30 -0
- package/dist/lib/utils/index.d.ts +4 -0
- package/dist/pipes.d.ts +236 -0
- package/dist/routing/NavigateRouteEvent.d.ts +52 -0
- package/dist/routing/RouteLink.d.ts +7 -0
- package/dist/routing/RoutingTarget.d.ts +22 -0
- package/dist/routing/index.d.ts +7 -0
- package/dist/routing/index.js +5 -0
- package/dist/routing/index.js.map +7 -0
- package/dist/routing/index.mjs +5 -0
- package/dist/routing/index.mjs.map +7 -0
- package/dist/routing/navigation.d.ts +70 -0
- package/dist/routing/routeMatching.d.ts +21 -0
- package/dist/routing/routeTargetRegistry.d.ts +23 -0
- package/dist/routing/types.d.ts +130 -0
- package/dist/templates/NodeTemplate.d.ts +38 -0
- package/dist/templates/accessorParser.d.ts +87 -0
- package/dist/templates/parseTemplate.d.ts +6 -0
- package/dist/templates/tokenizer.d.ts +76 -0
- package/dist/tools.d.ts +30 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.js +2 -0
- package/dist/utils/index.js.map +7 -0
- package/dist/utils/index.mjs +2 -0
- package/dist/utils/index.mjs.map +7 -0
- package/docs/Architecture.md +333 -0
- package/docs/DependencyInjection.md +237 -0
- package/docs/Errors.md +87 -0
- package/docs/GettingStarted.md +231 -0
- package/docs/Pipes.md +211 -0
- package/docs/Translations.md +312 -0
- package/docs/WhyRelaxjs.md +336 -0
- package/docs/elements/dom.md +102 -0
- package/docs/forms/creating-form-components.md +924 -0
- package/docs/forms/form-api.md +94 -0
- package/docs/forms/forms.md +99 -0
- package/docs/forms/patterns.md +311 -0
- package/docs/forms/reading-writing.md +365 -0
- package/docs/forms/validation.md +351 -0
- package/docs/html/TableRenderer.md +292 -0
- package/docs/html/html.md +175 -0
- package/docs/html/index.md +54 -0
- package/docs/html/template.md +422 -0
- package/docs/http/HttpClient.md +459 -0
- package/docs/http/ServerSentEvents.md +184 -0
- package/docs/http/index.md +109 -0
- package/docs/i18n/i18n.md +309 -0
- package/docs/i18n/intl-standard.md +178 -0
- package/docs/routing/RouteLink.md +98 -0
- package/docs/routing/Routing.md +332 -0
- package/docs/routing/RoutingTarget.md +136 -0
- package/docs/routing/layouts.md +207 -0
- package/docs/utilities.md +143 -0
- package/package.json +93 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# HTTP, WebSocket & SSE
|
|
2
|
+
|
|
3
|
+
This module provides HTTP, WebSocket, and Server-Sent Events clients for network communication.
|
|
4
|
+
|
|
5
|
+
## Available Features
|
|
6
|
+
|
|
7
|
+
| Feature | Description | Use Case |
|
|
8
|
+
|---------|-------------|----------|
|
|
9
|
+
| [HTTP Client](HttpClient.md#http-client) | Type-safe HTTP functions | REST API calls with JWT handling |
|
|
10
|
+
| [WebSocketClient](HttpClient.md#websocket-client) | WebSocket client with auto-reconnect | Real-time bidirectional messaging |
|
|
11
|
+
| [SSEClient](ServerSentEvents.md) | Server-Sent Events as DOM events | Server push notifications |
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
### HTTP Requests
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { configure, get, post } from 'relaxjs/http';
|
|
19
|
+
|
|
20
|
+
configure({ baseUrl: '/api/v1' });
|
|
21
|
+
|
|
22
|
+
// GET with query parameters
|
|
23
|
+
const response = await get('/users', { status: 'active' });
|
|
24
|
+
if (response.success) {
|
|
25
|
+
const users = response.as<User[]>();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// POST with JSON body
|
|
29
|
+
const result = await post('/users', JSON.stringify({ name: 'John' }));
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### WebSocket Messaging
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
import { WebSocketClient } from 'relaxjs/http';
|
|
36
|
+
|
|
37
|
+
const ws = new WebSocketClient<Message>('wss://api.example.com');
|
|
38
|
+
ws.connect();
|
|
39
|
+
|
|
40
|
+
// Check connection state
|
|
41
|
+
console.log(ws.connected); // true when connected
|
|
42
|
+
|
|
43
|
+
// Send (queued if disconnected)
|
|
44
|
+
ws.send({ type: 'chat', text: 'Hello' });
|
|
45
|
+
|
|
46
|
+
// Receive (rejects if connection closes)
|
|
47
|
+
try {
|
|
48
|
+
const message = await ws.receive();
|
|
49
|
+
} catch (e) {
|
|
50
|
+
console.log('Connection closed');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Disconnect when done
|
|
54
|
+
ws.disconnect();
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Server-Sent Events
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
import { SSEClient } from 'relaxjs/http';
|
|
61
|
+
|
|
62
|
+
const sse = new SSEClient('/api/events', {
|
|
63
|
+
eventTypes: ['user-updated', 'notification']
|
|
64
|
+
});
|
|
65
|
+
sse.connect();
|
|
66
|
+
|
|
67
|
+
// Events dispatch to document by default
|
|
68
|
+
document.addEventListener('notification', (e: SSEMessageEvent) => {
|
|
69
|
+
showNotification(e.data);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Disconnect when done
|
|
73
|
+
sse.disconnect();
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Key Features
|
|
77
|
+
|
|
78
|
+
### HTTP Client
|
|
79
|
+
|
|
80
|
+
- Standalone functions (`get`, `post`, `put`, `del`, `request`)
|
|
81
|
+
- Module-wide configuration via `configure()`
|
|
82
|
+
- Automatic base URL prefixing
|
|
83
|
+
- JWT token from localStorage (configurable)
|
|
84
|
+
- Query string building
|
|
85
|
+
- Type-safe response casting
|
|
86
|
+
- Replaceable fetch for testing via `setFetch()`
|
|
87
|
+
|
|
88
|
+
### WebSocketClient
|
|
89
|
+
|
|
90
|
+
- Auto-reconnect with exponential backoff
|
|
91
|
+
- Message queuing when disconnected
|
|
92
|
+
- Custom message codecs
|
|
93
|
+
- Type-safe generic messages
|
|
94
|
+
- Connection state tracking
|
|
95
|
+
- Graceful disconnect support
|
|
96
|
+
|
|
97
|
+
### SSEClient
|
|
98
|
+
|
|
99
|
+
- Dispatches SSE events as DOM events
|
|
100
|
+
- Configurable target element or document
|
|
101
|
+
- Auto-reconnect with exponential backoff
|
|
102
|
+
- Automatic JSON parsing
|
|
103
|
+
|
|
104
|
+
## Choosing the Right Tool
|
|
105
|
+
|
|
106
|
+
- **REST API calls?** → Use `get`, `post`, `put`, `del`
|
|
107
|
+
- **Bidirectional real-time?** → Use `WebSocketClient`
|
|
108
|
+
- **Server push only?** → Use `SSEClient`
|
|
109
|
+
- **Both?** → Use `HttpClient` for requests, `WebSocketClient` or `SSEClient` for subscriptions
|
|
@@ -0,0 +1,309 @@
|
|
|
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 'relaxjs/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
|
+
## Built-in Namespaces
|
|
110
|
+
|
|
111
|
+
### r-common
|
|
112
|
+
|
|
113
|
+
General application strings. Loaded automatically with `setLocale()`.
|
|
114
|
+
|
|
115
|
+
| Key | Message (EN) |
|
|
116
|
+
|-----|-------------|
|
|
117
|
+
| `greeting` | `Hello, {name}!` |
|
|
118
|
+
| `items` | `{count, plural, one {# item} other {# items}}` |
|
|
119
|
+
|
|
120
|
+
### r-pipes
|
|
121
|
+
|
|
122
|
+
Translations for [locale-aware pipes](../Pipes.md). Load before using `daysAgo` or `pieces` pipes:
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
await loadNamespace('r-pipes');
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
| Key | Message (EN) |
|
|
129
|
+
|-----|-------------|
|
|
130
|
+
| `today` | `today` |
|
|
131
|
+
| `yesterday` | `yesterday` |
|
|
132
|
+
| `daysAgo` | `{count, plural, one {# day ago} other {# days ago}}` |
|
|
133
|
+
| `pieces` | `{count, plural, =0 {none} one {one} other {# pcs}}` |
|
|
134
|
+
|
|
135
|
+
### r-validation
|
|
136
|
+
|
|
137
|
+
Translations for [form validation](../forms/validation.md) error messages:
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
await loadNamespace('r-validation');
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
| Key | Message (EN) |
|
|
144
|
+
|-----|-------------|
|
|
145
|
+
| `required` | `This field is required.` |
|
|
146
|
+
| `range` | `Number must be between {min} and {max}, was {actual}.` |
|
|
147
|
+
| `digits` | `Please enter only digits.` |
|
|
148
|
+
|
|
149
|
+
## Locale Switching
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
await setLocale('sv'); // Clears translations, loads sv/r-common.json, dispatches event
|
|
153
|
+
await setLocale('en'); // Clears translations, loads en/r-common.json, dispatches event
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Locale codes are normalized: `en-US` becomes `en`, `sv-SE` becomes `sv`.
|
|
157
|
+
|
|
158
|
+
## LocaleChangeEvent
|
|
159
|
+
|
|
160
|
+
`setLocale()` dispatches a `LocaleChangeEvent` on `document` after loading translations. Components can listen for it to re-render:
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
document.addEventListener('localechange', (e) => {
|
|
164
|
+
console.log(`Switched to ${e.locale}`);
|
|
165
|
+
this.render();
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
The event has a typed `locale` property (the normalized locale code) and is registered on `DocumentEventMap` for type-safe listeners.
|
|
170
|
+
|
|
171
|
+
## Missing Translation Handler
|
|
172
|
+
|
|
173
|
+
Register a callback for when `t()` encounters a missing key. Useful for development tooling, logging, or collecting untranslated strings:
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
import { onMissingTranslation } from 'relaxjs/i18n';
|
|
177
|
+
|
|
178
|
+
onMissingTranslation((key, namespace, locale) => {
|
|
179
|
+
console.warn(`Missing: ${namespace}:${key} [${locale}]`);
|
|
180
|
+
});
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Pass `null` to remove the handler:
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
onMissingTranslation(null);
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Fallback Behavior
|
|
190
|
+
|
|
191
|
+
1. If a namespace file doesn't exist for the current locale, loads from `en/`
|
|
192
|
+
2. If the key doesn't exist in the namespace, calls the missing handler (if set) and returns the key
|
|
193
|
+
3. If the namespace doesn't exist at all, logs a warning and returns the key
|
|
194
|
+
|
|
195
|
+
## Custom Message Formatter
|
|
196
|
+
|
|
197
|
+
The built-in formatter supports interpolation, pluralization, and select. For advanced ICU features (nested arguments, date/time formatting in messages), use an external library:
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
import { setMessageFormatter } from 'relaxjs/i18n';
|
|
201
|
+
import { IntlMessageFormat } from 'intl-messageformat';
|
|
202
|
+
|
|
203
|
+
setMessageFormatter((message, values, locale) => {
|
|
204
|
+
const fmt = new IntlMessageFormat(message, locale);
|
|
205
|
+
return fmt.format(values) as string;
|
|
206
|
+
});
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Integration with Pipes
|
|
210
|
+
|
|
211
|
+
Several [pipes](../Pipes.md) use the i18n system for localized output:
|
|
212
|
+
|
|
213
|
+
- `currency`: uses `getCurrentLocale()` for number formatting
|
|
214
|
+
- `date`: uses `getCurrentLocale()` for date formatting
|
|
215
|
+
- `daysAgo`: uses `t('r-pipes:...')` for translated text
|
|
216
|
+
- `pieces`: uses `t('r-pipes:...')` for translated text
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
await setLocale('sv');
|
|
220
|
+
await loadNamespace('r-pipes');
|
|
221
|
+
|
|
222
|
+
// Now pipes output Swedish
|
|
223
|
+
// {{createdAt | daysAgo}} → "idag", "igår", "3 dagar sedan"
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Adding a New Locale
|
|
227
|
+
|
|
228
|
+
1. Create `src/i18n/locales/{locale}/r-common.json`
|
|
229
|
+
2. Create `src/i18n/locales/{locale}/r-pipes.json`
|
|
230
|
+
3. Create `src/i18n/locales/{locale}/r-validation.json`
|
|
231
|
+
4. Add translations for all keys
|
|
232
|
+
|
|
233
|
+
Example for German:
|
|
234
|
+
|
|
235
|
+
```json
|
|
236
|
+
// src/i18n/locales/de/r-pipes.json
|
|
237
|
+
{
|
|
238
|
+
"today": "heute",
|
|
239
|
+
"yesterday": "gestern",
|
|
240
|
+
"daysAgo": "{count, plural, one {vor # Tag} other {vor # Tagen}}",
|
|
241
|
+
"pieces": "{count, plural, =0 {keine} one {eins} other {# Stück}}"
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## API Reference
|
|
246
|
+
|
|
247
|
+
### Functions
|
|
248
|
+
|
|
249
|
+
| Function | Description |
|
|
250
|
+
|----------|-------------|
|
|
251
|
+
| `setLocale(locale)` | Set locale, clear translations, load `r-common`, dispatch event |
|
|
252
|
+
| `loadNamespace(ns)` | Load a single translation namespace |
|
|
253
|
+
| `loadNamespaces(ns[])` | Load multiple namespaces in parallel |
|
|
254
|
+
| `t(key, values?)` | Translate a key with optional interpolation |
|
|
255
|
+
| `getCurrentLocale()` | Get the current normalized locale code |
|
|
256
|
+
| `onMissingTranslation(handler)` | Register/remove missing key handler |
|
|
257
|
+
| `setMessageFormatter(fn)` | Replace the default ICU formatter |
|
|
258
|
+
|
|
259
|
+
### Types
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
type MessageFormatter = (
|
|
263
|
+
message: string,
|
|
264
|
+
values?: Record<string, any>,
|
|
265
|
+
locale?: string,
|
|
266
|
+
) => string;
|
|
267
|
+
|
|
268
|
+
type MissingTranslationHandler = (
|
|
269
|
+
key: string,
|
|
270
|
+
namespace: string,
|
|
271
|
+
locale: string,
|
|
272
|
+
) => void;
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Events
|
|
276
|
+
|
|
277
|
+
| Event | Target | Property | Description |
|
|
278
|
+
|-------|--------|----------|-------------|
|
|
279
|
+
| `localechange` | `document` | `locale: string` | Fired after `setLocale()` completes |
|
|
280
|
+
|
|
281
|
+
## Example: Full Setup
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
import {
|
|
285
|
+
setLocale, loadNamespaces, t,
|
|
286
|
+
getCurrentLocale, onMissingTranslation,
|
|
287
|
+
} from 'relaxjs/i18n';
|
|
288
|
+
|
|
289
|
+
async function initI18n() {
|
|
290
|
+
// Dev-time missing key logging
|
|
291
|
+
onMissingTranslation((key, ns, locale) => {
|
|
292
|
+
console.warn(`Missing: ${ns}:${key} [${locale}]`);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// Detect browser locale or use default
|
|
296
|
+
const browserLocale = navigator.language || 'en';
|
|
297
|
+
await setLocale(browserLocale);
|
|
298
|
+
|
|
299
|
+
// Load namespaces needed by the app
|
|
300
|
+
await loadNamespaces(['r-pipes', 'r-validation']);
|
|
301
|
+
|
|
302
|
+
console.log(`Locale: ${getCurrentLocale()}`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// React to locale changes
|
|
306
|
+
document.addEventListener('localechange', (e) => {
|
|
307
|
+
console.log(`Locale switched to ${e.locale}`);
|
|
308
|
+
});
|
|
309
|
+
```
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# The Modern Intl Standard
|
|
2
|
+
|
|
3
|
+
The browser's built-in [`Intl`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl) object provides locale-aware formatting for numbers, dates, strings, and more. It is supported in all modern browsers and Node.js, so no polyfills or libraries are needed.
|
|
4
|
+
|
|
5
|
+
Relaxjs uses `Intl` internally (e.g. `Intl.PluralRules` for ICU pluralization) and exposes the current locale via `getCurrentLocale()` so pipes and components can pass it directly to `Intl` APIs.
|
|
6
|
+
|
|
7
|
+
## Intl.PluralRules
|
|
8
|
+
|
|
9
|
+
Determines the plural category (`zero`, `one`, `two`, `few`, `many`, `other`) for a number in a given locale. Different languages have different rules. English has two categories (`one` / `other`), Arabic has six.
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
const en = new Intl.PluralRules('en');
|
|
13
|
+
en.select(1); // 'one'
|
|
14
|
+
en.select(0); // 'other'
|
|
15
|
+
en.select(5); // 'other'
|
|
16
|
+
|
|
17
|
+
const sv = new Intl.PluralRules('sv');
|
|
18
|
+
sv.select(1); // 'one' → en
|
|
19
|
+
sv.select(0); // 'other' → flera
|
|
20
|
+
sv.select(5); // 'other' → flera
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The returned strings (`one`, `other`, `few`, etc.) are category identifiers, not localized text. The locale determines *which numbers* map to *which categories*. English and Swedish both use `one`/`other`, while languages like Arabic use all six categories (`zero`, `one`, `two`, `few`, `many`, `other`).
|
|
24
|
+
|
|
25
|
+
This is the engine behind ICU `{count, plural, ...}` messages. The Relaxjs ICU formatter delegates to `Intl.PluralRules` for category selection.
|
|
26
|
+
|
|
27
|
+
[MDN: Intl.PluralRules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules)
|
|
28
|
+
|
|
29
|
+
## Intl.NumberFormat
|
|
30
|
+
|
|
31
|
+
Formats numbers according to locale conventions: thousands separators, decimal marks, currency, percentages, units, and compact notation.
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
new Intl.NumberFormat('en').format(1234.5); // '1,234.5'
|
|
35
|
+
new Intl.NumberFormat('sv').format(1234.5); // '1 234,5'
|
|
36
|
+
|
|
37
|
+
new Intl.NumberFormat('en', {
|
|
38
|
+
style: 'currency', currency: 'USD'
|
|
39
|
+
}).format(42); // '$42.00'
|
|
40
|
+
|
|
41
|
+
new Intl.NumberFormat('sv', {
|
|
42
|
+
style: 'currency', currency: 'SEK'
|
|
43
|
+
}).format(42); // '42,00 kr'
|
|
44
|
+
|
|
45
|
+
new Intl.NumberFormat('en', {
|
|
46
|
+
notation: 'compact'
|
|
47
|
+
}).format(1_500_000); // '1.5M'
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The Relaxjs `currency` pipe uses `Intl.NumberFormat` with `getCurrentLocale()`.
|
|
51
|
+
|
|
52
|
+
[MDN: Intl.NumberFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat)
|
|
53
|
+
|
|
54
|
+
## Intl.DateTimeFormat
|
|
55
|
+
|
|
56
|
+
Formats dates and times according to locale conventions and configurable options.
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
const date = new Date('2025-06-15T14:30:00');
|
|
60
|
+
|
|
61
|
+
new Intl.DateTimeFormat('en').format(date); // '6/15/2025'
|
|
62
|
+
new Intl.DateTimeFormat('sv').format(date); // '2025-06-15'
|
|
63
|
+
|
|
64
|
+
new Intl.DateTimeFormat('en', {
|
|
65
|
+
dateStyle: 'long', timeStyle: 'short'
|
|
66
|
+
}).format(date); // 'June 15, 2025 at 2:30 PM'
|
|
67
|
+
|
|
68
|
+
new Intl.DateTimeFormat('sv', {
|
|
69
|
+
weekday: 'long', month: 'long', day: 'numeric'
|
|
70
|
+
}).format(date); // 'söndag 15 juni'
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The Relaxjs `date` pipe uses `Intl.DateTimeFormat` with `getCurrentLocale()`.
|
|
74
|
+
|
|
75
|
+
[MDN: Intl.DateTimeFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat)
|
|
76
|
+
|
|
77
|
+
## Intl.RelativeTimeFormat
|
|
78
|
+
|
|
79
|
+
Formats relative time descriptions ("3 days ago", "in 2 hours") with locale-aware phrasing.
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
|
|
83
|
+
rtf.format(-1, 'day'); // 'yesterday'
|
|
84
|
+
rtf.format(0, 'day'); // 'today'
|
|
85
|
+
rtf.format(3, 'day'); // 'in 3 days'
|
|
86
|
+
|
|
87
|
+
const svRtf = new Intl.RelativeTimeFormat('sv', { numeric: 'auto' });
|
|
88
|
+
svRtf.format(-1, 'day'); // 'igår'
|
|
89
|
+
svRtf.format(-3, 'day'); // 'för 3 dagar sedan'
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
[MDN: Intl.RelativeTimeFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat)
|
|
93
|
+
|
|
94
|
+
## Intl.Collator
|
|
95
|
+
|
|
96
|
+
Provides locale-sensitive string comparison for sorting. Handles accents, case, and locale-specific sort orders (e.g. Swedish å/ä/ö sort after z).
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
const col = new Intl.Collator('sv');
|
|
100
|
+
['ö', 'a', 'å', 'ä'].sort(col.compare); // ['a', 'å', 'ä', 'ö']
|
|
101
|
+
|
|
102
|
+
const enCol = new Intl.Collator('en', { sensitivity: 'base' });
|
|
103
|
+
enCol.compare('café', 'cafe'); // 0 (equal, ignoring accent)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
[MDN: Intl.Collator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator)
|
|
107
|
+
|
|
108
|
+
## Intl.Segmenter
|
|
109
|
+
|
|
110
|
+
Splits text into meaningful segments (graphemes, words, or sentences) while respecting locale rules. Essential for languages without spaces between words (Chinese, Japanese, Thai).
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
const seg = new Intl.Segmenter('en', { granularity: 'word' });
|
|
114
|
+
const words = [...seg.segment('Hello world!')].filter(s => s.isWordLike);
|
|
115
|
+
// [{segment: 'Hello'}, {segment: 'world'}]
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
[MDN: Intl.Segmenter](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter)
|
|
119
|
+
|
|
120
|
+
## Intl.ListFormat
|
|
121
|
+
|
|
122
|
+
Formats lists with locale-appropriate conjunctions and punctuation.
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
new Intl.ListFormat('en', { type: 'conjunction' })
|
|
126
|
+
.format(['Alice', 'Bob', 'Charlie']); // 'Alice, Bob, and Charlie'
|
|
127
|
+
|
|
128
|
+
new Intl.ListFormat('sv', { type: 'conjunction' })
|
|
129
|
+
.format(['Alice', 'Bob', 'Charlie']); // 'Alice, Bob och Charlie'
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
[MDN: Intl.ListFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat)
|
|
133
|
+
|
|
134
|
+
## Intl.DisplayNames
|
|
135
|
+
|
|
136
|
+
Translates language, region, currency, and script codes into human-readable names.
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
new Intl.DisplayNames('en', { type: 'language' }).of('sv'); // 'Swedish'
|
|
140
|
+
new Intl.DisplayNames('sv', { type: 'language' }).of('en'); // 'engelska'
|
|
141
|
+
new Intl.DisplayNames('en', { type: 'currency' }).of('SEK'); // 'Swedish Krona'
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
[MDN: Intl.DisplayNames](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames)
|
|
145
|
+
|
|
146
|
+
## ICU Message Format
|
|
147
|
+
|
|
148
|
+
ICU (International Components for Unicode) defines a message syntax used across platforms. The three core patterns:
|
|
149
|
+
|
|
150
|
+
### Simple Interpolation
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
Hello, {name}!
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Plural
|
|
157
|
+
|
|
158
|
+
Selects a branch based on `Intl.PluralRules`. The `#` token inserts the numeric value. Exact matches (`=0`, `=1`) take priority over category matches.
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
{count, plural, =0 {No items} one {# item} other {# items}}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Select
|
|
165
|
+
|
|
166
|
+
Selects a branch based on an exact string match. Always include an `other` fallback.
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
{gender, select, male {He} female {She} other {They}} liked your post.
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
[ICU User Guide: Formatting Messages](https://unicode-org.github.io/icu/userguide/format_parse/messages/)
|
|
173
|
+
|
|
174
|
+
## Further Reading
|
|
175
|
+
|
|
176
|
+
- [MDN: Intl](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl), full API reference
|
|
177
|
+
- [ICU Message Format](https://unicode-org.github.io/icu/userguide/format_parse/messages/), the message syntax standard
|
|
178
|
+
- [Can I Use: Intl](https://caniuse.com/?search=Intl), browser support tables
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# RouteLink
|
|
2
|
+
|
|
3
|
+
A clickable element that triggers client-side navigation to a named route.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
### Basic Navigation
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<r-link name="home">Home</r-link>
|
|
11
|
+
<r-link name="about">About Us</r-link>
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
### With Route Parameters
|
|
15
|
+
|
|
16
|
+
```html
|
|
17
|
+
<r-link name="user" param-id="123">View User</r-link>
|
|
18
|
+
<r-link name="product" param-id="456" param-tab="details">Product Details</r-link>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### With Target
|
|
22
|
+
|
|
23
|
+
```html
|
|
24
|
+
<r-link name="settings" target="sidebar">Settings</r-link>
|
|
25
|
+
<r-link name="preview" target="modal">Preview</r-link>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Complex Parameters via JSON
|
|
29
|
+
|
|
30
|
+
```html
|
|
31
|
+
<r-link name="search" params='{"query":"typescript","page":1}'>
|
|
32
|
+
Search Results
|
|
33
|
+
</r-link>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Attributes
|
|
37
|
+
|
|
38
|
+
| Attribute | Type | Description |
|
|
39
|
+
|-----------|------|-------------|
|
|
40
|
+
| `name` | `string` | Route name to navigate to (required) |
|
|
41
|
+
| `target` | `string` | Target `r-route-target` name |
|
|
42
|
+
| `params` | `string` | JSON object with additional route data |
|
|
43
|
+
| `param-*` | `string` | Individual route parameters (e.g., `param-id="123"`) |
|
|
44
|
+
|
|
45
|
+
## Behavior
|
|
46
|
+
|
|
47
|
+
1. Renders as an inline element with `cursor: pointer`
|
|
48
|
+
2. Sets `role="link"` and `tabindex="0"` for accessibility
|
|
49
|
+
3. On click, calls `navigate()` with the route name and collected parameters
|
|
50
|
+
4. Prevents default click behavior
|
|
51
|
+
|
|
52
|
+
## Combining Parameters
|
|
53
|
+
|
|
54
|
+
Both `param-*` attributes and `params` JSON can be used together:
|
|
55
|
+
|
|
56
|
+
```html
|
|
57
|
+
<r-link name="product"
|
|
58
|
+
param-id="789"
|
|
59
|
+
params='{"source":"email","campaign":"summer"}'>
|
|
60
|
+
View Product
|
|
61
|
+
</r-link>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
All parameters are merged into a single flat object:
|
|
65
|
+
```typescript
|
|
66
|
+
{ id: '789', source: 'email', campaign: 'summer' }
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Styling
|
|
70
|
+
|
|
71
|
+
Style as any inline element:
|
|
72
|
+
|
|
73
|
+
```css
|
|
74
|
+
r-link {
|
|
75
|
+
color: var(--accent-primary);
|
|
76
|
+
text-decoration: underline;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
r-link:hover {
|
|
80
|
+
color: var(--accent-hover);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
r-link:focus {
|
|
84
|
+
outline: 2px solid var(--accent-primary);
|
|
85
|
+
outline-offset: 2px;
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Comparison with Native Links
|
|
90
|
+
|
|
91
|
+
| Feature | `<a href>` | `<r-link>` |
|
|
92
|
+
|---------|------------|---------|
|
|
93
|
+
| Navigation | Full page reload | Client-side routing |
|
|
94
|
+
| Parameters | Query string | Route params + data |
|
|
95
|
+
| Target | Window/frame | Route target |
|
|
96
|
+
| SEO | Crawlable | Requires SSR |
|
|
97
|
+
|
|
98
|
+
Use `<r-link>` for SPA navigation within the app. Use native `<a>` for external links or when SEO is critical.
|