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 +19 -0
- package/README.md +663 -0
- package/dist/index.d.mts +324 -0
- package/dist/index.d.ts +324 -0
- package/dist/index.js +231 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +221 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +59 -0
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)
|