i18n-typed-store-react 0.1.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 ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2025 Alexander Lvov
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the “Software”), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,663 @@
1
+ # i18n-typed-store-react
2
+
3
+ > ⚠️ **WARNING: The library API is under active development and may change significantly between versions. Use exact versions in package.json and read the changelog carefully when updating.**
4
+
5
+ React integration for [i18n-typed-store](https://github.com/ialexanderlvov/i18n-typed-store) - a type-safe translation store for managing i18n locales with full TypeScript support. Provides React hooks, components, and SSR utilities for seamless integration with React applications.
6
+
7
+ ## Features
8
+
9
+ - ✅ **React Hooks** - `useI18nTranslation`, `useI18nTranslationLazy`, `useI18nLocale`
10
+ - ✅ **React Suspense Support** - Built-in support for React Suspense with lazy loading
11
+ - ✅ **Provider Component** - `I18nTypedStoreProvider` for providing translation context
12
+ - ✅ **SSR/SSG Support** - Utilities for Next.js and other SSR frameworks
13
+ - ✅ **Type-Safe** - Full TypeScript support with autocomplete and go-to definition
14
+ - ✅ **Safe Component** - Error-safe component for accessing translations
15
+ - ✅ **Locale Management** - Hook for accessing and changing locales with automatic updates
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install i18n-typed-store-react
21
+ ```
22
+
23
+ ```bash
24
+ yarn add i18n-typed-store-react
25
+ ```
26
+
27
+ ```bash
28
+ pnpm add i18n-typed-store-react
29
+ ```
30
+
31
+ ## Quick Start
32
+
33
+ ### Basic Setup
34
+
35
+ First, create your translation store using `i18n-typed-store`:
36
+
37
+ ```typescript
38
+ // store.ts
39
+ import { createTranslationStore } from 'i18n-typed-store';
40
+ import type CommonTranslationsEn from './translations/common/en';
41
+ import { TRANSLATIONS, LOCALES } from './constants';
42
+
43
+ export interface ITranslationStoreTypes extends Record<keyof typeof TRANSLATIONS, any> {
44
+ common: CommonTranslationsEn;
45
+ }
46
+
47
+ export const store = createTranslationStore({
48
+ namespaces: TRANSLATIONS,
49
+ locales: LOCALES,
50
+ loadModule: async (locale, namespace) => {
51
+ return await import(`./translations/${namespace}/${locale}.tsx`);
52
+ },
53
+ extractTranslation: (module) => new module.default(),
54
+ defaultLocale: 'en',
55
+ }).type<ITranslationStoreTypes>();
56
+ ```
57
+
58
+ ```typescript
59
+ // constants.ts
60
+ export const TRANSLATIONS = {
61
+ common: 'common',
62
+ } as const;
63
+
64
+ export const LOCALES = {
65
+ en: 'en',
66
+ ru: 'ru',
67
+ } as const;
68
+ ```
69
+
70
+ ### Wrap Your App with Provider
71
+
72
+ ```tsx
73
+ // App.tsx
74
+ import { I18nTypedStoreProvider } from 'i18n-typed-store-react';
75
+ import { store } from './store';
76
+ import { MyComponent } from './MyComponent';
77
+
78
+ function App() {
79
+ return (
80
+ <I18nTypedStoreProvider store={store}>
81
+ <MyComponent />
82
+ </I18nTypedStoreProvider>
83
+ );
84
+ }
85
+ ```
86
+
87
+ ### Use Translations in Components
88
+
89
+ ```tsx
90
+ // MyComponent.tsx
91
+ import { useI18nTranslation, useI18nLocale } from 'i18n-typed-store-react';
92
+ import { TRANSLATIONS, LOCALES } from './constants';
93
+ import type { ITranslationStoreTypes } from './store';
94
+
95
+ function MyComponent() {
96
+ const translations = useI18nTranslation<typeof TRANSLATIONS, typeof LOCALES, ITranslationStoreTypes, 'common'>('common');
97
+ const { locale, setLocale } = useI18nLocale<typeof TRANSLATIONS, typeof LOCALES, ITranslationStoreTypes>();
98
+
99
+ if (!translations) {
100
+ return <div>Loading...</div>;
101
+ }
102
+
103
+ return (
104
+ <div>
105
+ <h1>{translations.title}</h1>
106
+ <p>{translations.greeting}</p>
107
+ <button onClick={() => setLocale('ru')}>Switch to Russian</button>
108
+ </div>
109
+ );
110
+ }
111
+ ```
112
+
113
+ ### Creating Typed Hook Wrappers (Recommended)
114
+
115
+ For better type safety and cleaner code, create typed wrapper hooks:
116
+
117
+ ```typescript
118
+ // hooks/useTranslation.ts
119
+ import { useI18nTranslation } from 'i18n-typed-store-react/useI18nTranslation';
120
+ import type { TRANSLATIONS, LOCALES } from '../constants';
121
+ import type { ITranslationStoreTypes } from '../store';
122
+
123
+ export const useTranslation = <K extends keyof typeof TRANSLATIONS>(translation: K) => {
124
+ return useI18nTranslation<typeof TRANSLATIONS, typeof LOCALES, ITranslationStoreTypes, K>(translation);
125
+ };
126
+ ```
127
+
128
+ ```typescript
129
+ // hooks/useTranslationLazy.ts
130
+ import { useI18nTranslationLazy } from 'i18n-typed-store-react/useI18nTranslationLazy';
131
+ import type { TRANSLATIONS, LOCALES } from '../constants';
132
+ import type { ITranslationStoreTypes } from '../store';
133
+
134
+ export const useTranslationLazy = <K extends keyof typeof TRANSLATIONS>(translation: K) => {
135
+ return useI18nTranslationLazy<typeof TRANSLATIONS, typeof LOCALES, ITranslationStoreTypes, K>(translation);
136
+ };
137
+ ```
138
+
139
+ Now you can use them with full type inference:
140
+
141
+ ```tsx
142
+ // MyComponent.tsx
143
+ import { useTranslation } from './hooks/useTranslation';
144
+ import { useI18nLocale } from 'i18n-typed-store-react';
145
+
146
+ function MyComponent() {
147
+ const translations = useTranslation('common');
148
+ const { locale, setLocale } = useI18nLocale();
149
+
150
+ if (!translations) {
151
+ return <div>Loading...</div>;
152
+ }
153
+
154
+ return (
155
+ <div>
156
+ <h1>{translations.title}</h1>
157
+ <p>{translations.greeting}</p>
158
+ <button onClick={() => setLocale('ru')}>Switch to Russian</button>
159
+ </div>
160
+ );
161
+ }
162
+ ```
163
+
164
+ ## React Suspense Support
165
+
166
+ Use `useI18nTranslationLazy` with React Suspense for automatic loading states:
167
+
168
+ ```tsx
169
+ // MyComponent.tsx
170
+ import { Suspense } from 'react';
171
+ import { useTranslationLazy } from './hooks/useTranslationLazy';
172
+
173
+ function MyComponent() {
174
+ // This hook throws a promise if translation is not loaded (for Suspense)
175
+ const translations = useTranslationLazy('common');
176
+
177
+ return (
178
+ <div>
179
+ <h1>{translations.title}</h1>
180
+ <p>{translations.greeting}</p>
181
+ </div>
182
+ );
183
+ }
184
+ ```
185
+
186
+ ```tsx
187
+ // App.tsx
188
+ import { Suspense } from 'react';
189
+ import { I18nTypedStoreProvider } from 'i18n-typed-store-react';
190
+ import { store } from './store';
191
+ import { MyComponent } from './MyComponent';
192
+
193
+ function App() {
194
+ return (
195
+ <I18nTypedStoreProvider store={store} suspenseMode="first-load-locale">
196
+ <Suspense fallback={<div>Loading translations...</div>}>
197
+ <MyComponent />
198
+ </Suspense>
199
+ </I18nTypedStoreProvider>
200
+ );
201
+ }
202
+ ```
203
+
204
+ ## API Reference
205
+
206
+ ### `I18nTypedStoreProvider`
207
+
208
+ Provider component that wraps your application to provide translation store context.
209
+
210
+ ```tsx
211
+ <I18nTypedStoreProvider store={store} suspenseMode="first-load-locale">
212
+ {children}
213
+ </I18nTypedStoreProvider>
214
+ ```
215
+
216
+ **Props:**
217
+
218
+ - `store` - Translation store instance (created with `createTranslationStore`)
219
+ - `suspenseMode` - Suspense mode: `'once'` | `'first-load-locale'` | `'change-locale'` (default: `'first-load-locale'`)
220
+ - `'once'` - Suspense only on first load
221
+ - `'first-load-locale'` - Suspense on first load for each locale
222
+ - `'change-locale'` - Suspense on every locale change
223
+ - `children` - React children
224
+
225
+ ### `useI18nTranslation`
226
+
227
+ Hook for accessing translations with automatic loading. Returns `undefined` if translation is not yet loaded.
228
+
229
+ ```tsx
230
+ // Direct usage
231
+ const translations = useI18nTranslation<
232
+ typeof TRANSLATIONS,
233
+ typeof LOCALES,
234
+ ITranslationStoreTypes,
235
+ 'common'
236
+ >('common', fromCache?: boolean);
237
+
238
+ // Typed wrapper (recommended)
239
+ import { useI18nTranslation } from 'i18n-typed-store-react/useI18nTranslation';
240
+ import type { TRANSLATIONS, LOCALES } from './constants';
241
+ import type { ITranslationStoreTypes } from './store';
242
+
243
+ export const useTranslation = <K extends keyof typeof TRANSLATIONS>(translation: K) => {
244
+ return useI18nTranslation<typeof TRANSLATIONS, typeof LOCALES, ITranslationStoreTypes, K>(translation);
245
+ };
246
+
247
+ // Usage
248
+ const translations = useTranslation('common');
249
+ if (translations) {
250
+ console.log(translations.greeting);
251
+ }
252
+ ```
253
+
254
+ **Parameters:**
255
+
256
+ - `namespace` - Namespace key to load translations for
257
+ - `fromCache` - Whether to use cached translation if available (default: `true`)
258
+
259
+ **Returns:** Translation object for the specified namespace, or `undefined` if not loaded
260
+
261
+ ### `useI18nTranslationLazy`
262
+
263
+ Hook for accessing translations with React Suspense support. Throws a promise if translation is not loaded.
264
+
265
+ ```tsx
266
+ // Direct usage
267
+ const translations = useI18nTranslationLazy<
268
+ typeof TRANSLATIONS,
269
+ typeof LOCALES,
270
+ ITranslationStoreTypes,
271
+ 'common'
272
+ >('common', fromCache?: boolean);
273
+
274
+ // Typed wrapper (recommended)
275
+ import { useI18nTranslationLazy } from 'i18n-typed-store-react/useI18nTranslationLazy';
276
+ import type { TRANSLATIONS, LOCALES } from './constants';
277
+ import type { ITranslationStoreTypes } from './store';
278
+
279
+ export const useTranslationLazy = <K extends keyof typeof TRANSLATIONS>(translation: K) => {
280
+ return useI18nTranslationLazy<typeof TRANSLATIONS, typeof LOCALES, ITranslationStoreTypes, K>(translation);
281
+ };
282
+
283
+ // Usage
284
+ function MyComponent() {
285
+ const translations = useTranslationLazy('common');
286
+ return <div>{translations.greeting}</div>;
287
+ }
288
+ ```
289
+
290
+ **Parameters:**
291
+
292
+ - `namespace` - Namespace key to load translations for
293
+ - `fromCache` - Whether to use cached translation if available (default: `true`)
294
+
295
+ **Returns:** Translation object for the specified namespace (never `undefined`)
296
+
297
+ **Throws:** Promise if translation is not yet loaded (for React Suspense)
298
+
299
+ ### `useI18nLocale`
300
+
301
+ Hook for accessing and managing the current locale. Supports SSR/SSG by using `useSyncExternalStore`.
302
+
303
+ ```tsx
304
+ const { locale, setLocale } = useI18nLocale<typeof TRANSLATIONS, typeof LOCALES, ITranslationStoreTypes>();
305
+ ```
306
+
307
+ **Returns:**
308
+
309
+ - `locale` - Current locale key
310
+ - `setLocale` - Function to change the current locale
311
+
312
+ **Example:**
313
+
314
+ ```tsx
315
+ function LocaleSwitcher() {
316
+ const { locale, setLocale } = useI18nLocale();
317
+
318
+ return (
319
+ <select value={locale} onChange={(e) => setLocale(e.target.value as keyof typeof LOCALES)}>
320
+ <option value="en">English</option>
321
+ <option value="ru">Русский</option>
322
+ </select>
323
+ );
324
+ }
325
+ ```
326
+
327
+ ### `Safe`
328
+
329
+ Component that safely extracts strings from translation objects, catching errors.
330
+
331
+ ```tsx
332
+ <Safe errorComponent={<span>N/A</span>} errorHandler={(error) => console.error(error)}>
333
+ {() => translations.common.pages.main.title}
334
+ </Safe>
335
+ ```
336
+
337
+ **Props:**
338
+
339
+ - `children` - Function that returns a string (called during render)
340
+ - `errorComponent` - Component to display if an error occurs (default: empty string)
341
+ - `errorHandler` - Optional error handler callback
342
+
343
+ ## SSR/SSG Support
344
+
345
+ ### Next.js Pages Router
346
+
347
+ ```typescript
348
+ // pages/_app.tsx
349
+ import { I18nTypedStoreProvider } from 'i18n-typed-store-react';
350
+ import { storeFactory } from '../lib/i18n';
351
+ import type { AppProps } from 'next/app';
352
+
353
+ const store = storeFactory.type<TranslationData>();
354
+
355
+ function MyApp({ Component, pageProps }: AppProps) {
356
+ return (
357
+ <I18nTypedStoreProvider store={store}>
358
+ <Component {...pageProps} />
359
+ </I18nTypedStoreProvider>
360
+ );
361
+ }
362
+
363
+ export default MyApp;
364
+ ```
365
+
366
+ ```typescript
367
+ // pages/index.tsx
368
+ import type { GetServerSidePropsContext } from 'next';
369
+ import { getLocaleFromRequest, initializeStore } from 'i18n-typed-store-react';
370
+ import { storeFactory } from '../lib/i18n';
371
+ import type { TranslationData } from '../lib/i18n';
372
+
373
+ export async function getServerSideProps(context: GetServerSidePropsContext) {
374
+ const locale = getLocaleFromRequest(context, {
375
+ defaultLocale: 'en',
376
+ availableLocales: ['en', 'ru'],
377
+ cookieName: 'locale',
378
+ queryParamName: 'locale',
379
+ });
380
+
381
+ const store = storeFactory.type<TranslationData>();
382
+ initializeStore(store, locale);
383
+
384
+ // Preload translations if needed
385
+ await store.translations.common.load(locale);
386
+
387
+ return {
388
+ props: {
389
+ locale,
390
+ },
391
+ };
392
+ }
393
+ ```
394
+
395
+ ### Next.js App Router
396
+
397
+ ```typescript
398
+ // app/layout.tsx
399
+ import { I18nTypedStoreProvider } from 'i18n-typed-store-react';
400
+ import { storeFactory } from '../lib/i18n';
401
+ import type { TranslationData } from '../lib/i18n';
402
+
403
+ const store = storeFactory.type<TranslationData>();
404
+
405
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
406
+ return (
407
+ <html>
408
+ <body>
409
+ <I18nTypedStoreProvider store={store}>
410
+ {children}
411
+ </I18nTypedStoreProvider>
412
+ </body>
413
+ </html>
414
+ );
415
+ }
416
+ ```
417
+
418
+ ```typescript
419
+ // app/page.tsx
420
+ import { getLocaleFromRequest, initializeStore } from 'i18n-typed-store-react';
421
+ import { storeFactory } from '../lib/i18n';
422
+ import type { TranslationData } from '../lib/i18n';
423
+ import { headers, cookies } from 'next/headers';
424
+
425
+ export default async function Page() {
426
+ const headersList = await headers();
427
+ const cookieStore = await cookies();
428
+
429
+ const locale = getLocaleFromRequest(
430
+ {
431
+ headers: Object.fromEntries(headersList),
432
+ cookies: Object.fromEntries(cookieStore),
433
+ },
434
+ {
435
+ defaultLocale: 'en',
436
+ availableLocales: ['en', 'ru'],
437
+ }
438
+ );
439
+
440
+ const store = storeFactory.type<TranslationData>();
441
+ initializeStore(store, locale);
442
+ await store.translations.common.load(locale);
443
+
444
+ return <div>...</div>;
445
+ }
446
+ ```
447
+
448
+ ### SSR API
449
+
450
+ #### `getLocaleFromRequest`
451
+
452
+ Gets locale from SSR request context (query params, cookies, headers).
453
+
454
+ ```typescript
455
+ function getLocaleFromRequest<L extends Record<string, string>>(context: RequestContext, options: GetLocaleFromRequestOptions): keyof L;
456
+ ```
457
+
458
+ **Parameters:**
459
+
460
+ - `context` - Request context with `query`, `cookies`, and `headers`
461
+ - `options` - Options object:
462
+ - `defaultLocale` - Default locale to use if locale cannot be determined
463
+ - `availableLocales` - Array of available locale keys for validation
464
+ - `headerName` - Header name to read locale from (default: `'accept-language'`)
465
+ - `cookieName` - Cookie name to read locale from
466
+ - `queryParamName` - Query parameter name to read locale from (default: `'locale'`)
467
+ - `parseAcceptLanguage` - Whether to parse Accept-Language header (default: `true`)
468
+
469
+ **Example:**
470
+
471
+ ```typescript
472
+ const locale = getLocaleFromRequest(context, {
473
+ defaultLocale: 'en',
474
+ availableLocales: ['en', 'ru'],
475
+ cookieName: 'locale',
476
+ queryParamName: 'locale',
477
+ headerName: 'accept-language',
478
+ parseAcceptLanguage: true,
479
+ });
480
+ ```
481
+
482
+ #### `initializeStore`
483
+
484
+ Initializes translation store with a specific locale for SSR.
485
+
486
+ ```typescript
487
+ function initializeStore<N, L, M>(store: TranslationStore<N, L, M>, locale: keyof L): void;
488
+ ```
489
+
490
+ **Parameters:**
491
+
492
+ - `store` - Translation store instance
493
+ - `locale` - Locale to initialize with
494
+
495
+ **Example:**
496
+
497
+ ```typescript
498
+ const locale = getLocaleFromRequest(context, {
499
+ defaultLocale: 'en',
500
+ availableLocales: ['en', 'ru'],
501
+ });
502
+
503
+ const store = storeFactory.type<TranslationData>();
504
+ initializeStore(store, locale);
505
+ ```
506
+
507
+ ## Complete Example
508
+
509
+ ```typescript
510
+ // constants.ts
511
+ export const TRANSLATIONS = {
512
+ common: 'common',
513
+ errors: 'errors',
514
+ } as const;
515
+
516
+ export const LOCALES = {
517
+ en: 'en',
518
+ ru: 'ru',
519
+ } as const;
520
+ ```
521
+
522
+ ```typescript
523
+ // translations/common/en.tsx
524
+ import { createPluralSelector } from 'i18n-typed-store';
525
+
526
+ const plur = createPluralSelector('en');
527
+
528
+ export default class CommonTranslationsEn {
529
+ title = 'Welcome';
530
+ greeting = 'Hello, World!';
531
+
532
+ buttons = {
533
+ save: 'Save',
534
+ cancel: 'Cancel',
535
+ };
536
+
537
+ items = (count: number) =>
538
+ count +
539
+ ' ' +
540
+ plur(count, {
541
+ one: 'item',
542
+ other: 'items',
543
+ });
544
+ }
545
+ ```
546
+
547
+ ```typescript
548
+ // store.ts
549
+ import { createTranslationStore } from 'i18n-typed-store';
550
+ import type CommonTranslationsEn from './translations/common/en';
551
+ import { TRANSLATIONS, LOCALES } from './constants';
552
+
553
+ export interface ITranslationStoreTypes extends Record<keyof typeof TRANSLATIONS, any> {
554
+ common: CommonTranslationsEn;
555
+ }
556
+
557
+ export const store = createTranslationStore({
558
+ namespaces: TRANSLATIONS,
559
+ locales: LOCALES,
560
+ loadModule: async (locale, namespace) => {
561
+ return await import(`./translations/${namespace}/${locale}.tsx`);
562
+ },
563
+ extractTranslation: (module) => new module.default(),
564
+ defaultLocale: 'en',
565
+ }).type<ITranslationStoreTypes>();
566
+ ```
567
+
568
+ ```typescript
569
+ // hooks/useTranslation.ts
570
+ import { useI18nTranslation } from 'i18n-typed-store-react/useI18nTranslation';
571
+ import type { TRANSLATIONS, LOCALES } from '../constants';
572
+ import type { ITranslationStoreTypes } from '../store';
573
+
574
+ export const useTranslation = <K extends keyof typeof TRANSLATIONS>(translation: K) => {
575
+ return useI18nTranslation<typeof TRANSLATIONS, typeof LOCALES, ITranslationStoreTypes, K>(translation);
576
+ };
577
+ ```
578
+
579
+ ```tsx
580
+ // App.tsx
581
+ import { I18nTypedStoreProvider } from 'i18n-typed-store-react';
582
+ import { store } from './store';
583
+ import { MyComponent } from './MyComponent';
584
+
585
+ function App() {
586
+ return (
587
+ <I18nTypedStoreProvider store={store}>
588
+ <MyComponent />
589
+ </I18nTypedStoreProvider>
590
+ );
591
+ }
592
+
593
+ export default App;
594
+ ```
595
+
596
+ ```tsx
597
+ // MyComponent.tsx
598
+ import { useTranslation } from './hooks/useTranslation';
599
+ import { useI18nLocale } from 'i18n-typed-store-react';
600
+
601
+ function MyComponent() {
602
+ const translations = useTranslation('common');
603
+ const { locale, setLocale } = useI18nLocale();
604
+
605
+ if (!translations) {
606
+ return <div>Loading...</div>;
607
+ }
608
+
609
+ return (
610
+ <div>
611
+ <h1>{translations.title}</h1>
612
+ <p>{translations.greeting}</p>
613
+ <p>{translations.items(5)}</p>
614
+ <button onClick={() => setLocale(locale === 'en' ? 'ru' : 'en')}>Switch to {locale === 'en' ? 'Russian' : 'English'}</button>
615
+ </div>
616
+ );
617
+ }
618
+ ```
619
+
620
+ ## Type Safety
621
+
622
+ All hooks and components are fully type-safe:
623
+
624
+ ```tsx
625
+ // ✅ TypeScript knows all available translation keys
626
+ const translations = useTranslation('common');
627
+ if (translations) {
628
+ const title = translations.title; // ✅ Type-safe
629
+ const greeting = translations.greeting; // ✅ Type-safe
630
+ }
631
+
632
+ // ❌ TypeScript error: 'invalidKey' doesn't exist
633
+ // const invalid = translations.invalidKey;
634
+
635
+ // ✅ TypeScript knows all available locales
636
+ const { locale, setLocale } = useI18nLocale();
637
+ setLocale('en'); // ✅ Type-safe
638
+ setLocale('ru'); // ✅ Type-safe
639
+
640
+ // ❌ TypeScript error: 'fr' is not a valid locale
641
+ // setLocale('fr');
642
+ ```
643
+
644
+ ## Contributing
645
+
646
+ Contributions are welcome! Please feel free to submit a Pull Request.
647
+
648
+ ## License
649
+
650
+ MIT
651
+
652
+ ## Author
653
+
654
+ Alexander Lvov
655
+
656
+ ## Related
657
+
658
+ - [i18n-typed-store](https://github.com/ialexanderlvov/i18n-typed-store) - Core library
659
+ - [React Example](https://github.com/ialexanderlvov/i18n-typed-store-react-example) - Complete working example with React, TypeScript, and all features demonstrated
660
+
661
+ ## Repository
662
+
663
+ [GitHub](https://github.com/ialexanderlvov/i18n-typed-store)