i18n-typed-store 0.1.0 → 0.1.2

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 CHANGED
@@ -1,15 +1,21 @@
1
1
  # i18n-typed-store
2
2
 
3
- Type-safe translation store for managing i18n locales with full TypeScript support. A lightweight, zero-dependency library for handling internationalization with compile-time type safety.
3
+ Type-safe translation store for managing i18n locales with full TypeScript support. A lightweight, zero-dependency library for handling internationalization with compile-time type safety. Designed to work with TypeScript classes for translations, providing full IDE support (go-to definition, autocomplete) and support for JSX elements and methods in translations.
4
4
 
5
5
  ## Features
6
6
 
7
7
  - ✅ **Full TypeScript support** - Complete type safety for translations and locales
8
+ - ✅ **IDE integration** - Go-to definition, autocomplete, and refactoring support with translation classes
8
9
  - ✅ **Lazy loading** - Load translations only when needed
9
10
  - ✅ **Type-safe API** - Compile-time validation of translation keys and locales
11
+ - ✅ **Translation classes/objects** - Use TypeScript classes or objects for translations with JSX support and methods
10
12
  - ✅ **Pluralization support** - Built-in plural form selector using `Intl.PluralRules`
11
13
  - ✅ **Flexible module loading** - Support for any module format (ESM, CommonJS, dynamic imports)
12
14
  - ✅ **Zero runtime dependencies** - Lightweight and framework-agnostic
15
+ - ✅ **React integration** - Hooks and components for React applications
16
+ - ✅ **SSR/SSG support** - Built-in utilities for server-side rendering
17
+ - ✅ **Fallback locales** - Automatic merging with fallback translations
18
+ - ✅ **Caching** - Built-in translation caching for better performance
13
19
 
14
20
  ## Installation
15
21
 
@@ -25,6 +31,17 @@ yarn add i18n-typed-store
25
31
  pnpm add i18n-typed-store
26
32
  ```
27
33
 
34
+ ## Example Project
35
+
36
+ For a complete working example, check out the [React example project](https://github.com/ialexanderlvov/i18n-typed-store-react-example):
37
+
38
+ ```bash
39
+ git clone https://github.com/ialexanderlvov/i18n-typed-store-react-example
40
+ cd i18n-typed-store-react-example
41
+ yarn
42
+ yarn dev
43
+ ```
44
+
28
45
  ## Quick Start
29
46
 
30
47
  ### Basic Usage
@@ -32,6 +49,10 @@ pnpm add i18n-typed-store
32
49
  ```typescript
33
50
  import { createTranslationStore } from 'i18n-typed-store';
34
51
 
52
+ // Import translation types for type safety
53
+ import type CommonTranslationsEn from './translations/common/en';
54
+ import type ErrorsTranslationsEn from './translations/errors/en';
55
+
35
56
  // Define your translation keys
