@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 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 };