next-i18n-lite
v2.1.0
Published
Lightweight i18n library for Next.js with TypeScript support
Maintainers
Readme
next-i18n-lite
🌍 Lightweight internationalization (i18n) library for Next.js with TypeScript support.
Features
- ✅ auto-configure the i18n library using scaffold
- ✅ TypeScript support with full type safety
- ✅ React Context API integration
- ✅ Automatic RTL support for Arabic.
- ✅ Nested translation keys with dot notation
- ✅ Dynamic parameter replacement
- ✅ localStorage persistence
- ✅ Browser language detection
- ✅ Zero dependencies (except React peer deps)
- ✅ Tree-shakeable
- ✅ Works with Next.js 13+ App Router
Installation
npm install next-i18n-lite
# or
yarn add next-i18n-lite
# or
pnpm add next-i18n-liteScaffold Prompt
npx setup-i18nWhen you install the library, you will see a prompt like this in your terminal:
⚡ Prompt: Do you want to auto-configure the i18n library using scaffold? (Y/n)
✅ Press Enter (default) → scaffold runs
❌ Type 'n' → skip
Note: it won't effect on the existed files!
Quick Start
📁 Folder Structure
Create the following files inside the app/ directory (recommended):
Required Files
Language files
- Location:
app/lib/locales/ - Files:
en.ts– English translationses.ts– Spanish translationsfr.ts– French translationspt.ts– Portuguese translationsde.ts– German translationsar.ts– Arabic translations
- Location:
I18nBoundary.tsx
- Location:
app/lib/ - Purpose: Client-only i18n wrapper
- Location:
LocaleSwitcher.tsx
- Location:
app/components/i18n/ - Purpose: Language switch button
- Location:
✅that's all
🗂 Directory Tree
app/
├─ layout.tsx # Root layout
├─ globals.css # Global styles
├─ Header.tsx # Header (nav + LocaleSwitcher)
│
├─ components/
│ └─ i18n/
│ └─ LocaleSwitcher.tsx # Language switcher button
│
└─ lib/
├─ I18nBoundary.tsx # Client-only i18n wrapper
└─ locales/
├─ en.ts # English translations
├─ es.ts # Spanish translations
├─ fr.ts # French translations
├─ pt.ts # Portuguese translations
├─ de.ts # German translations
└─ ar.ts # Arabic translations1. Create translation files
// lib/locales/en.ts
export const en = {
home:'Home',
common: {
welcome: 'Welcome',
loading: 'Loading...',
},
product: {
addToCart: 'Add to Cart',
price: 'Price: ${amount}',
},
};// lib/locales/es.ts
export const es = {
home:'Home',
common: {
welcome: "Bienvenido",
loading: "Cargando...",
},
product: {
addToCart: "Agregar al carrito",
price: "Precio: ${amount}",
},
};// lib/locales/fr.ts
export const fr = {
common: {
welcome: "Bienvenue",
loading: "Chargement...",
},
product: {
addToCart: "Ajouter au panier",
price: "Prix : ${amount}",
},
};// lib/locales/pt.ts
export const pt = {
common: {
welcome: "Bem-vindo",
loading: "Carregando...",
},
product: {
addToCart: "Adicionar ao carrinho",
price: "Preço: ${amount}",
},
};// lib/locales/de.ts
export const de = {
common: {
welcome: "Willkommen",
loading: "Wird geladen...",
},
product: {
addToCart: "In den Warenkorb",
price: "Preis: ${amount}",
},
};// lib/locales/ar.ts
export const ar = {
common: {
welcome: 'مرحباً',
loading: 'جاري التحميل...',
},
product: {
addToCart: 'أضف إلى السلة',
price: 'السعر: ${amount}',
},
};2. Setup Provider (Next.js App Router)
I18nBoundary.tsx
// app/lib/I18nBoundary.tsx
'use client';
import { ReactNode, useEffect, useState } from 'react';
import { I18nProvider } from 'next-i18n-lite/react';
import { en } from './locales/en';
import { es } from './locales/es';
import { fr } from './locales/fr';
import { pt } from './locales/pt';
import { de } from './locales/de';
import { ar } from './locales/ar';
const translations = { en, ar, es, fr, pt, de };
// Supporting Arabic
const RTL_LOCALES = new Set(['ar']);
function getDirection(locale: string) {
return RTL_LOCALES.has(locale) ? 'rtl' : 'ltr';
}
export function I18nBoundary({ children }: { children: ReactNode }) {
const [locale, setLocale] = useState<string | null>(null);
useEffect(() => {
const saved = localStorage.getItem('locale') || 'en';
// eslint-disable-next-line react-hooks/set-state-in-effect
setLocale(saved);
// handle document direction here (ONCE)
document.documentElement.lang = saved;
document.documentElement.dir = getDirection(saved);
}, []);
// hydration-safe gate
if (!locale) {
return null;
}
return (
<I18nProvider translations={translations} defaultLocale={locale}>
{children}
</I18nProvider>
);
}just created it not to fall in 'use client' in layout.tsx while you cannot use it with Metadata and similar issues ^^ still may face issue for setLocale
- Error: Calling setState synchronously within an effect can trigger cascading renders
Fix: not actually fixed yet but if you don't like red flags in code just
// eslint-disable-next-line react-hooks/set-state-in-effectyup! ignore it :D
// app/layout.tsx
import { I18nBoundary } from "./lib/I18nBoundary";
export default function RootLayout({ children }) {
return (
<html>
<body>
<I18nBoundary>
{children}
</I18nBoundary>
</body>
</html>
);
}3. Create LocaleSwitcher.tsx
/components/i18n/LocaleSwitcher.tsx (recommended) also i provided 2 options:
- Choice 1: Single-click toggle button
- Choice 2:Hover dropdown list
you can ignore styling & lucide-react library both only for example ^^
// components/i18n/LocaleSwitcher.tsx
'use client';
import { Globe } from "lucide-react";
import { useI18n } from "next-i18n-lite/react";
import { useEffect } from "react";
const languages = [
{ code: 'en', name: 'English' },
{ code: 'es', name: 'Español' },
{ code: 'fr', name: 'Français' },
{ code: 'pt', name: 'Português' },
{ code: 'de', name: 'Deutsch' },
{ code: 'ar', name: 'العربية' },
];
export function LocaleSwitcher() {
const { locale, setLocale, isRTL } = useI18n();
const dropdownPosition = isRTL ? 'left-0' : 'right-0';
const changeLocale = (code: string) => {
setLocale(code);
localStorage.setItem('locale', code);
};
// sync document direction (just to make sure eveything in running well in rtl direction)
useEffect(() => {
document.documentElement.lang = locale;
document.documentElement.dir = locale === 'ar' ? 'rtl' : 'ltr';
}, [locale]);
return (
<div className="relative group inline-block">
<Globe color="white" />
<div
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`}
>
{languages.map((lang) => (
<button
key={lang.code}
onClick={() => changeLocale(lang.code)}
className={`w-full flex items-center gap-3 px-4 py-3 transition-colors
${locale === lang.code ? 'bg-primary/10 text-primary' : 'hover:bg-gray-600'}
`}
>
{lang.name}
</button>
))}
</div>
</div>
);
}4. Use in Header Component
useI18n()
const { t, locale, setLocale, isRTL } = useI18n();
Returns:
t(key, params?)- Translation functionlocale- Current localesetLocale(locale)- Change localeisRTL- Boolean indicating RTL direction
'use client';
import { useI18n } from "next-i18n-lite/react";
export function Header() {
const { t } = useI18n();
const [mounted, setMounted] = useState(false);
// To avoid hydration mismatch
if (typeof window !== "undefined" && !mounted) {
setMounted(true);
}
if (!mounted) {
return <div className="w-10 h-10" />;
}
return (
<div>
<h1>{t('common.welcome')}</h1>
<p>{t('product.price', { amount: '99' })}</p>
</div>
);
}Formatters
library also provides helper function to
- handle pluralization according to the current locale.
import { pluralize } from "next-i18n-lite"
export default function Cart() {
const { t } = useI18n();
const items = 1 // 2 try it out
return (
<p>{pluralize(items, t('unit'), t('units'))}</p>
)
};Upcoming Features
- format dates,
formatDate(date: Date, locale: string): string - format numbers,
formatNumber(num: number, locale: string): string - format currencies,
formatCurrency(amount: number, locale: string, currency = 'USD'): string
import {
formatDate,
formatNumber,
formatCurrency
} from "next-i18n-lite"API Reference
useI18n()
Returns:
t(key, params?)- Translation functionlocale- Current localesetLocale(locale)- Change localeisRTL- Boolean indicating RTL direction
Compatibility
- ✅ Node.js >= 18
- ✅ React >= 18
- ✅ Next.js >= 13
- ✅ TypeScript >= 5
contact
License
MIT © Islam Abozeed
