@flightdev/i18n 0.1.5
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 +644 -0
- package/dist/adapters/formatjs.d.ts +61 -0
- package/dist/adapters/formatjs.js +81 -0
- package/dist/adapters/i18next.d.ts +15 -0
- package/dist/adapters/i18next.js +51 -0
- package/dist/adapters/lingui.d.ts +82 -0
- package/dist/adapters/lingui.js +73 -0
- package/dist/adapters/paraglide.d.ts +65 -0
- package/dist/adapters/paraglide.js +43 -0
- package/dist/chunk-F5CV7JNA.js +76 -0
- package/dist/chunk-O3FQ7FPU.js +150 -0
- package/dist/index.d.ts +165 -0
- package/dist/index.js +18 -0
- package/dist/middleware.d.ts +107 -0
- package/dist/middleware.js +127 -0
- package/dist/routing.d.ts +179 -0
- package/dist/routing.js +22 -0
- package/dist/typegen.d.ts +127 -0
- package/dist/typegen.js +214 -0
- package/package.json +72 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-2026 Flight Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
# @flight-framework/i18n
|
|
2
|
+
|
|
3
|
+
Internationalization for Flight Framework. Supports multiple translation libraries with a unified API, automatic locale detection, and SSR-friendly design.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Features](#features)
|
|
8
|
+
- [Installation](#installation)
|
|
9
|
+
- [Quick Start](#quick-start)
|
|
10
|
+
- [Adapters](#adapters)
|
|
11
|
+
- [Locale Detection](#locale-detection)
|
|
12
|
+
- [UI Framework Integration](#ui-framework-integration)
|
|
13
|
+
- [Formatting Utilities](#formatting-utilities)
|
|
14
|
+
- [Pluralization](#pluralization)
|
|
15
|
+
- [Interpolation](#interpolation)
|
|
16
|
+
- [Namespace Support](#namespace-support)
|
|
17
|
+
- [Loading Translations](#loading-translations)
|
|
18
|
+
- [SSR and Hydration](#ssr-and-hydration)
|
|
19
|
+
- [API Reference](#api-reference)
|
|
20
|
+
- [Creating Custom Adapters](#creating-custom-adapters)
|
|
21
|
+
- [License](#license)
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Features
|
|
26
|
+
|
|
27
|
+
- Unified API across i18next, Paraglide, FormatJS, and Lingui
|
|
28
|
+
- Automatic locale detection (URL, cookie, header, browser)
|
|
29
|
+
- Type-safe translations with TypeScript
|
|
30
|
+
- SSR support with hydration
|
|
31
|
+
- Lazy loading of translation bundles
|
|
32
|
+
- Built-in formatting for numbers, dates, and relative time
|
|
33
|
+
- Pluralization and gender support
|
|
34
|
+
- React, Vue, Svelte, and Solid integrations
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm install @flight-framework/i18n
|
|
42
|
+
|
|
43
|
+
# Install your preferred adapter:
|
|
44
|
+
npm install i18next # Most popular, feature-rich
|
|
45
|
+
npm install @inlang/paraglide-js # Compile-time, smallest bundle
|
|
46
|
+
npm install @formatjs/intl # ICU message format
|
|
47
|
+
npm install @lingui/core # Compile-time extraction
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Quick Start
|
|
53
|
+
|
|
54
|
+
### 1. Create the i18n Instance
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
// src/i18n.ts
|
|
58
|
+
import { createI18n } from '@flight-framework/i18n';
|
|
59
|
+
import { i18next } from '@flight-framework/i18n/i18next';
|
|
60
|
+
|
|
61
|
+
export const i18n = createI18n(i18next({
|
|
62
|
+
locales: ['en', 'es', 'fr', 'de'],
|
|
63
|
+
defaultLocale: 'en',
|
|
64
|
+
fallbackLocale: 'en',
|
|
65
|
+
}));
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 2. Add Translation Files
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
src/
|
|
72
|
+
locales/
|
|
73
|
+
en/
|
|
74
|
+
common.json
|
|
75
|
+
errors.json
|
|
76
|
+
es/
|
|
77
|
+
common.json
|
|
78
|
+
errors.json
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
// src/locales/en/common.json
|
|
83
|
+
{
|
|
84
|
+
"welcome": "Welcome to our app",
|
|
85
|
+
"greeting": "Hello, {{name}}!",
|
|
86
|
+
"items": "You have {{count}} item",
|
|
87
|
+
"items_plural": "You have {{count}} items"
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### 3. Initialize and Use
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import { i18n } from './i18n';
|
|
95
|
+
|
|
96
|
+
await i18n.init();
|
|
97
|
+
|
|
98
|
+
// Basic translation
|
|
99
|
+
i18n.t('welcome'); // "Welcome to our app"
|
|
100
|
+
|
|
101
|
+
// With interpolation
|
|
102
|
+
i18n.t('greeting', { name: 'Maria' }); // "Hello, Maria!"
|
|
103
|
+
|
|
104
|
+
// With pluralization
|
|
105
|
+
i18n.t('items', { count: 1 }); // "You have 1 item"
|
|
106
|
+
i18n.t('items', { count: 5 }); // "You have 5 items"
|
|
107
|
+
|
|
108
|
+
// Change locale
|
|
109
|
+
await i18n.setLocale('es');
|
|
110
|
+
i18n.t('welcome'); // "Bienvenido a nuestra app"
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Adapters
|
|
116
|
+
|
|
117
|
+
### i18next
|
|
118
|
+
|
|
119
|
+
The most popular option with extensive features.
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
import { i18next } from '@flight-framework/i18n/i18next';
|
|
123
|
+
|
|
124
|
+
const adapter = i18next({
|
|
125
|
+
locales: ['en', 'es', 'fr'],
|
|
126
|
+
defaultLocale: 'en',
|
|
127
|
+
fallbackLocale: 'en',
|
|
128
|
+
|
|
129
|
+
// i18next-specific options
|
|
130
|
+
detection: {
|
|
131
|
+
order: ['cookie', 'header', 'navigator'],
|
|
132
|
+
caches: ['cookie'],
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
backend: {
|
|
136
|
+
loadPath: '/locales/{{lng}}/{{ns}}.json',
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Paraglide
|
|
142
|
+
|
|
143
|
+
Compile-time translations for the smallest bundle size.
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
import { paraglide } from '@flight-framework/i18n/paraglide';
|
|
147
|
+
import * as messages from './paraglide/messages';
|
|
148
|
+
|
|
149
|
+
const adapter = paraglide({
|
|
150
|
+
messages,
|
|
151
|
+
locales: ['en', 'es'],
|
|
152
|
+
defaultLocale: 'en',
|
|
153
|
+
});
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### FormatJS (react-intl)
|
|
157
|
+
|
|
158
|
+
ICU message format with powerful formatting.
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
import { formatjs } from '@flight-framework/i18n/formatjs';
|
|
162
|
+
|
|
163
|
+
const adapter = formatjs({
|
|
164
|
+
locales: ['en', 'es'],
|
|
165
|
+
defaultLocale: 'en',
|
|
166
|
+
messages: {
|
|
167
|
+
en: { greeting: 'Hello, {name}!' },
|
|
168
|
+
es: { greeting: 'Hola, {name}!' },
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Lingui
|
|
174
|
+
|
|
175
|
+
Compile-time extraction with excellent developer experience.
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
import { lingui } from '@flight-framework/i18n/lingui';
|
|
179
|
+
import { messages as enMessages } from './locales/en/messages';
|
|
180
|
+
import { messages as esMessages } from './locales/es/messages';
|
|
181
|
+
|
|
182
|
+
const adapter = lingui({
|
|
183
|
+
locales: ['en', 'es'],
|
|
184
|
+
defaultLocale: 'en',
|
|
185
|
+
messages: {
|
|
186
|
+
en: enMessages,
|
|
187
|
+
es: esMessages,
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Locale Detection
|
|
195
|
+
|
|
196
|
+
Automatic detection from multiple sources:
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
import { createI18n, detectLocale } from '@flight-framework/i18n';
|
|
200
|
+
|
|
201
|
+
const i18n = createI18n(adapter, {
|
|
202
|
+
detection: {
|
|
203
|
+
// Detection order (first match wins)
|
|
204
|
+
order: ['path', 'cookie', 'header', 'navigator'],
|
|
205
|
+
|
|
206
|
+
// URL path detection: /en/about -> locale: 'en'
|
|
207
|
+
pathIndex: 0,
|
|
208
|
+
|
|
209
|
+
// Cookie name for persistence
|
|
210
|
+
cookieName: 'locale',
|
|
211
|
+
|
|
212
|
+
// Header to check (Accept-Language)
|
|
213
|
+
headerName: 'accept-language',
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Server-Side Detection
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
import { getLocaleFromRequest } from '@flight-framework/i18n';
|
|
222
|
+
|
|
223
|
+
export async function loader({ request }) {
|
|
224
|
+
const locale = getLocaleFromRequest(request, {
|
|
225
|
+
supported: ['en', 'es', 'fr'],
|
|
226
|
+
fallback: 'en',
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
return { locale };
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### URL-Based Routing
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
// Routes: /en/about, /es/about, /fr/about
|
|
237
|
+
import { createLocaleRouter } from '@flight-framework/i18n';
|
|
238
|
+
|
|
239
|
+
const router = createLocaleRouter({
|
|
240
|
+
locales: ['en', 'es', 'fr'],
|
|
241
|
+
defaultLocale: 'en',
|
|
242
|
+
strategy: 'prefix', // 'prefix' | 'prefix-except-default' | 'domain'
|
|
243
|
+
});
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## UI Framework Integration
|
|
249
|
+
|
|
250
|
+
### React
|
|
251
|
+
|
|
252
|
+
```tsx
|
|
253
|
+
import { I18nProvider, useTranslation, Trans } from '@flight-framework/i18n/react';
|
|
254
|
+
|
|
255
|
+
function App() {
|
|
256
|
+
return (
|
|
257
|
+
<I18nProvider i18n={i18n}>
|
|
258
|
+
<Content />
|
|
259
|
+
</I18nProvider>
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function Content() {
|
|
264
|
+
const { t, locale, setLocale, locales } = useTranslation();
|
|
265
|
+
|
|
266
|
+
return (
|
|
267
|
+
<div>
|
|
268
|
+
<h1>{t('welcome')}</h1>
|
|
269
|
+
<p>{t('greeting', { name: 'World' })}</p>
|
|
270
|
+
|
|
271
|
+
{/* Rich text with components */}
|
|
272
|
+
<Trans
|
|
273
|
+
i18nKey="terms"
|
|
274
|
+
components={{
|
|
275
|
+
link: <a href="/terms" />,
|
|
276
|
+
bold: <strong />,
|
|
277
|
+
}}
|
|
278
|
+
/>
|
|
279
|
+
|
|
280
|
+
{/* Locale switcher */}
|
|
281
|
+
<select value={locale} onChange={e => setLocale(e.target.value)}>
|
|
282
|
+
{locales.map(loc => (
|
|
283
|
+
<option key={loc} value={loc}>{loc}</option>
|
|
284
|
+
))}
|
|
285
|
+
</select>
|
|
286
|
+
</div>
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Vue
|
|
292
|
+
|
|
293
|
+
```vue
|
|
294
|
+
<script setup>
|
|
295
|
+
import { useI18n } from '@flight-framework/i18n/vue';
|
|
296
|
+
|
|
297
|
+
const { t, locale, setLocale, locales } = useI18n();
|
|
298
|
+
</script>
|
|
299
|
+
|
|
300
|
+
<template>
|
|
301
|
+
<h1>{{ t('welcome') }}</h1>
|
|
302
|
+
<p>{{ t('greeting', { name: 'World' }) }}</p>
|
|
303
|
+
|
|
304
|
+
<select v-model="locale" @change="setLocale(locale)">
|
|
305
|
+
<option v-for="loc in locales" :key="loc" :value="loc">
|
|
306
|
+
{{ loc }}
|
|
307
|
+
</option>
|
|
308
|
+
</select>
|
|
309
|
+
</template>
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Svelte
|
|
313
|
+
|
|
314
|
+
```svelte
|
|
315
|
+
<script>
|
|
316
|
+
import { getI18n } from '@flight-framework/i18n/svelte';
|
|
317
|
+
|
|
318
|
+
const { t, locale, setLocale, locales } = getI18n();
|
|
319
|
+
</script>
|
|
320
|
+
|
|
321
|
+
<h1>{$t('welcome')}</h1>
|
|
322
|
+
<p>{$t('greeting', { name: 'World' })}</p>
|
|
323
|
+
|
|
324
|
+
<select bind:value={$locale} on:change={() => setLocale($locale)}>
|
|
325
|
+
{#each $locales as loc}
|
|
326
|
+
<option value={loc}>{loc}</option>
|
|
327
|
+
{/each}
|
|
328
|
+
</select>
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Solid
|
|
332
|
+
|
|
333
|
+
```tsx
|
|
334
|
+
import { useI18n, Trans } from '@flight-framework/i18n/solid';
|
|
335
|
+
|
|
336
|
+
function Content() {
|
|
337
|
+
const { t, locale, setLocale, locales } = useI18n();
|
|
338
|
+
|
|
339
|
+
return (
|
|
340
|
+
<>
|
|
341
|
+
<h1>{t('welcome')}</h1>
|
|
342
|
+
<p>{t('greeting', { name: 'World' })}</p>
|
|
343
|
+
|
|
344
|
+
<select value={locale()} onChange={e => setLocale(e.target.value)}>
|
|
345
|
+
<For each={locales()}>
|
|
346
|
+
{loc => <option value={loc}>{loc}</option>}
|
|
347
|
+
</For>
|
|
348
|
+
</select>
|
|
349
|
+
</>
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
## Formatting Utilities
|
|
357
|
+
|
|
358
|
+
Built-in formatters using the Intl API:
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
import {
|
|
362
|
+
formatNumber,
|
|
363
|
+
formatCurrency,
|
|
364
|
+
formatDate,
|
|
365
|
+
formatTime,
|
|
366
|
+
formatRelativeTime,
|
|
367
|
+
formatList,
|
|
368
|
+
} from '@flight-framework/i18n';
|
|
369
|
+
|
|
370
|
+
// Numbers
|
|
371
|
+
formatNumber(1234567.89, 'en-US'); // "1,234,567.89"
|
|
372
|
+
formatNumber(1234567.89, 'de-DE'); // "1.234.567,89"
|
|
373
|
+
formatNumber(0.75, 'en-US', { style: 'percent' }); // "75%"
|
|
374
|
+
|
|
375
|
+
// Currency
|
|
376
|
+
formatCurrency(99.99, 'USD', 'en-US'); // "$99.99"
|
|
377
|
+
formatCurrency(99.99, 'EUR', 'de-DE'); // "99,99 €"
|
|
378
|
+
formatCurrency(99.99, 'JPY', 'ja-JP'); // "¥100"
|
|
379
|
+
|
|
380
|
+
// Dates
|
|
381
|
+
formatDate(new Date(), 'en-US'); // "1/15/2026"
|
|
382
|
+
formatDate(new Date(), 'de-DE'); // "15.1.2026"
|
|
383
|
+
formatDate(new Date(), 'en-US', { dateStyle: 'full' });
|
|
384
|
+
// "Thursday, January 15, 2026"
|
|
385
|
+
|
|
386
|
+
// Time
|
|
387
|
+
formatTime(new Date(), 'en-US'); // "3:45 PM"
|
|
388
|
+
formatTime(new Date(), 'de-DE'); // "15:45"
|
|
389
|
+
|
|
390
|
+
// Relative time
|
|
391
|
+
formatRelativeTime(Date.now() - 3600000, 'en-US'); // "1 hour ago"
|
|
392
|
+
formatRelativeTime(Date.now() + 86400000, 'en-US'); // "in 1 day"
|
|
393
|
+
|
|
394
|
+
// Lists
|
|
395
|
+
formatList(['Apple', 'Banana', 'Cherry'], 'en-US');
|
|
396
|
+
// "Apple, Banana, and Cherry"
|
|
397
|
+
formatList(['Apple', 'Banana', 'Cherry'], 'es-ES');
|
|
398
|
+
// "Apple, Banana y Cherry"
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
---
|
|
402
|
+
|
|
403
|
+
## Pluralization
|
|
404
|
+
|
|
405
|
+
Automatic plural form selection based on count:
|
|
406
|
+
|
|
407
|
+
```json
|
|
408
|
+
// English (2 forms: one, other)
|
|
409
|
+
{
|
|
410
|
+
"apple": "{{count}} apple",
|
|
411
|
+
"apple_plural": "{{count}} apples"
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Russian (3 forms: one, few, many)
|
|
415
|
+
{
|
|
416
|
+
"apple_one": "{{count}} яблоко",
|
|
417
|
+
"apple_few": "{{count}} яблока",
|
|
418
|
+
"apple_many": "{{count}} яблок"
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Arabic (6 forms)
|
|
422
|
+
{
|
|
423
|
+
"apple_zero": "...",
|
|
424
|
+
"apple_one": "...",
|
|
425
|
+
"apple_two": "...",
|
|
426
|
+
"apple_few": "...",
|
|
427
|
+
"apple_many": "...",
|
|
428
|
+
"apple_other": "..."
|
|
429
|
+
}
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
```typescript
|
|
433
|
+
t('apple', { count: 0 }); // "0 apples"
|
|
434
|
+
t('apple', { count: 1 }); // "1 apple"
|
|
435
|
+
t('apple', { count: 5 }); // "5 apples"
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
---
|
|
439
|
+
|
|
440
|
+
## Interpolation
|
|
441
|
+
|
|
442
|
+
Insert dynamic values into translations:
|
|
443
|
+
|
|
444
|
+
```json
|
|
445
|
+
{
|
|
446
|
+
"greeting": "Hello, {{name}}!",
|
|
447
|
+
"order": "Order #{{orderId}} placed on {{date, datetime}}",
|
|
448
|
+
"price": "Total: {{amount, currency(USD)}}"
|
|
449
|
+
}
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
```typescript
|
|
453
|
+
t('greeting', { name: 'John' });
|
|
454
|
+
// "Hello, John!"
|
|
455
|
+
|
|
456
|
+
t('order', { orderId: '12345', date: new Date() });
|
|
457
|
+
// "Order #12345 placed on Jan 15, 2026"
|
|
458
|
+
|
|
459
|
+
t('price', { amount: 49.99 });
|
|
460
|
+
// "Total: $49.99"
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
---
|
|
464
|
+
|
|
465
|
+
## Namespace Support
|
|
466
|
+
|
|
467
|
+
Organize translations by feature:
|
|
468
|
+
|
|
469
|
+
```
|
|
470
|
+
locales/
|
|
471
|
+
en/
|
|
472
|
+
common.json # Shared translations
|
|
473
|
+
auth.json # Login, signup, etc.
|
|
474
|
+
dashboard.json # Dashboard-specific
|
|
475
|
+
errors.json # Error messages
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
```typescript
|
|
479
|
+
const i18n = createI18n(adapter, {
|
|
480
|
+
namespaces: ['common', 'auth', 'dashboard', 'errors'],
|
|
481
|
+
defaultNamespace: 'common',
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
// Use namespace prefix
|
|
485
|
+
t('auth:login'); // From auth.json
|
|
486
|
+
t('dashboard:welcome'); // From dashboard.json
|
|
487
|
+
t('errors:not_found'); // From errors.json
|
|
488
|
+
t('greeting'); // From common.json (default)
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
---
|
|
492
|
+
|
|
493
|
+
## Loading Translations
|
|
494
|
+
|
|
495
|
+
### Static (Build-time)
|
|
496
|
+
|
|
497
|
+
Include all translations in the bundle:
|
|
498
|
+
|
|
499
|
+
```typescript
|
|
500
|
+
import en from './locales/en/common.json';
|
|
501
|
+
import es from './locales/es/common.json';
|
|
502
|
+
|
|
503
|
+
const i18n = createI18n(adapter, {
|
|
504
|
+
resources: { en, es },
|
|
505
|
+
});
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
### Dynamic (Runtime)
|
|
509
|
+
|
|
510
|
+
Load translations on demand:
|
|
511
|
+
|
|
512
|
+
```typescript
|
|
513
|
+
const i18n = createI18n(adapter, {
|
|
514
|
+
loadPath: '/locales/{{locale}}/{{namespace}}.json',
|
|
515
|
+
|
|
516
|
+
// Preload specific locales
|
|
517
|
+
preload: ['en'],
|
|
518
|
+
|
|
519
|
+
// Load namespace on demand
|
|
520
|
+
partialBundledLanguages: true,
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
// Load additional namespace
|
|
524
|
+
await i18n.loadNamespace('dashboard');
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
---
|
|
528
|
+
|
|
529
|
+
## SSR and Hydration
|
|
530
|
+
|
|
531
|
+
Avoid content flash by synchronizing server and client:
|
|
532
|
+
|
|
533
|
+
```typescript
|
|
534
|
+
// Server
|
|
535
|
+
export async function loader({ request }) {
|
|
536
|
+
const locale = getLocaleFromRequest(request);
|
|
537
|
+
await i18n.setLocale(locale);
|
|
538
|
+
|
|
539
|
+
return {
|
|
540
|
+
locale,
|
|
541
|
+
translations: i18n.getResourceBundle(locale, 'common'),
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Client hydration
|
|
546
|
+
const { locale, translations } = useLoaderData();
|
|
547
|
+
|
|
548
|
+
i18n.addResourceBundle(locale, 'common', translations);
|
|
549
|
+
await i18n.setLocale(locale);
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
### With React
|
|
553
|
+
|
|
554
|
+
```tsx
|
|
555
|
+
// entry-server.tsx
|
|
556
|
+
const html = renderToString(
|
|
557
|
+
<I18nProvider i18n={i18n} locale={locale}>
|
|
558
|
+
<App />
|
|
559
|
+
</I18nProvider>
|
|
560
|
+
);
|
|
561
|
+
|
|
562
|
+
// entry-client.tsx
|
|
563
|
+
hydrateRoot(
|
|
564
|
+
document,
|
|
565
|
+
<I18nProvider i18n={i18n} locale={window.__LOCALE__}>
|
|
566
|
+
<App />
|
|
567
|
+
</I18nProvider>
|
|
568
|
+
);
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
---
|
|
572
|
+
|
|
573
|
+
## API Reference
|
|
574
|
+
|
|
575
|
+
### createI18n Options
|
|
576
|
+
|
|
577
|
+
| Option | Type | Default | Description |
|
|
578
|
+
|--------|------|---------|-------------|
|
|
579
|
+
| `locales` | `string[]` | required | Supported locales |
|
|
580
|
+
| `defaultLocale` | `string` | required | Fallback locale |
|
|
581
|
+
| `fallbackLocale` | `string` | defaultLocale | Missing key fallback |
|
|
582
|
+
| `namespaces` | `string[]` | `['translation']` | Translation namespaces |
|
|
583
|
+
| `defaultNamespace` | `string` | `'translation'` | Default namespace |
|
|
584
|
+
| `loadPath` | `string` | - | Path pattern for loading |
|
|
585
|
+
| `detection` | `object` | - | Locale detection config |
|
|
586
|
+
|
|
587
|
+
### i18n Instance Methods
|
|
588
|
+
|
|
589
|
+
| Method | Description |
|
|
590
|
+
|--------|-------------|
|
|
591
|
+
| `init()` | Initialize the instance |
|
|
592
|
+
| `t(key, options?)` | Translate a key |
|
|
593
|
+
| `setLocale(locale)` | Change current locale |
|
|
594
|
+
| `getLocale()` | Get current locale |
|
|
595
|
+
| `hasLocale(locale)` | Check if locale exists |
|
|
596
|
+
| `loadNamespace(ns)` | Load a namespace |
|
|
597
|
+
| `addResourceBundle(locale, ns, resources)` | Add translations |
|
|
598
|
+
| `getResourceBundle(locale, ns)` | Get translations |
|
|
599
|
+
|
|
600
|
+
---
|
|
601
|
+
|
|
602
|
+
## Creating Custom Adapters
|
|
603
|
+
|
|
604
|
+
Implement the `I18nAdapter` interface:
|
|
605
|
+
|
|
606
|
+
```typescript
|
|
607
|
+
import type { I18nAdapter } from '@flight-framework/i18n';
|
|
608
|
+
|
|
609
|
+
export function myAdapter(config: MyConfig): I18nAdapter {
|
|
610
|
+
return {
|
|
611
|
+
name: 'my-adapter',
|
|
612
|
+
|
|
613
|
+
async init() {
|
|
614
|
+
// Initialize your library
|
|
615
|
+
},
|
|
616
|
+
|
|
617
|
+
t(key, options) {
|
|
618
|
+
// Return translated string
|
|
619
|
+
},
|
|
620
|
+
|
|
621
|
+
async setLocale(locale) {
|
|
622
|
+
// Change locale
|
|
623
|
+
},
|
|
624
|
+
|
|
625
|
+
getLocale() {
|
|
626
|
+
// Return current locale
|
|
627
|
+
},
|
|
628
|
+
|
|
629
|
+
hasKey(key) {
|
|
630
|
+
// Check if key exists
|
|
631
|
+
},
|
|
632
|
+
|
|
633
|
+
async loadNamespace(namespace) {
|
|
634
|
+
// Load namespace translations
|
|
635
|
+
},
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
---
|
|
641
|
+
|
|
642
|
+
## License
|
|
643
|
+
|
|
644
|
+
MIT
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Locale, I18nAdapter } from '../index.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* FormatJS Adapter for @flightdev/i18n
|
|
5
|
+
*
|
|
6
|
+
* FormatJS (react-intl) provides:
|
|
7
|
+
* - ICU MessageFormat syntax for complex translations
|
|
8
|
+
* - Rich date/time/number formatting with locale awareness
|
|
9
|
+
* - Pluralization and select rules
|
|
10
|
+
* - Used by Facebook, Yahoo, and many Fortune 500 companies
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* import { createI18n } from '@flightdev/i18n';
|
|
15
|
+
* import { formatjs } from '@flightdev/i18n/formatjs';
|
|
16
|
+
*
|
|
17
|
+
* const i18n = createI18n(formatjs({
|
|
18
|
+
* locales: ['en', 'es', 'fr'],
|
|
19
|
+
* defaultLocale: 'en',
|
|
20
|
+
* messages: {
|
|
21
|
+
* en: { greeting: 'Hello, {name}!', items: '{count, plural, one {# item} other {# items}}' },
|
|
22
|
+
* es: { greeting: '¡Hola, {name}!', items: '{count, plural, one {# artículo} other {# artículos}}' },
|
|
23
|
+
* },
|
|
24
|
+
* }));
|
|
25
|
+
*
|
|
26
|
+
* await i18n.init();
|
|
27
|
+
* console.log(i18n.t('greeting', { name: 'World' })); // "Hello, World!"
|
|
28
|
+
* console.log(i18n.t('items', { count: 5 })); // "5 items"
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* @see https://formatjs.io/
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
interface FormatJSConfig {
|
|
35
|
+
/** Supported locales */
|
|
36
|
+
locales: Locale[];
|
|
37
|
+
/** Default locale */
|
|
38
|
+
defaultLocale: Locale;
|
|
39
|
+
/** Messages for each locale */
|
|
40
|
+
messages: Record<Locale, Record<string, string>>;
|
|
41
|
+
/** Default timezone for date formatting */
|
|
42
|
+
timeZone?: string;
|
|
43
|
+
/** Error handler for formatting errors */
|
|
44
|
+
onError?: (err: Error) => void;
|
|
45
|
+
/** Warning handler */
|
|
46
|
+
onWarn?: (warning: string) => void;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Creates a FormatJS adapter for Flight's i18n service.
|
|
50
|
+
*
|
|
51
|
+
* FormatJS is ideal when you need:
|
|
52
|
+
* 1. ICU MessageFormat syntax (plurals, selects, etc.)
|
|
53
|
+
* 2. Rich formatting for dates, times, numbers
|
|
54
|
+
* 3. Compatibility with react-intl ecosystem
|
|
55
|
+
*
|
|
56
|
+
* @param config - FormatJS configuration
|
|
57
|
+
* @returns I18nAdapter instance
|
|
58
|
+
*/
|
|
59
|
+
declare function formatjs(config: FormatJSConfig): I18nAdapter;
|
|
60
|
+
|
|
61
|
+
export { type FormatJSConfig, formatjs as default, formatjs };
|