36
57
  const translations = {
37
58
  common: 'common',
@@ -44,35 +65,267 @@ const locales = {
44
65
  ru: 'ru',
45
66
  } as const;
46
67
 
47
- // Define your translation data structure
48
- type TranslationData = {
49
- common: {
50
- title: string;
51
- description: string;
52
- };
53
- errors: {
54
- notFound: string;
55
- };
56
- };
68
+ // Define your translation data structure using imported types
69
+ interface TranslationData extends Record<keyof typeof translations, any> {
70
+ common: CommonTranslationsEn;
71
+ errors: ErrorsTranslationsEn;
72
+ }
57
73
 
58
- // Create the store
59
- const storeFactory = createTranslationStore(
74
+ // Create the store factory
75
+ const storeFactory = createTranslationStore({
60
76
  translations,
61
77
  locales,
62
- async (locale, translation) => {
63
- // Load translation module dynamically
64
- const module = await import(`./locales/${locale}/${translation}.json`);
65
- return module.default;
78
+ loadModule: async (locale, translation) => {
79
+ // Load translation class dynamically
80
+ return await import(`./translations/${translation}/${locale}.ts`);
66
81
  },
67
- (module, locale, translation) => module // Extract translation data
68
- );
82
+ extractTranslation: (module) => {
83
+ // Instantiate the translation class
84
+ return new module.default();
85
+ },
86
+ defaultLocale: 'en',
87
+ useFallback: true,
88
+ fallbackLocale: 'en',
89
+ });
69
90
 
70
91
  // Create typed store
71
92
  const store = storeFactory.type<TranslationData>();
72
93
 
73
94
  // Load and use translations
74
- await store.common.load('en');
75
- console.log(store.common.translation?.title); // Type-safe access
95
+ await store.translations.common.load('en');
96
+ const title = store.translations.common.currentTranslation?.title; // Type-safe access with IDE go-to support
97
+ ```
98
+
99
+ ### React Usage
100
+
101
+ ```typescript
102
+ // constants.ts
103
+ export const TRANSLATIONS = {
104
+ common: 'common',
105
+ } as const;
106
+
107
+ export const LOCALES = {
108
+ en: 'en',
109
+ ru: 'ru',
110
+ } as const;
111
+ ```
112
+
113
+ ```typescript
114
+ // store.ts
115
+ import { createTranslationStore } from 'i18n-typed-store';
116
+ import type CommonTranslationsEn from './translations/common/en';
117
+ import { TRANSLATIONS, LOCALES } from './constants';
118
+
119
+ export interface ITranslationStoreTypes extends Record<keyof typeof TRANSLATIONS, any> {
120
+ common: CommonTranslationsEn;
121
+ }
122
+
123
+ export const store = createTranslationStore({
124
+ translations: TRANSLATIONS,
125
+ locales: LOCALES,
126
+ loadModule: async (locale, translation) => {
127
+ return await import(`./translations/${translation}/${locale}.tsx`);
128
+ },
129
+ extractTranslation: (module) => new module.default(),
130
+ defaultLocale: 'en',
131
+ }).type<ITranslationStoreTypes>();
132
+ ```
133
+
134
+ ```typescript
135
+ // hooks/useTranslation.ts
136
+ import { useI18nTranslation } from 'i18n-typed-store/react/useI18nTranslation';
137
+ import type { TRANSLATIONS, LOCALES } from '../constants';
138
+ import type { ITranslationStoreTypes } from '../store';
139
+
140
+ export const useTranslation = <K extends keyof typeof TRANSLATIONS>(translation: K) => {
141
+ return useI18nTranslation<typeof TRANSLATIONS, typeof LOCALES, ITranslationStoreTypes, K>(translation);
142
+ };
143
+ ```
144
+
145
+ ```tsx
146
+ // App.tsx
147
+ import { I18nTypedStoreProvider, useI18nLocale } from 'i18n-typed-store/react';
148
+ import { store } from './store';
149
+ import { useTranslation } from './hooks/useTranslation';
150
+
151
+ function App() {
152
+ return (
153
+ <I18nTypedStoreProvider store={store}>
154
+ <MyComponent />
155
+ </I18nTypedStoreProvider>
156
+ );
157
+ }
158
+ ```
159
+
160
+ ```tsx
161
+ // MyComponent.tsx
162
+ function MyComponent() {
163
+ const translations = useTranslation('common');
164
+ const { locale, setLocale } = useI18nLocale();
165
+
166
+ if (!translations) {
167
+ return <div>Loading...</div>;
168
+ }
169
+
170
+ return (
171
+ <div>
172
+ <h1>{translations.title}</h1>
173
+ <p>{translations.greeting}</p>
174
+ <button onClick={() => setLocale('ru')}>Switch to Russian</button>
175
+ </div>
176
+ );
177
+ }
178
+ ```
179
+
180
+ ### React Suspense Support
181
+
182
+ ```typescript
183
+ // hooks/useTranslationLazy.ts
184
+ import { useI18nTranslationLazy } from 'i18n-typed-store/react/useI18nTranslationLazy';
185
+ import type { TRANSLATIONS, LOCALES } from '../constants';
186
+ import type { ITranslationStoreTypes } from '../store';
187
+
188
+ export const useTranslationLazy = <K extends keyof typeof TRANSLATIONS>(translation: K) => {
189
+ return useI18nTranslationLazy<typeof TRANSLATIONS, typeof LOCALES, ITranslationStoreTypes, K>(translation);
190
+ };
191
+ ```
192
+
193
+ ```tsx
194
+ // MyComponent.tsx
195
+ import { useTranslationLazy } from './hooks/useTranslationLazy';
196
+
197
+ function MyComponent() {
198
+ // This hook throws a promise if translation is not loaded (for Suspense)
199
+ const translations = useTranslationLazy('common');
200
+
201
+ return (
202
+ <div>
203
+ <h1>{translations.title}</h1>
204
+ <p>{translations.greeting}</p>
205
+ </div>
206
+ );
207
+ }
208
+ ```
209
+
210
+ ```tsx
211
+ // App.tsx
212
+ import { Suspense } from 'react';
213
+ import { I18nTypedStoreProvider } from 'i18n-typed-store/react';
214
+ import { store } from './store';
215
+ import { MyComponent } from './MyComponent';
216
+
217
+ function App() {
218
+ return (
219
+ <I18nTypedStoreProvider store={store} suspenseMode="first-load-locale">
220
+ <Suspense fallback={<div>Loading translations...</div>}>
221
+ <MyComponent />
222
+ </Suspense>
223
+ </I18nTypedStoreProvider>
224
+ );
225
+ }
226
+ ```
227
+
228
+ ### SSR with Next.js
229
+
230
+ ```typescript
231
+ // lib/i18n.ts
232
+ import { createTranslationStore } from 'i18n-typed-store';
233
+ import type CommonTranslationsEn from './translations/common/en';
234
+ import type ErrorsTranslationsEn from './translations/errors/en';
235
+
236
+ const translations = { common: 'common', errors: 'errors' } as const;
237
+ const locales = { en: 'en', ru: 'ru' } as const;
238
+
239
+ export const storeFactory = createTranslationStore({
240
+ translations,
241
+ locales,
242
+ loadModule: async (locale, translation) => {
243
+ return await import(`./translations/${translation}/${locale}.tsx`);
244
+ },
245
+ extractTranslation: (module) => new module.default(),
246
+ defaultLocale: 'en',
247
+ });
248
+
249
+ interface TranslationData extends Record<keyof typeof translations, any> {
250
+ common: CommonTranslationsEn;
251
+ errors: ErrorsTranslationsEn;
252
+ }
253
+
254
+ export type Store = ReturnType<typeof storeFactory.type<TranslationData>>;
255
+ ```
256
+
257
+ ```typescript
258
+ // pages/_app.tsx or app/layout.tsx
259
+ import { I18nTypedStoreProvider } from 'i18n-typed-store/react';
260
+ import { storeFactory } from '../lib/i18n';
261
+
262
+ const store = storeFactory.type<TranslationData>();
263
+
264
+ function MyApp({ Component, pageProps }: AppProps) {
265
+ return (
266
+ <I18nTypedStoreProvider store={store}>
267
+ <Component {...pageProps} />
268
+ </I18nTypedStoreProvider>
269
+ );
270
+ }
271
+ ```
272
+
273
+ ```typescript
274
+ // pages/index.tsx (getServerSideProps)
275
+ import type { GetServerSidePropsContext } from 'next';
276
+ import { getLocaleFromRequest, initializeStore } from 'i18n-typed-store/react';
277
+ import { storeFactory } from '../lib/i18n';
278
+
279
+ export async function getServerSideProps(context: GetServerSidePropsContext) {
280
+ const locale = getLocaleFromRequest(context, {
281
+ defaultLocale: 'en',
282
+ availableLocales: ['en', 'ru'],
283
+ cookieName: 'locale',
284
+ queryParamName: 'locale',
285
+ });
286
+
287
+ const store = storeFactory.type<TranslationData>();
288
+ initializeStore(store, locale);
289
+
290
+ // Preload translations if needed
291
+ await store.translations.common.load(locale);
292
+
293
+ return {
294
+ props: {
295
+ locale,
296
+ // You can pass translations as props or use context
297
+ },
298
+ };
299
+ }
300
+ ```
301
+
302
+ ```typescript
303
+ // app/page.tsx (App Router)
304
+ import { getLocaleFromRequest, initializeStore } from 'i18n-typed-store/react';
305
+ import { storeFactory } from '../lib/i18n';
306
+ import { headers, cookies } from 'next/headers';
307
+
308
+ export default async function Page() {
309
+ const headersList = await headers();
310
+ const cookieStore = await cookies();
311
+
312
+ const locale = getLocaleFromRequest(
313
+ {
314
+ headers: Object.fromEntries(headersList),
315
+ cookies: Object.fromEntries(cookieStore),
316
+ },
317
+ {
318
+ defaultLocale: 'en',
319
+ availableLocales: ['en', 'ru'],
320
+ }
321
+ );
322
+
323
+ const store = storeFactory.type<TranslationData>();
324
+ initializeStore(store, locale);
325
+ await store.translations.common.load(locale);
326
+
327
+ return <div>...</div>;
328
+ }
76
329
  ```
77
330
 
78
331
  ## Core API
@@ -82,46 +335,65 @@ console.log(store.common.translation?.title); // Type-safe access
82
335
  Creates a type-safe translation store with lazy loading support.
83
336
 
84
337
  ```typescript
85
- const storeFactory = createTranslationStore<T, L, Module>(
86
- translations: T,
87
- locales: L,
88
- loadModule: (locale: keyof L, translation: keyof T) => Promise<Module>,
89
- extractTranslation: (module: Module, locale: keyof L, translation: keyof T) => unknown
90
- );
338
+ function createTranslationStore<T, L, Module>(options: {
339
+ translations: T;
340
+ locales: L;
341
+ loadModule: (locale: keyof L, namespace: keyof T) => Promise<Module>;
342
+ extractTranslation: (module: Module, locale: keyof L, namespace: keyof T) => unknown | Promise<unknown>;
343
+ defaultLocale: keyof L;
344
+ useFallback?: boolean;
345
+ fallbackLocale?: keyof L;
346
+ deleteOtherLocalesAfterLoad?: boolean;
347
+ loadFromCache?: boolean;
348
+ changeLocaleEventName?: string;
349
+ }): {
350
+ type<M extends { [K in keyof T]: any }>(): TranslationStore<T, L, M>;
351
+ }
91
352
  ```
92
353
 
93
- **Parameters:**
354
+ **Options:**
94
355
 
95
356
  - `translations` - Object with translation keys (e.g., `{ common: 'common', errors: 'errors' }`)
96
357
  - `locales` - Object with locale keys (e.g., `{ en: 'en', ru: 'ru' }`)
97
358
  - `loadModule` - Async function to load a translation module
98
- - `extractTranslation` - Function to extract translation data from the loaded module. Receives the module, locale, and translation key as parameters, allowing for locale-specific or translation-specific extraction logic.
359
+ - `extractTranslation` - Function to extract translation data from the loaded module. Receives the module, locale, and namespace key as parameters
360
+ - `defaultLocale` - Default locale key to use
361
+ - `useFallback` - Whether to use fallback locale for missing translations (default: `false`)
362
+ - `fallbackLocale` - Fallback locale key (default: `defaultLocale`)
363
+ - `deleteOtherLocalesAfterLoad` - Whether to delete translations for other locales after loading (default: `false`)
364
+ - `loadFromCache` - Whether to load translations from cache by default (default: `true`)
365
+ - `changeLocaleEventName` - Event name for locale change events (default: `'change-locale'`)
99
366
 
100
367
  **Returns:** Object with `type<M>()` method that creates a typed store.
101
368
 
102
369
  **Example:**
103
370
 
104
371
  ```typescript
105
- const storeFactory = createTranslationStore(
106
- { common: 'common' },
107
- { en: 'en', ru: 'ru' },
108
- async (locale, translation) => {
109
- return await import(`./locales/${locale}/${translation}.json`);
110
- },
111
- (module, locale, translation) => module.default
112
- );
372
+ import type CommonTranslationsEn from './translations/common/en';
113
373
 
114
- type TranslationData = {
115
- common: { title: string; description: string };
116
- };
374
+ const storeFactory = createTranslationStore({
375
+ translations: { common: 'common' },
376
+ locales: { en: 'en', ru: 'ru' },
377
+ loadModule: async (locale, translation) => {
378
+ return await import(`./translations/${translation}/${locale}.tsx`);
379
+ },
380
+ extractTranslation: (module) => new module.default(),
381
+ defaultLocale: 'en',
382
+ useFallback: true,
383
+ fallbackLocale: 'en',
384
+ });
385
+
386
+ interface TranslationData extends Record<keyof typeof translations, any> {
387
+ common: CommonTranslationsEn;
388
+ }
117
389
 
118
390
  const store = storeFactory.type<TranslationData>();
119
391
 
120
392
  // Load translation
121
- await store.common.load('en');
393
+ await store.translations.common.load('en');
122
394
 
123
- // Access translation (type-safe)
124
- const title = store.common.translation?.title;
395
+ // Access translation (type-safe with IDE go-to support)
396
+ const title = store.translations.common.currentTranslation?.title;
125
397
  ```
126
398
 
127
399
  ### `createTranslationModuleMap`
@@ -129,11 +401,11 @@ const title = store.common.translation?.title;
129
401
  Creates a map of translation module loaders for all combinations of translations and locales.
130
402
 
131
403
  ```typescript
132
- const moduleMap = createTranslationModuleMap<T, L, Module>(
404
+ function createTranslationModuleMap<T, L, Module>(
133
405
  translations: T,
134
406
  locales: L,
135
407
  loadModule: (locale: keyof L, translation: keyof T) => Promise<Module>
136
- );
408
+ ): Record<keyof T, Record<keyof L, () => Promise<Module>>>
137
409
  ```
138
410
 
139
411
  **Example:**
@@ -143,7 +415,7 @@ const moduleMap = createTranslationModuleMap(
143
415
  { common: 'common' },
144
416
  { en: 'en', ru: 'ru' },
145
417
  async (locale, translation) => {
146
- return await import(`./locales/${locale}/${translation}.json`);
418
+ return await import(`./translations/${translation}/${locale}.tsx`);
147
419
  }
148
420
  );
149
421
 
@@ -157,7 +429,10 @@ const module = await loader();
157
429
  Creates a plural form selector function for a specific locale using `Intl.PluralRules`.
158
430
 
159
431
  ```typescript
160
- const selectPlural = createPluralSelector(locale: string);
432
+ function createPluralSelector(
433
+ locale: string,
434
+ options?: { strict?: boolean }
435
+ ): (count: number, variants: PluralVariants) => string
161
436
  ```
162
437
 
163
438
  **Example:**
@@ -194,57 +469,360 @@ selectPlural(2, variants); // => 'яблока'
194
469
  selectPlural(5, variants); // => 'яблок'
195
470
  ```
196
471
 
472
+ ## React API
473
+
474
+ ### `I18nTypedStoreProvider`
475
+
476
+ Provider component that wraps your application to provide translation store context.
477
+
478
+ ```tsx
479
+ <I18nTypedStoreProvider
480
+ store={store}
481
+ suspenseMode="first-load-locale"
482
+ >
483
+ {children}
484
+ </I18nTypedStoreProvider>
485
+ ```
486
+
487
+ **Props:**
488
+
489
+ - `store` - Translation store instance
490
+ - `suspenseMode` - Suspense mode: `'once'` | `'first-load-locale'` | `'change-locale'` (default: `'first-load-locale'`)
491
+ - `children` - React children
492
+
493
+ ### `useI18nTranslation`
494
+
495
+ Hook for accessing translations with automatic loading. Returns `undefined` if translation is not yet loaded.
496
+
497
+ ```tsx
498
+ // Direct usage
499
+ const translations = useI18nTranslation('common', fromCache?: boolean);
500
+
501
+ // Typed wrapper (recommended)
502
+ import { useI18nTranslation } from 'i18n-typed-store/react/useI18nTranslation';
503
+ import type { TRANSLATIONS, LOCALES } from './constants';
504
+ import type { ITranslationStoreTypes } from './store';
505
+
506
+ export const useTranslation = <K extends keyof typeof TRANSLATIONS>(translation: K) => {
507
+ return useI18nTranslation<typeof TRANSLATIONS, typeof LOCALES, ITranslationStoreTypes, K>(translation);
508
+ };
509
+
510
+ // Usage
511
+ const translations = useTranslation('common');
512
+ ```
513
+
514
+ ### `useI18nTranslationLazy`
515
+
516
+ Hook for accessing translations with React Suspense support. Throws a promise if translation is not loaded.
517
+
518
+ ```tsx
519
+ // Direct usage
520
+ const translations = useI18nTranslationLazy('common', fromCache?: boolean);
521
+
522
+ // Typed wrapper (recommended)
523
+ import { useI18nTranslationLazy } from 'i18n-typed-store/react/useI18nTranslationLazy';
524
+ import type { TRANSLATIONS, LOCALES } from './constants';
525
+ import type { ITranslationStoreTypes } from './store';
526
+
527
+ export const useTranslationLazy = <K extends keyof typeof TRANSLATIONS>(translation: K) => {
528
+ return useI18nTranslationLazy<typeof TRANSLATIONS, typeof LOCALES, ITranslationStoreTypes, K>(translation);
529
+ };
530
+
531
+ // Usage
532
+ const translations = useTranslationLazy('common');
533
+ ```
534
+
535
+ ### `useI18nLocale`
536
+
537
+ Hook for accessing and managing the current locale.
538
+
539
+ ```tsx
540
+ const { locale, setLocale } = useI18nLocale();
541
+ ```
542
+
543
+ ### `Safe`
544
+
545
+ Component that safely extracts strings from translation objects, catching errors.
546
+
547
+ ```tsx
548
+ <Safe
549
+ errorComponent={<span>N/A</span>}
550
+ errorHandler={(error) => console.error(error)}
551
+ >
552
+ {() => translations.common.pages.main.title}
553
+ </Safe>
554
+ ```
555
+
556
+ ## SSR API
557
+
558
+ ### `getLocaleFromRequest`
559
+
560
+ Gets locale from SSR request context (query params, cookies, headers).
561
+
562
+ ```typescript
563
+ function getLocaleFromRequest<L extends Record<string, string>>(
564
+ context: RequestContext,
565
+ options: {
566
+ defaultLocale: string;
567
+ availableLocales: readonly string[];
568
+ headerName?: string;
569
+ cookieName?: string;
570
+ queryParamName?: string;
571
+ parseAcceptLanguage?: boolean;
572
+ }
573
+ ): keyof L
574
+ ```
575
+
576
+ **Example:**
577
+
578
+ ```typescript
579
+ const locale = getLocaleFromRequest(context, {
580
+ defaultLocale: 'en',
581
+ availableLocales: ['en', 'ru'],
582
+ cookieName: 'locale',
583
+ queryParamName: 'locale',
584
+ headerName: 'accept-language',
585
+ parseAcceptLanguage: true,
586
+ });
587
+ ```
588
+
589
+ ### `initializeStore`
590
+
591
+ Initializes translation store with a specific locale for SSR.
592
+
593
+ ```typescript
594
+ function initializeStore<T, L, M>(
595
+ store: TranslationStore<T, L, M>,
596
+ locale: keyof L
597
+ ): void
598
+ ```
599
+
197
600
  ## Advanced Usage
198
601
 
602
+ ### Translation Classes Structure
603
+
604
+ The library is designed to work with TypeScript classes for translations, providing full type safety and IDE support (go-to definition, autocomplete). Here's an example of a translation class:
605
+
606
+ ```typescript
607
+ // translations/common/en.tsx
608
+ import { createPluralSelector } from 'i18n-typed-store';
609
+
610
+ const plur = createPluralSelector('en');
611
+
612
+ export default class CommonTranslationsEn {
613
+ title = 'Welcome';
614
+ loading = 'Loading...';
615
+ error = 'An error occurred';
616
+
617
+ greeting = (
618
+ <>
619
+ Hello, <strong>World</strong>!
620
+ </>
621
+ );
622
+
623
+ buttons = {
624
+ save: 'Save',
625
+ cancel: 'Cancel',
626
+ delete: 'Delete',
627
+ };
628
+
629
+ messages = {
630
+ notFound: 'Not found',
631
+ unauthorized: (
632
+ <>
633
+ You are <strong>not authorized</strong> to perform this action
634
+ </>
635
+ ),
636
+ };
637
+
638
+ // Pluralization method
639
+ items = (count: number) =>
640
+ count + ' ' + plur(count, {
641
+ one: 'item',
642
+ other: 'items',
643
+ });
644
+ }
645
+ ```
646
+
647
+ ```typescript
648
+ // lib/i18n.ts
649
+ import { createTranslationStore } from 'i18n-typed-store';
650
+ import type CommonTranslationsEn from './translations/common/en';
651
+ import type MainTranslationsEn from './translations/main/en';
652
+ import type NewsTranslationsEn from './translations/news/en';
653
+ import type SettingsTranslationsEn from './translations/settings/en';
654
+
655
+ const translations = {
656
+ common: 'common',
657
+ main: 'main',
658
+ news: 'news',
659
+ settings: 'settings',
660
+ } as const;
661
+
662
+ const locales = {
663
+ en: 'en',
664
+ ru: 'ru',
665
+ } as const;
666
+
667
+ export interface ITranslationStoreTypes extends Record<keyof typeof translations, any> {
668
+ common: CommonTranslationsEn;
669
+ main: MainTranslationsEn;
670
+ news: NewsTranslationsEn;
671
+ settings: SettingsTranslationsEn;
672
+ }
673
+
674
+ export const store = createTranslationStore({
675
+ translations,
676
+ locales,
677
+ loadModule: (locale, namespace) => {
678
+ return import(`./translations/${namespace}/${locale}.tsx`);
679
+ },
680
+ extractTranslation: (module) => new module.default(),
681
+ defaultLocale: 'en',
682
+ deleteOtherLocalesAfterLoad: false,
683
+ loadFromCache: false,
684
+ }).type<ITranslationStoreTypes>();
685
+ ```
686
+
687
+ **Benefits of using classes:**
688
+
689
+ - ✅ Full TypeScript type safety with IDE go-to definition support
690
+ - ✅ Support for JSX elements in translations
691
+ - ✅ Methods for pluralization and dynamic translations
692
+ - ✅ Better code organization and maintainability
693
+ - ✅ Compile-time validation of translation keys
694
+
695
+ ### Creating Typed Hook Wrappers
696
+
697
+ For better type safety and IDE support, it's recommended to create typed wrapper hooks. This ensures full type inference and autocomplete when using translations in your components.
698
+
699
+ ```typescript
700
+ // hooks/useTranslation.ts
701
+ import { useI18nTranslation } from 'i18n-typed-store/react/useI18nTranslation';
702
+ import type { TRANSLATIONS, LOCALES } from '../constants';
703
+ import type { ITranslationStoreTypes } from '../store';
704
+
705
+ export const useTranslation = <K extends keyof typeof TRANSLATIONS>(translation: K) => {
706
+ return useI18nTranslation<typeof TRANSLATIONS, typeof LOCALES, ITranslationStoreTypes, K>(translation);
707
+ };
708
+ ```
709
+
710
+ ```typescript
711
+ // hooks/useTranslationLazy.ts
712
+ import { useI18nTranslationLazy } from 'i18n-typed-store/react/useI18nTranslationLazy';
713
+ import type { TRANSLATIONS, LOCALES } from '../constants';
714
+ import type { ITranslationStoreTypes } from '../store';
715
+
716
+ export const useTranslationLazy = <K extends keyof typeof TRANSLATIONS>(translation: K) => {
717
+ return useI18nTranslationLazy<typeof TRANSLATIONS, typeof LOCALES, ITranslationStoreTypes, K>(translation);
718
+ };
719
+ ```
720
+
721
+ ```typescript
722
+ // constants.ts
723
+ export const TRANSLATIONS = {
724
+ common: 'common',
725
+ main: 'main',
726
+ news: 'news',
727
+ settings: 'settings',
728
+ } as const;
729
+
730
+ export const LOCALES = {
731
+ en: 'en',
732
+ ru: 'ru',
733
+ } as const;
734
+ ```
735
+
736
+ **Usage in components:**
737
+
738
+ ```tsx
739
+ import { useTranslation } from './hooks/useTranslation';
740
+ import { useTranslationLazy } from './hooks/useTranslationLazy';
741
+
742
+ // With useTranslation (returns undefined if not loaded)
743
+ function MyComponent() {
744
+ const translations = useTranslation('common');
745
+
746
+ if (!translations) {
747
+ return <div>Loading...</div>;
748
+ }
749
+
750
+ return <div>{translations.title}</div>;
751
+ }
752
+
753
+ // With useTranslationLazy (for Suspense)
754
+ function MyComponentLazy() {
755
+ const translations = useTranslationLazy('common');
756
+
757
+ return <div>{translations.title}</div>;
758
+ }
759
+ ```
760
+
761
+ **Benefits:**
762
+
763
+ - ✅ Full type safety with autocomplete
764
+ - ✅ IDE go-to definition support
765
+ - ✅ Compile-time validation of translation keys
766
+ - ✅ Consistent API across your application
767
+
199
768
  ### Working with Dynamic Imports
200
769
 
201
770
  ```typescript
202
- const storeFactory = createTranslationStore(
771
+ const storeFactory = createTranslationStore({
203
772
  translations,
204
773
  locales,
205
- async (locale, translation) => {
774
+ loadModule: async (locale, translation) => {
206
775
  // Dynamic import with error handling
207
776
  try {
208
777
  const module = await import(
209
- `./locales/${locale}/${translation}.json`
778
+ `./translations/${translation}/${locale}.tsx`
210
779
  );
211
- return module.default;
780
+ return module;
212
781
  } catch (error) {
213
782
  console.error(`Failed to load ${translation} for ${locale}`);
214
783
  throw error;
215
784
  }
216
785
  },
217
- (module, locale, translation) => module
218
- );
786
+ extractTranslation: (module) => {
787
+ // Instantiate the translation class
788
+ return new module.default();
789
+ },
790
+ defaultLocale: 'en',
791
+ });
219
792
  ```
220
793
 
221
794
  ### Custom Module Extraction
222
795
 
223
- The `extractTranslation` function receives the module, locale, and translation key, allowing for advanced extraction logic:
796
+ The `extractTranslation` function receives the module, locale, and namespace key, allowing for advanced extraction logic:
224
797
 
225
798
  ```typescript
226
- const storeFactory = createTranslationStore(
799
+ const storeFactory = createTranslationStore({
227
800
  translations,
228
801
  locales,
229
- async (locale, translation) => {
230
- // Load module that exports default
231
- return await import(`./locales/${locale}/${translation}.ts`);
802
+ loadModule: async (locale, translation) => {
803
+ // Special handling for certain namespaces
804
+ if (translation === 'lang') {
805
+ return await import(`./translations/${translation}/index.tsx`);
806
+ }
807
+ return await import(`./translations/${translation}/${locale}.tsx`);
232
808
  },
233
- (module, locale, translation) => {
234
- // Extract from module.default or module
809
+ extractTranslation: (module) => {
810
+ // Instantiate the translation class
235
811
  // You can use locale and translation parameters for custom logic
236
- if (locale === 'en' && translation === 'common') {
237
- // Special handling for English common translations
238
- return module.default?.en || module.default;
239
- }
240
- return module.default || module;
241
- }
242
- );
812
+ return new module.default();
813
+ },
814
+ defaultLocale: 'en',
815
+ });
243
816
  ```
244
817
 
245
818
  ### Handling Multiple Translation Namespaces
246
819
 
247
820
  ```typescript
821
+ import type CommonTranslationsEn from './translations/common/en';
822
+ import type ErrorsTranslationsEn from './translations/errors/en';
823
+ import type UiTranslationsEn from './translations/ui/en';
824
+ import type AdminTranslationsEn from './translations/admin/en';
825
+
248
826
  const translations = {
249
827
  common: 'common',
250
828
  errors: 'errors',
@@ -252,22 +830,44 @@ const translations = {
252
830
  admin: 'admin',
253
831
  } as const;
254
832
 
255
- type TranslationData = {
256
- common: { title: string };
257
- errors: { notFound: string };
258
- ui: { buttons: { save: string } };
259
- admin: { dashboard: { title: string } };
260
- };
833
+ interface TranslationData extends Record<keyof typeof translations, any> {
834
+ common: CommonTranslationsEn;
835
+ errors: ErrorsTranslationsEn;
836
+ ui: UiTranslationsEn;
837
+ admin: AdminTranslationsEn;
838
+ }
261
839
 
262
840
  const store = storeFactory.type<TranslationData>();
263
841
 
264
842
  // Load specific translations
265
- await store.common.load('en');
266
- await store.ui.load('en');
843
+ await store.translations.common.load('en');
844
+ await store.translations.ui.load('en');
267
845
 
268
- // Access translations
269
- const title = store.common.translation?.title;
270
- const saveButton = store.ui.translation?.buttons.save;
846
+ // Access translations (with full IDE support)
847
+ const title = store.translations.common.currentTranslation?.title;
848
+ const saveButton = store.translations.ui.currentTranslation?.buttons.save;
849
+ ```
850
+
851
+ ### Using Fallback Locales
852
+
853
+ When `useFallback` is enabled, missing translations are automatically filled from the fallback locale:
854
+
855
+ ```typescript
856
+ const storeFactory = createTranslationStore({
857
+ translations: { common: 'common' },
858
+ locales: { en: 'en', ru: 'ru' },
859
+ loadModule: async (locale, translation) => {
860
+ return await import(`./translations/${translation}/${locale}.tsx`);
861
+ },
862
+ extractTranslation: (module) => new module.default(),
863
+ defaultLocale: 'en',
864
+ useFallback: true,
865
+ fallbackLocale: 'en',
866
+ });
867
+
868
+ // If 'ru' translation is missing some keys, they will be filled from 'en'
869
+ await store.translations.common.load('ru');
870
+ // Result: merged translation with 'en' as fallback
271
871
  ```
272
872
 
273
873
  ## Type Safety
@@ -276,16 +876,16 @@ The library provides complete type safety:
276
876
 
277
877
  ```typescript
278
878
  // ✅ TypeScript knows all available translation keys
279
- const title = store.common.translation?.title;
879
+ const title = store.translations.common.currentTranslation?.title;
280
880
 
281
881
  // ❌ TypeScript error: 'invalidKey' doesn't exist
282
- const invalid = store.common.translation?.invalidKey;
882
+ const invalid = store.translations.common.currentTranslation?.invalidKey;
283
883
 
284
884
  // ✅ TypeScript knows all available locales
285
- await store.common.load('en');
885
+ await store.translations.common.load('en');
286
886
 
287
887
  // ❌ TypeScript error: 'fr' is not a valid locale
288
- await store.common.load('fr');
888
+ await store.translations.common.load('fr');
289
889
  ```
290
890
 
291
891
  ## Pluralization
@@ -307,6 +907,100 @@ The library uses `Intl.PluralRules` for plural form selection, supporting all Un
307
907
  - Arabic (zero/one/two/few/many/other)
308
908
  - And many more...
309
909
 
910
+ ## Examples
911
+
912
+ ### Complete Example Project
913
+
914
+ For a full-featured example with React, TypeScript, and all features demonstrated, see the [example repository](https://github.com/ialexanderlvov/i18n-typed-store-react-example).
915
+
916
+ ### Example: E-commerce Application
917
+
918
+ ```typescript
919
+ import type ProductsTranslationsEn from './translations/products/en';
920
+ import type CartTranslationsEn from './translations/cart/en';
921
+ import type CheckoutTranslationsEn from './translations/checkout/en';
922
+
923
+ const translations = {
924
+ products: 'products',
925
+ cart: 'cart',
926
+ checkout: 'checkout',
927
+ } as const;
928
+
929
+ const locales = {
930
+ en: 'en',
931
+ ru: 'ru',
932
+ de: 'de',
933
+ } as const;
934
+
935
+ interface TranslationData extends Record<keyof typeof translations, any> {
936
+ products: ProductsTranslationsEn;
937
+ cart: CartTranslationsEn;
938
+ checkout: CheckoutTranslationsEn;
939
+ }
940
+
941
+ const storeFactory = createTranslationStore({
942
+ translations,
943
+ locales,
944
+ loadModule: async (locale, translation) => {
945
+ return await import(`./translations/${translation}/${locale}.ts`);
946
+ },
947
+ extractTranslation: (module) => new module.default(),
948
+ defaultLocale: 'en',
949
+ });
950
+
951
+ const store = storeFactory.type<TranslationData>();
952
+
953
+ // Load translations
954
+ await store.translations.products.load('en');
955
+ await store.translations.cart.load('en');
956
+
957
+ // Use translations (with full IDE go-to support)
958
+ const productTitle = store.translations.products.currentTranslation?.title;
959
+ const cartTitle = store.translations.cart.currentTranslation?.title;
960
+ ```
961
+
962
+ ### Example: Pluralization in Product List
963
+
964
+ ```typescript
965
+ // translations/products/en.tsx
966
+ import { createPluralSelector } from 'i18n-typed-store';
967
+
968
+ const plur = createPluralSelector('en');
969
+
970
+ export default class ProductsTranslationsEn {
971
+ title = 'Products';
972
+ addToCart = 'Add to Cart';
973
+ price = 'Price';
974
+
975
+ // Pluralization method
976
+ productCount = (count: number) =>
977
+ count + ' ' + plur(count, {
978
+ one: 'product',
979
+ other: 'products',
980
+ });
981
+
982
+ itemsInCart = (count: number) =>
983
+ count + ' ' + plur(count, {
984
+ zero: 'No items',
985
+ one: 'item',
986
+ other: 'items',
987
+ }) + ' in cart';
988
+ }
989
+
990
+ // Usage in component with typed hook
991
+ import { useTranslation } from './hooks/useTranslation';
992
+
993
+ const translations = useTranslation('products');
994
+
995
+ if (translations) {
996
+ translations.productCount(1); // => "1 product"
997
+ translations.productCount(5); // => "5 products"
998
+ translations.itemsInCart(0); // => "0 No items in cart"
999
+ translations.itemsInCart(1); // => "1 item in cart"
1000
+ translations.itemsInCart(5); // => "5 items in cart"
1001
+ }
1002
+ ```
1003
+
310
1004
  ## API Reference
311
1005
 
312
1006
  ### `createTranslationStore`
@@ -316,13 +1010,8 @@ function createTranslationStore<
316
1010
  T extends Record<string, string>,
317
1011
  L extends Record<string, string>,
318
1012
  Module = unknown
319
- >(
320
- translations: T,
321
- locales: L,
322
- loadModule: (locale: keyof L, translation: keyof T) => Promise<Module>,
323
- extractTranslation: (module: Module, locale: keyof L, translation: keyof T) => unknown
324
- ): {
325
- type<M extends { [K in keyof T]: Record<string, unknown> }>(): TranslationStore<T, L, M>;
1013
+ >(options: CreateTranslationStoreOptions<T, L, Module>): {
1014
+ type<M extends { [K in keyof T]: any }>(): TranslationStore<T, L, M>;
326
1015
  }
327
1016
  ```
328
1017
 
@@ -343,10 +1032,10 @@ function createTranslationModuleMap<
343
1032
  ### `createPluralSelector`
344
1033
 
345
1034
  ```typescript
346
- function createPluralSelector(locale: string): (
347
- count: number,
348
- variants: PluralVariants
349
- ) => string
1035
+ function createPluralSelector(
1036
+ locale: string,
1037
+ options?: { strict?: boolean }
1038
+ ): (count: number, variants: PluralVariants) => string
350
1039
  ```
351
1040
 
352
1041
  ### `PluralVariants`
@@ -362,79 +1051,6 @@ type PluralVariants = {
362
1051
  };
363
1052
  ```
364
1053
 
365
- ## Examples
366
-
367
- ### Example: E-commerce Application
368
-
369
- ```typescript
370
- const translations = {
371
- products: 'products',
372
- cart: 'cart',
373
- checkout: 'checkout',
374
- } as const;
375
-
376
- const locales = {
377
- en: 'en',
378
- ru: 'ru',
379
- de: 'de',
380
- } as const;
381
-
382
- type TranslationData = {
383
- products: {
384
- title: string;
385
- addToCart: string;
386
- price: string;
387
- };
388
- cart: {
389
- title: string;
390
- empty: string;
391
- total: string;
392
- };
393
- checkout: {
394
- title: string;
395
- placeOrder: string;
396
- };
397
- };
398
-
399
- const storeFactory = createTranslationStore(
400
- translations,
401
- locales,
402
- async (locale, translation) => {
403
- return await import(`./locales/${locale}/${translation}.json`);
404
- },
405
- (module, locale, translation) => module.default
406
- );
407
-
408
- const store = storeFactory.type<TranslationData>();
409
-
410
- // Load translations
411
- await store.products.load('en');
412
- await store.cart.load('en');
413
-
414
- // Use translations
415
- const productTitle = store.products.translation?.title;
416
- const cartTitle = store.cart.translation?.title;
417
- ```
418
-
419
- ### Example: Pluralization in Product List
420
-
421
- ```typescript
422
- import { createPluralSelector } from 'i18n-typed-store';
423
-
424
- const selectPlural = createPluralSelector('en');
425
-
426
- function getProductCountText(count: number): string {
427
- return selectPlural(count, {
428
- one: `${count} product`,
429
- other: `${count} products`,
430
- });
431
- }
432
-
433
- // Usage
434
- getProductCountText(1); // => "1 product"
435
- getProductCountText(5); // => "5 products"
436
- ```
437
-
438
1054
  ## Contributing
439
1055
 
440
1056
  Contributions are welcome! Please feel free to submit a Pull Request.
@@ -450,3 +1066,7 @@ Alexander Lvov
450
1066
  ## Repository
451
1067
 
452
1068
  [GitHub](https://github.com/ialexanderlvov/i18n-typed-store)
1069
+
1070
+ ## Example Project Git
1071
+
1072
+ [React Example](https://github.com/ialexanderlvov/i18n-typed-store-react-example) - Complete working example with React, TypeScript, and all features demonstrated.