next-i18n-lite 1.0.4 → 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,9 +26,18 @@ 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
40
+ ## 📁 Folder Structure
31
41
 
32
42
  Create the following files **inside the `app/` directory** (recommended):
33
43
 
@@ -37,6 +47,10 @@ Create the following files **inside the `app/` directory** (recommended):
37
47
  - Location: `app/lib/locales/`
38
48
  - Files:
39
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
40
54
  - `ar.ts` – Arabic translations
41
55
 
42
56
  2. **I18nBoundary.tsx**
@@ -49,7 +63,7 @@ Create the following files **inside the `app/` directory** (recommended):
49
63
 
50
64
 
51
65
 
52
- that's all :D
66
+ ##### ✅that's all
53
67
  ---
54
68
 
55
69
  ## 🗂 Directory Tree
@@ -68,13 +82,18 @@ app/
68
82
  ├─ I18nBoundary.tsx # Client-only i18n wrapper
69
83
  └─ locales/
70
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
71
89
  └─ ar.ts # Arabic translations
72
90
  ```
73
91
  ### 1. Create translation files
74
92
 
75
- ```typescript
93
+ ```bash
76
94
  // lib/locales/en.ts
77
95
  export const en = {
96
+ home:'Home',
78
97
  common: {
79
98
  welcome: 'Welcome',
80
99
  loading: 'Loading...',
@@ -84,7 +103,62 @@ export const en = {
84
103
  price: 'Price: ${amount}',
85
104
  },
86
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
+ ```
87
160
 
161
+ ```bash
88
162
  // lib/locales/ar.ts
89
163
  export const ar = {
90
164
  common: {
@@ -97,39 +171,49 @@ export const ar = {
97
171
  },
98
172
  };
99
173
  ```
100
- - ✅ feel free to deal with language object:
101
- ```typescript
102
- export const en = {
103
- welcome: 'Welcome',
104
- loading: 'Loading...',
105
- }
106
- ```
174
+
107
175
  ### 2. Setup Provider (Next.js App Router)
108
176
 
109
177
  #### I18nBoundary.tsx
110
- ```typescript
111
- // lib/I18nBoundary.tsx
178
+ ```bash
179
+ // app/lib/I18nBoundary.tsx
112
180
  'use client';
113
-
114
181
  import { ReactNode, useEffect, useState } from 'react';
115
182
  import { I18nProvider } from 'next-i18n-lite/react';
116
-
117
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';
118
188
  import { ar } from './locales/ar';
119
189
 
120
- 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
+ }
121
198
 
122
199
  export function I18nBoundary({ children }: { children: ReactNode }) {
123
- const [locale, setLocale] = useState<'en' | 'ar' | null>(null);
200
+ const [locale, setLocale] = useState<string | null>(null);
124
201
 
125
202
  useEffect(() => {
126
- const saved = localStorage.getItem('locale');
127
- 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);
128
210
  }, []);
129
211
 
212
+ // hydration-safe gate
130
213
  if (!locale) {
131
214
  return null;
132
215
  }
216
+
133
217
  return (
134
218
  <I18nProvider translations={translations} defaultLocale={locale}>
135
219
  {children}
@@ -143,11 +227,11 @@ still may face issue for setLocale
143
227
 
144
228
  Fix: not actually fixed yet but if you don't like red flags in code just
145
229
 
146
- ```typescript
230
+ ```bash
147
231
  // eslint-disable-next-line react-hooks/set-state-in-effect
148
232
  ```
149
233
  yup! ignore it :D
150
- ```typescript
234
+ ```bash
151
235
  // app/layout.tsx
152
236
  import { I18nBoundary } from "./lib/I18nBoundary";
153
237
 
@@ -170,59 +254,56 @@ also i provided 2 options:
170
254
  - Choice 2:Hover dropdown list
171
255
 
172
256
  you can ignore styling & lucide-react library both only for example ^^
173
- ```typescript
257
+ ```bash
258
+ // components/i18n/LocaleSwitcher.tsx
174
259
  'use client';
260
+
175
261
  import { Globe } from "lucide-react";
176
262
  import { useI18n } from "next-i18n-lite/react";
177
- import { useEffect, useState } from "react";
263
+ import { useEffect } from "react";
178
264
 
179
265
  const languages = [
180
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' },
181
271
  { code: 'ar', name: 'العربية' },
182
272
  ];
183
273
 
184
274
  export function LocaleSwitcher() {
185
275
  const { locale, setLocale, isRTL } = useI18n();
186
- const [mounted, setMounted] = useState(false);
187
276
 
188
- useEffect(() => {
189
- // eslint-disable-next-line react-hooks/set-state-in-effect
190
- setMounted(true);
191
- }, []);
192
-
193
- if (!mounted) return null;
277
+ const dropdownPosition = isRTL ? 'left-0' : 'right-0';
194
278
 
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
279
+ const changeLocale = (code: string) => {
280
+ setLocale(code);
281
+ localStorage.setItem('locale', code);
200
282
  };
201
283
 
202
- // Positioning
203
- 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]);
204
289
 
205
290
  return (
206
291
  <div className="relative group inline-block">
207
- {/* Choice 1: Single-click toggle button */}
208
- <Globe color="white" onClick={toggleLocale} className="top-5 "/>
292
+ <Globe color="white" />
209
293
 
210
- {/* Choice 2:Hover dropdown list */}
211
294
  <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`}
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`}
213
297
  >
214
298
  {languages.map((lang) => (
215
299
  <button
216
300
  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
301
+ onClick={() => changeLocale(lang.code)}
302
+ className={`w-full flex items-center gap-3 px-4 py-3 transition-colors
222
303
  ${locale === lang.code ? 'bg-primary/10 text-primary' : 'hover:bg-gray-600'}
223
304
  `}
224
305
  >
225
- <span className="text-sm font-medium">{lang.name}</span>
306
+ {lang.name}
226
307
  </button>
227
308
  ))}
228
309
  </div>
@@ -230,7 +311,7 @@ export function LocaleSwitcher() {
230
311
  );
231
312
  }
232
313
  ```
233
- ### 4. Use in Components
314
+ ### 4. Use in Header Component
234
315
  ### `useI18n()`
235
316
  const { t, locale, setLocale, isRTL } = useI18n();
236
317
 
@@ -239,13 +320,21 @@ Returns:
239
320
  - `locale` - Current locale
240
321
  - `setLocale(locale)` - Change locale
241
322
  - `isRTL` - Boolean indicating RTL direction
242
- ```typescript
323
+ ```bash
243
324
  'use client';
244
325
  import { useI18n } from "next-i18n-lite/react";
245
326
 
246
- export function page() {
327
+ export function Header() {
247
328
  const { t } = useI18n();
248
-
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
+ }
249
338
  return (
250
339
  <div>
251
340
  <h1>{t('common.welcome')}</h1>
@@ -254,7 +343,41 @@ export function page() {
254
343
  );
255
344
  }
256
345
  ```
257
- 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
+ ```
258
381
  ## API Reference
259
382
 
260
383
  ### `useI18n()`
@@ -276,4 +399,4 @@ Returns:
276
399
  codex410@gmail.com
277
400
  ## License
278
401
 
279
- 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.4",
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
+ }