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 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
- ## 📁 File Structure
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 :D
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
- ```typescript
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
- - ✅ feel free to deal with language object:
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
- ```typescript
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<'en' | 'ar' | null>(null);
203
+ const [locale, setLocale] = useState<string | null>(null);
124
204
 
125
205
  useEffect(() => {
126
- const saved = localStorage.getItem('locale');
127
- setLocale(saved === 'ar' ? 'ar' : 'en');
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
- ```typescript
233
+ ```ts
147
234
  // eslint-disable-next-line react-hooks/set-state-in-effect
148
235
  ```
149
236
  yup! ignore it :D
150
- ```typescript
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
- ```typescript
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, useState } from "react";
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
- if (!mounted) return null;
280
+ const dropdownPosition = isRTL ? 'left-0' : 'right-0';
194
281
 
195
- // Toggle between languages on single click
196
- const toggleLocale = () => {
197
- const persist = locale === 'en' ? 'ar' : 'en';
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
- // Positioning
203
- const dropdownPosition = isRTL ? 'left-0' : 'right-0';
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
- {/* Choice 1: Single-click toggle button */}
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 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all z-50`}
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
- setLocale(lang.code);
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
- <span className="text-sm font-medium">{lang.name}</span>
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 Components
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
- ```typescript
326
+ ```ts
243
327
  'use client';
244
328
  import { useI18n } from "next-i18n-lite/react";
245
329
 
246
- export function page() {
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
- and that's it!!!
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 © IslamAbozeed
405
+ MIT © Islam Abozeed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-i18n-lite",
3
- "version": "1.0.4",
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 <codexpro410@gmail.com>",
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
+ );