next-i18n-lite 1.0.4 → 2.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/README.md +178 -52
- package/package.json +7 -2
- package/scripts/scaffoldI18n.mjs +169 -0
- package/scripts/setup-i18n.mjs +26 -0
package/README.md
CHANGED
|
@@ -4,6 +4,7 @@ next-i18n-lite
|
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
+
- ✅ auto-configure the i18n library using scaffold
|
|
7
8
|
- ✅ TypeScript support with full type safety
|
|
8
9
|
- ✅ React Context API integration
|
|
9
10
|
- ✅ Automatic RTL support for Arabic.
|
|
@@ -25,9 +26,21 @@ yarn add next-i18n-lite
|
|
|
25
26
|
pnpm add next-i18n-lite
|
|
26
27
|
```
|
|
27
28
|
|
|
29
|
+
## Scaffold Prompt
|
|
30
|
+
```bash
|
|
31
|
+
npx setup-i18n
|
|
32
|
+
```
|
|
33
|
+
When you install the library, you will see a prompt like this in your terminal:
|
|
34
|
+
|
|
35
|
+
⚡ **Prompt:** Do you want to auto-configure the i18n library using scaffold? (Y/n)
|
|
36
|
+
|
|
37
|
+
✅ Press Enter (default) → scaffold runs
|
|
38
|
+
❌ Type 'n' → skip
|
|
39
|
+
|
|
40
|
+
##### Note: it won't effect on the existed files!
|
|
28
41
|
## Quick Start
|
|
29
42
|
|
|
30
|
-
## 📁
|
|
43
|
+
## 📁 Folder Structure
|
|
31
44
|
|
|
32
45
|
Create the following files **inside the `app/` directory** (recommended):
|
|
33
46
|
|
|
@@ -37,6 +50,10 @@ Create the following files **inside the `app/` directory** (recommended):
|
|
|
37
50
|
- Location: `app/lib/locales/`
|
|
38
51
|
- Files:
|
|
39
52
|
- `en.ts` – English translations
|
|
53
|
+
- `es.ts` – Spanish translations
|
|
54
|
+
- `fr.ts` – French translations
|
|
55
|
+
- `pt.ts` – Portuguese translations
|
|
56
|
+
- `de.ts` – German translations
|
|
40
57
|
- `ar.ts` – Arabic translations
|
|
41
58
|
|
|
42
59
|
2. **I18nBoundary.tsx**
|
|
@@ -49,7 +66,7 @@ Create the following files **inside the `app/` directory** (recommended):
|
|
|
49
66
|
|
|
50
67
|
|
|
51
68
|
|
|
52
|
-
that's all
|
|
69
|
+
##### ✅that's all
|
|
53
70
|
---
|
|
54
71
|
|
|
55
72
|
## 🗂 Directory Tree
|
|
@@ -68,13 +85,18 @@ app/
|
|
|
68
85
|
├─ I18nBoundary.tsx # Client-only i18n wrapper
|
|
69
86
|
└─ locales/
|
|
70
87
|
├─ en.ts # English translations
|
|
88
|
+
├─ es.ts # Spanish translations
|
|
89
|
+
├─ fr.ts # French translations
|
|
90
|
+
├─ pt.ts # Portuguese translations
|
|
91
|
+
├─ de.ts # German translations
|
|
71
92
|
└─ ar.ts # Arabic translations
|
|
72
93
|
```
|
|
73
94
|
### 1. Create translation files
|
|
74
95
|
|
|
75
|
-
```
|
|
96
|
+
```ts
|
|
76
97
|
// lib/locales/en.ts
|
|
77
98
|
export const en = {
|
|
99
|
+
home:'Home',
|
|
78
100
|
common: {
|
|
79
101
|
welcome: 'Welcome',
|
|
80
102
|
loading: 'Loading...',
|
|
@@ -84,7 +106,62 @@ export const en = {
|
|
|
84
106
|
price: 'Price: ${amount}',
|
|
85
107
|
},
|
|
86
108
|
};
|
|
109
|
+
```
|
|
110
|
+
```ts
|
|
111
|
+
// lib/locales/es.ts
|
|
112
|
+
export const es = {
|
|
113
|
+
home:'Home',
|
|
114
|
+
common: {
|
|
115
|
+
welcome: "Bienvenido",
|
|
116
|
+
loading: "Cargando...",
|
|
117
|
+
},
|
|
118
|
+
product: {
|
|
119
|
+
addToCart: "Agregar al carrito",
|
|
120
|
+
price: "Precio: ${amount}",
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
```
|
|
124
|
+
```ts
|
|
125
|
+
// lib/locales/fr.ts
|
|
126
|
+
export const fr = {
|
|
127
|
+
common: {
|
|
128
|
+
welcome: "Bienvenue",
|
|
129
|
+
loading: "Chargement...",
|
|
130
|
+
},
|
|
131
|
+
product: {
|
|
132
|
+
addToCart: "Ajouter au panier",
|
|
133
|
+
price: "Prix : ${amount}",
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
```
|
|
137
|
+
```ts
|
|
138
|
+
// lib/locales/pt.ts
|
|
139
|
+
export const pt = {
|
|
140
|
+
common: {
|
|
141
|
+
welcome: "Bem-vindo",
|
|
142
|
+
loading: "Carregando...",
|
|
143
|
+
},
|
|
144
|
+
product: {
|
|
145
|
+
addToCart: "Adicionar ao carrinho",
|
|
146
|
+
price: "Preço: ${amount}",
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
```
|
|
150
|
+
```ts
|
|
151
|
+
// lib/locales/de.ts
|
|
152
|
+
export const de = {
|
|
153
|
+
common: {
|
|
154
|
+
welcome: "Willkommen",
|
|
155
|
+
loading: "Wird geladen...",
|
|
156
|
+
},
|
|
157
|
+
product: {
|
|
158
|
+
addToCart: "In den Warenkorb",
|
|
159
|
+
price: "Preis: ${amount}",
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
```
|
|
87
163
|
|
|
164
|
+
```ts
|
|
88
165
|
// lib/locales/ar.ts
|
|
89
166
|
export const ar = {
|
|
90
167
|
common: {
|
|
@@ -97,39 +174,49 @@ export const ar = {
|
|
|
97
174
|
},
|
|
98
175
|
};
|
|
99
176
|
```
|
|
100
|
-
|
|
101
|
-
```typescript
|
|
102
|
-
export const en = {
|
|
103
|
-
welcome: 'Welcome',
|
|
104
|
-
loading: 'Loading...',
|
|
105
|
-
}
|
|
106
|
-
```
|
|
177
|
+
|
|
107
178
|
### 2. Setup Provider (Next.js App Router)
|
|
108
179
|
|
|
109
180
|
#### I18nBoundary.tsx
|
|
110
|
-
```
|
|
111
|
-
// lib/I18nBoundary.tsx
|
|
181
|
+
```ts
|
|
182
|
+
// app/lib/I18nBoundary.tsx
|
|
112
183
|
'use client';
|
|
113
|
-
|
|
114
184
|
import { ReactNode, useEffect, useState } from 'react';
|
|
115
185
|
import { I18nProvider } from 'next-i18n-lite/react';
|
|
116
|
-
|
|
117
186
|
import { en } from './locales/en';
|
|
187
|
+
import { es } from './locales/es';
|
|
188
|
+
import { fr } from './locales/fr';
|
|
189
|
+
import { pt } from './locales/pt';
|
|
190
|
+
import { de } from './locales/de';
|
|
118
191
|
import { ar } from './locales/ar';
|
|
119
192
|
|
|
120
|
-
const translations = { en, ar };
|
|
193
|
+
const translations = { en, ar, es, fr, pt, de };
|
|
194
|
+
|
|
195
|
+
// Supporting Arabic
|
|
196
|
+
const RTL_LOCALES = new Set(['ar']);
|
|
197
|
+
|
|
198
|
+
function getDirection(locale: string) {
|
|
199
|
+
return RTL_LOCALES.has(locale) ? 'rtl' : 'ltr';
|
|
200
|
+
}
|
|
121
201
|
|
|
122
202
|
export function I18nBoundary({ children }: { children: ReactNode }) {
|
|
123
|
-
const [locale, setLocale] = useState<
|
|
203
|
+
const [locale, setLocale] = useState<string | null>(null);
|
|
124
204
|
|
|
125
205
|
useEffect(() => {
|
|
126
|
-
const saved = localStorage.getItem('locale');
|
|
127
|
-
|
|
206
|
+
const saved = localStorage.getItem('locale') || 'en';
|
|
207
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
208
|
+
setLocale(saved);
|
|
209
|
+
|
|
210
|
+
// handle document direction here (ONCE)
|
|
211
|
+
document.documentElement.lang = saved;
|
|
212
|
+
document.documentElement.dir = getDirection(saved);
|
|
128
213
|
}, []);
|
|
129
214
|
|
|
215
|
+
// hydration-safe gate
|
|
130
216
|
if (!locale) {
|
|
131
217
|
return null;
|
|
132
218
|
}
|
|
219
|
+
|
|
133
220
|
return (
|
|
134
221
|
<I18nProvider translations={translations} defaultLocale={locale}>
|
|
135
222
|
{children}
|
|
@@ -143,11 +230,11 @@ still may face issue for setLocale
|
|
|
143
230
|
|
|
144
231
|
Fix: not actually fixed yet but if you don't like red flags in code just
|
|
145
232
|
|
|
146
|
-
```
|
|
233
|
+
```ts
|
|
147
234
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
148
235
|
```
|
|
149
236
|
yup! ignore it :D
|
|
150
|
-
```
|
|
237
|
+
```ts
|
|
151
238
|
// app/layout.tsx
|
|
152
239
|
import { I18nBoundary } from "./lib/I18nBoundary";
|
|
153
240
|
|
|
@@ -170,59 +257,56 @@ also i provided 2 options:
|
|
|
170
257
|
- Choice 2:Hover dropdown list
|
|
171
258
|
|
|
172
259
|
you can ignore styling & lucide-react library both only for example ^^
|
|
173
|
-
```
|
|
260
|
+
```ts
|
|
261
|
+
// components/i18n/LocaleSwitcher.tsx
|
|
174
262
|
'use client';
|
|
263
|
+
|
|
175
264
|
import { Globe } from "lucide-react";
|
|
176
265
|
import { useI18n } from "next-i18n-lite/react";
|
|
177
|
-
import { useEffect
|
|
266
|
+
import { useEffect } from "react";
|
|
178
267
|
|
|
179
268
|
const languages = [
|
|
180
269
|
{ code: 'en', name: 'English' },
|
|
270
|
+
{ code: 'es', name: 'Español' },
|
|
271
|
+
{ code: 'fr', name: 'Français' },
|
|
272
|
+
{ code: 'pt', name: 'Português' },
|
|
273
|
+
{ code: 'de', name: 'Deutsch' },
|
|
181
274
|
{ code: 'ar', name: 'العربية' },
|
|
182
275
|
];
|
|
183
276
|
|
|
184
277
|
export function LocaleSwitcher() {
|
|
185
278
|
const { locale, setLocale, isRTL } = useI18n();
|
|
186
|
-
const [mounted, setMounted] = useState(false);
|
|
187
|
-
|
|
188
|
-
useEffect(() => {
|
|
189
|
-
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
190
|
-
setMounted(true);
|
|
191
|
-
}, []);
|
|
192
279
|
|
|
193
|
-
|
|
280
|
+
const dropdownPosition = isRTL ? 'left-0' : 'right-0';
|
|
194
281
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
setLocale(persist);
|
|
199
|
-
localStorage.setItem('locale', persist); // persist choice
|
|
282
|
+
const changeLocale = (code: string) => {
|
|
283
|
+
setLocale(code);
|
|
284
|
+
localStorage.setItem('locale', code);
|
|
200
285
|
};
|
|
201
286
|
|
|
202
|
-
//
|
|
203
|
-
|
|
287
|
+
// sync document direction (just to make sure eveything in running well in rtl direction)
|
|
288
|
+
useEffect(() => {
|
|
289
|
+
document.documentElement.lang = locale;
|
|
290
|
+
document.documentElement.dir = locale === 'ar' ? 'rtl' : 'ltr';
|
|
291
|
+
}, [locale]);
|
|
204
292
|
|
|
205
293
|
return (
|
|
206
294
|
<div className="relative group inline-block">
|
|
207
|
-
|
|
208
|
-
<Globe color="white" onClick={toggleLocale} className="top-5 "/>
|
|
295
|
+
<Globe color="white" />
|
|
209
296
|
|
|
210
|
-
{/* Choice 2:Hover dropdown list */}
|
|
211
297
|
<div
|
|
212
|
-
className={`absolute ${dropdownPosition} mt-2 bg-card border border-border rounded-xl shadow-lg
|
|
298
|
+
className={`absolute ${dropdownPosition} mt-2 bg-card border border-border rounded-xl shadow-lg
|
|
299
|
+
opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all z-50`}
|
|
213
300
|
>
|
|
214
301
|
{languages.map((lang) => (
|
|
215
302
|
<button
|
|
216
303
|
key={lang.code}
|
|
217
|
-
onClick={() =>
|
|
218
|
-
|
|
219
|
-
localStorage.setItem('locale', lang.code);
|
|
220
|
-
}}
|
|
221
|
-
className={`w-full flex items-center gap-3 px-4 py-3 transition-colors first:rounded-t-xl last:rounded-b-xl
|
|
304
|
+
onClick={() => changeLocale(lang.code)}
|
|
305
|
+
className={`w-full flex items-center gap-3 px-4 py-3 transition-colors
|
|
222
306
|
${locale === lang.code ? 'bg-primary/10 text-primary' : 'hover:bg-gray-600'}
|
|
223
307
|
`}
|
|
224
308
|
>
|
|
225
|
-
|
|
309
|
+
{lang.name}
|
|
226
310
|
</button>
|
|
227
311
|
))}
|
|
228
312
|
</div>
|
|
@@ -230,7 +314,7 @@ export function LocaleSwitcher() {
|
|
|
230
314
|
);
|
|
231
315
|
}
|
|
232
316
|
```
|
|
233
|
-
### 4. Use in
|
|
317
|
+
### 4. Use in Header Component
|
|
234
318
|
### `useI18n()`
|
|
235
319
|
const { t, locale, setLocale, isRTL } = useI18n();
|
|
236
320
|
|
|
@@ -239,13 +323,21 @@ Returns:
|
|
|
239
323
|
- `locale` - Current locale
|
|
240
324
|
- `setLocale(locale)` - Change locale
|
|
241
325
|
- `isRTL` - Boolean indicating RTL direction
|
|
242
|
-
```
|
|
326
|
+
```ts
|
|
243
327
|
'use client';
|
|
244
328
|
import { useI18n } from "next-i18n-lite/react";
|
|
245
329
|
|
|
246
|
-
export function
|
|
330
|
+
export function Header() {
|
|
247
331
|
const { t } = useI18n();
|
|
248
|
-
|
|
332
|
+
const [mounted, setMounted] = useState(false);
|
|
333
|
+
// To avoid hydration mismatch
|
|
334
|
+
if (typeof window !== "undefined" && !mounted) {
|
|
335
|
+
setMounted(true);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (!mounted) {
|
|
339
|
+
return <div className="w-10 h-10" />;
|
|
340
|
+
}
|
|
249
341
|
return (
|
|
250
342
|
<div>
|
|
251
343
|
<h1>{t('common.welcome')}</h1>
|
|
@@ -254,7 +346,41 @@ export function page() {
|
|
|
254
346
|
);
|
|
255
347
|
}
|
|
256
348
|
```
|
|
257
|
-
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
## Formatters
|
|
352
|
+
|
|
353
|
+
library also provides helper function to
|
|
354
|
+
- **handle pluralization**
|
|
355
|
+
according to the current locale.
|
|
356
|
+
---
|
|
357
|
+
|
|
358
|
+
```ts
|
|
359
|
+
import { pluralize } from "next-i18n-lite"
|
|
360
|
+
export default function Cart() {
|
|
361
|
+
const { t } = useI18n();
|
|
362
|
+
const items = 1 // 2 try it out
|
|
363
|
+
return (
|
|
364
|
+
<p>{pluralize(items, t('unit'), t('units'))}</p>
|
|
365
|
+
)
|
|
366
|
+
};
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### Upcoming Features
|
|
370
|
+
- **format dates,**
|
|
371
|
+
`formatDate(date: Date, locale: string): string`
|
|
372
|
+
- **format numbers,**
|
|
373
|
+
`formatNumber(num: number, locale: string): string`
|
|
374
|
+
- **format currencies,**
|
|
375
|
+
`formatCurrency(amount: number, locale: string, currency = 'USD'): string`
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
import {
|
|
379
|
+
formatDate,
|
|
380
|
+
formatNumber,
|
|
381
|
+
formatCurrency
|
|
382
|
+
} from "next-i18n-lite"
|
|
383
|
+
```
|
|
258
384
|
## API Reference
|
|
259
385
|
|
|
260
386
|
### `useI18n()`
|
|
@@ -276,4 +402,4 @@ Returns:
|
|
|
276
402
|
codex410@gmail.com
|
|
277
403
|
## License
|
|
278
404
|
|
|
279
|
-
MIT ©
|
|
405
|
+
MIT © Islam Abozeed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "next-i18n-lite",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Lightweight i18n library for Next.js with TypeScript support",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -19,15 +19,20 @@
|
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
|
21
21
|
"dist",
|
|
22
|
+
"scripts",
|
|
22
23
|
"README.md",
|
|
23
24
|
"LICENSE"
|
|
24
25
|
],
|
|
25
26
|
"scripts": {
|
|
26
27
|
"build": "tsup",
|
|
27
28
|
"dev": "tsup --watch",
|
|
29
|
+
"postinstall": "node -e \"console.log('✨ next-i18n-lite installed! Run: npx setup-i18n')\"",
|
|
28
30
|
"prepublishOnly": "npm run build",
|
|
29
31
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
30
32
|
},
|
|
33
|
+
"bin": {
|
|
34
|
+
"setup-i18n": "./scripts/setup-i18n.mjs"
|
|
35
|
+
},
|
|
31
36
|
"keywords": [
|
|
32
37
|
"i18n",
|
|
33
38
|
"internationalization",
|
|
@@ -37,7 +42,7 @@
|
|
|
37
42
|
"localization",
|
|
38
43
|
"translation"
|
|
39
44
|
],
|
|
40
|
-
"author": "codexpro410 <
|
|
45
|
+
"author": "codexpro410 <codex410@gmail.com>",
|
|
41
46
|
"license": "MIT",
|
|
42
47
|
"repository": {
|
|
43
48
|
"type": "git",
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export function scaffoldI18n() {
|
|
5
|
+
// ----------------------
|
|
6
|
+
// 1️⃣ Locales Scaffold
|
|
7
|
+
// ----------------------
|
|
8
|
+
const locales = {
|
|
9
|
+
en: {
|
|
10
|
+
home: 'Home',
|
|
11
|
+
common: { welcome: 'Welcome', loading: 'Loading...' },
|
|
12
|
+
product: { addToCart: 'Add to Cart', price: 'Price: ${amount}' },
|
|
13
|
+
},
|
|
14
|
+
es: {
|
|
15
|
+
home: 'Inicio',
|
|
16
|
+
common: { welcome: 'Bienvenido', loading: 'Cargando...' },
|
|
17
|
+
product: { addToCart: 'Agregar al carrito', price: 'Precio: ${amount}' },
|
|
18
|
+
},
|
|
19
|
+
fr: {
|
|
20
|
+
home: 'Accueil',
|
|
21
|
+
common: { welcome: 'Bienvenue', loading: 'Chargement...' },
|
|
22
|
+
product: { addToCart: 'Ajouter au panier', price: 'Prix: ${amount}' },
|
|
23
|
+
},
|
|
24
|
+
pt: {
|
|
25
|
+
home: 'Início',
|
|
26
|
+
common: { welcome: 'Bem-vindo', loading: 'Carregando...' },
|
|
27
|
+
product: { addToCart: 'Adicionar ao carrinho', price: 'Preço: ${amount}' },
|
|
28
|
+
},
|
|
29
|
+
de: {
|
|
30
|
+
home: 'Startseite',
|
|
31
|
+
common: { welcome: 'Willkommen', loading: 'Wird geladen...' },
|
|
32
|
+
product: { addToCart: 'In den Warenkorb', price: 'Preis: ${amount}' },
|
|
33
|
+
},
|
|
34
|
+
ar: {
|
|
35
|
+
home: 'الصفحة الرئيسية',
|
|
36
|
+
common: { welcome: 'مرحباً', loading: 'جاري التحميل...' },
|
|
37
|
+
product: { addToCart: 'أضف إلى السلة', price: 'السعر: ${amount}' },
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const localesDir = path.resolve('./app/lib/locales');
|
|
42
|
+
if (!fs.existsSync(localesDir)) fs.mkdirSync(localesDir, { recursive: true });
|
|
43
|
+
|
|
44
|
+
for (const [code, content] of Object.entries(locales)) {
|
|
45
|
+
const filePath = path.join(localesDir, `${code}.ts`);
|
|
46
|
+
if (!fs.existsSync(filePath)) {
|
|
47
|
+
const fileContent = `export const ${code} = ${JSON.stringify(content, null, 2)};\n`;
|
|
48
|
+
fs.writeFileSync(filePath, fileContent, 'utf-8');
|
|
49
|
+
console.log(`Created locale: ${filePath}`);
|
|
50
|
+
} else {
|
|
51
|
+
console.log(`Locale exists: ${filePath}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ----------------------
|
|
56
|
+
// 2️⃣ I18nBoundary Scaffold
|
|
57
|
+
// ----------------------
|
|
58
|
+
const i18nBoundaryPath = path.resolve('./app/lib/I18nBoundary.tsx');
|
|
59
|
+
if (!fs.existsSync(i18nBoundaryPath)) {
|
|
60
|
+
const i18nBoundaryContent = `
|
|
61
|
+
// app/lib/I18nBoundary.tsx
|
|
62
|
+
'use client';
|
|
63
|
+
import { ReactNode, useEffect, useState } from 'react';
|
|
64
|
+
import { I18nProvider } from 'next-i18n-lite/react';
|
|
65
|
+
import { en } from './locales/en';
|
|
66
|
+
import { es } from './locales/es';
|
|
67
|
+
import { fr } from './locales/fr';
|
|
68
|
+
import { pt } from './locales/pt';
|
|
69
|
+
import { de } from './locales/de';
|
|
70
|
+
import { ar } from './locales/ar';
|
|
71
|
+
|
|
72
|
+
const translations = { en, ar, es, fr, pt, de };
|
|
73
|
+
const RTL_LOCALES = new Set(['ar']);
|
|
74
|
+
|
|
75
|
+
function getDirection(locale: string) {
|
|
76
|
+
return RTL_LOCALES.has(locale) ? 'rtl' : 'ltr';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function I18nBoundary({ children }: { children: ReactNode }) {
|
|
80
|
+
const [locale, setLocale] = useState<string | null>(null);
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
const saved = localStorage.getItem('locale') || 'en';
|
|
84
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
85
|
+
setLocale(saved);
|
|
86
|
+
document.documentElement.lang = saved;
|
|
87
|
+
document.documentElement.dir = getDirection(saved);
|
|
88
|
+
}, []);
|
|
89
|
+
|
|
90
|
+
if (!locale) return null;
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<I18nProvider translations={translations} defaultLocale={locale}>
|
|
94
|
+
{children}
|
|
95
|
+
</I18nProvider>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
`.trim();
|
|
99
|
+
fs.writeFileSync(i18nBoundaryPath, i18nBoundaryContent, 'utf-8');
|
|
100
|
+
console.log(`Created: ${i18nBoundaryPath}`);
|
|
101
|
+
} else {
|
|
102
|
+
console.log(`Exists: ${i18nBoundaryPath}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ----------------------
|
|
106
|
+
// 3️⃣ LocaleSwitcher Scaffold
|
|
107
|
+
// ----------------------
|
|
108
|
+
const localeSwitcherPath = path.resolve('./app/components/i18n/LocaleSwitcher.tsx');
|
|
109
|
+
const localeSwitcherDir = path.dirname(localeSwitcherPath);
|
|
110
|
+
if (!fs.existsSync(localeSwitcherDir)) fs.mkdirSync(localeSwitcherDir, { recursive: true });
|
|
111
|
+
|
|
112
|
+
if (!fs.existsSync(localeSwitcherPath)) {
|
|
113
|
+
const localeSwitcherContent = `
|
|
114
|
+
// components/i18n/LocaleSwitcher.tsx
|
|
115
|
+
'use client';
|
|
116
|
+
import { Globe } from "lucide-react";
|
|
117
|
+
import { useI18n } from "next-i18n-lite/react";
|
|
118
|
+
import { useEffect } from "react";
|
|
119
|
+
|
|
120
|
+
const languages = [
|
|
121
|
+
{ code: 'en', name: 'English' },
|
|
122
|
+
{ code: 'es', name: 'Español' },
|
|
123
|
+
{ code: 'fr', name: 'Français' },
|
|
124
|
+
{ code: 'pt', name: 'Português' },
|
|
125
|
+
{ code: 'de', name: 'Deutsch' },
|
|
126
|
+
{ code: 'ar', name: 'العربية' },
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
export function LocaleSwitcher() {
|
|
130
|
+
const { locale, setLocale, isRTL } = useI18n();
|
|
131
|
+
const dropdownPosition = isRTL ? 'left-0' : 'right-0';
|
|
132
|
+
|
|
133
|
+
const changeLocale = (code: string) => {
|
|
134
|
+
setLocale(code);
|
|
135
|
+
localStorage.setItem('locale', code);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// sync document direction
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
document.documentElement.lang = locale;
|
|
141
|
+
document.documentElement.dir = locale === 'ar' ? 'rtl' : 'ltr';
|
|
142
|
+
}, [locale]);
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<div className="relative group inline-block">
|
|
146
|
+
<Globe color="white" />
|
|
147
|
+
<div className={\`absolute \${dropdownPosition} mt-2 bg-card border border-border rounded-xl shadow-lg
|
|
148
|
+
opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all z-50\`}>
|
|
149
|
+
{languages.map((lang) => (
|
|
150
|
+
<button
|
|
151
|
+
key={lang.code}
|
|
152
|
+
onClick={() => changeLocale(lang.code)}
|
|
153
|
+
className={\`w-full flex items-center gap-3 px-4 py-3 transition-colors
|
|
154
|
+
\${locale === lang.code ? 'bg-primary/10 text-primary' : 'hover:bg-gray-600'}\`}
|
|
155
|
+
>
|
|
156
|
+
{lang.name}
|
|
157
|
+
</button>
|
|
158
|
+
))}
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
`.trim();
|
|
164
|
+
fs.writeFileSync(localeSwitcherPath, localeSwitcherContent, 'utf-8');
|
|
165
|
+
console.log(`Created: ${localeSwitcherPath}`);
|
|
166
|
+
} else {
|
|
167
|
+
console.log(`Exists: ${localeSwitcherPath}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import readline from 'readline';
|
|
3
|
+
import { scaffoldI18n } from './scaffoldI18n.mjs';
|
|
4
|
+
|
|
5
|
+
const rl = readline.createInterface({
|
|
6
|
+
input: process.stdin,
|
|
7
|
+
output: process.stdout,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
// Default = YES → user must type N
|
|
11
|
+
rl.question(
|
|
12
|
+
'🌍 Do you want to auto-configure the i18n library using scaffold? (Y/n) ',
|
|
13
|
+
(answer) => {
|
|
14
|
+
const normalized = answer.trim().toLowerCase();
|
|
15
|
+
|
|
16
|
+
if (normalized === 'n') {
|
|
17
|
+
console.log('⏭️ Scaffold skipped.');
|
|
18
|
+
} else {
|
|
19
|
+
console.log('⚙️ Running scaffold...');
|
|
20
|
+
scaffoldI18n();
|
|
21
|
+
console.log('✅ Scaffold completed!');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
rl.close();
|
|
25
|
+
}
|
|
26
|
+
);
|