next-i18n-lite 1.0.3 → 2.0.1

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