i18n Implementation
Purpose
Practical guide for adding internationalization to web applications, with a focus on Next.js using next-i18next. Covers setup, translation management, language switching, and advanced formatting patterns.
When to Use This Skill
- Adding multi-language support to a Next.js application
- Setting up translation file structure and naming conventions
- Implementing a language switcher UI component
- Supporting right-to-left (RTL) languages
- Formatting dates, numbers, and currencies per locale
- Managing translations with a team or translation service
Quick Start
New i18n Project Checklist
- Install
next-i18nextandreact-i18next - Create
next-i18next.config.jsat project root - Update
next.config.jsto load i18n config - Create
public/locales/{locale}/{namespace}.jsonfiles - Wrap
_app.tsxwithappWithTranslation - Use
serverSideTranslationsingetStaticProps/getServerSideProps - Import
useTranslationhook in components - Add language switcher to layout
Minimal Setup
npm install next-i18next react-i18next i18next
// next-i18next.config.js
module.exports = {
i18n: {
defaultLocale: "en",
locales: ["en", "es"],
},
};
// next.config.js
const { i18n } = require("./next-i18next.config");
module.exports = { i18n };
Translation File Structure
public/
locales/
en/
common.json # Shared across pages (nav, buttons, errors)
home.json # Home page strings
auth.json # Login, register
es/
common.json
home.json
auth.json
Namespace Strategy
| Namespace | Contents |
|---|---|
common |
Navigation, buttons, global errors |
home |
Landing page, hero section |
auth |
Login, register, forgot password |
dashboard |
Dashboard-specific strings |
errors |
Error page messages |
Translation Key Naming
Use dot notation for grouping; keep flat within namespace files.
// en/common.json
{
"nav": {
"home": "Home",
"about": "About",
"contact": "Contact"
},
"button": {
"save": "Save",
"cancel": "Cancel",
"delete": "Delete"
},
"error": {
"required": "This field is required",
"notFound": "Page not found"
}
}
Naming rules:
- Use camelCase for keys:
firstName, notfirst_name - Group by UI area, not by component:
nav.home, notNavbar.homeLink - Keep key names semantic:
button.save, notbutton.green
Using Translations in Components
Basic Usage
import { useTranslation } from "next-i18next";
export const Navbar: React.FC = () => {
const { t } = useTranslation("common");
return (
<nav>
<a href="/">{t("nav.home")}</a>
<a href="/about">{t("nav.about")}</a>
</nav>
);
};
Multiple Namespaces
const { t: tCommon } = useTranslation("common");
const { t: tDashboard } = useTranslation("dashboard");
Server-Side Translation Loading
// pages/index.tsx
import { GetStaticProps } from "next";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
export const getStaticProps: GetStaticProps = async ({ locale }) => ({
props: {
...(await serverSideTranslations(locale ?? "en", ["common", "home"])),
},
});
Language Switcher Component
// components/LanguageSwitcher.tsx
import { useRouter } from "next/router";
const LANGUAGES = [
{ code: "en", label: "English" },
{ code: "es", label: "Español" },
];
export const LanguageSwitcher: React.FC = () => {
const router = useRouter();
const { pathname, asPath, query, locale } = router;
const switchLanguage = (lang: string) => {
router.push({ pathname, query }, asPath, { locale: lang });
};
return (
<select
value={locale}
=> switchLanguage(e.target.value)}
aria-label="Select language"
>
{LANGUAGES.map(({ code, label }) => (
<option key={code} value={code}>
{label}
</option>
))}
</select>
);
};
RTL Support
Right-to-left languages (Arabic, Hebrew, Persian) require document direction changes.
Detecting RTL
// lib/rtl.ts
const RTL_LOCALES = ["ar", "he", "fa", "ur"];
export const isRTL = (locale: string): boolean => RTL_LOCALES.includes(locale);
Applying Direction in _app.tsx
import { isRTL } from "../lib/rtl";
function MyApp({ Component, pageProps, router }: AppProps) {
const dir = isRTL(router.locale ?? "en") ? "rtl" : "ltr";
return (
<div dir={dir} lang={router.locale}>
<Component {...pageProps} />
</div>
);
}
CSS Logical Properties (RTL-safe)
Use logical properties instead of left/right for automatic RTL support:
/* Instead of: margin-left, padding-right */
margin-inline-start: 1rem; /* left in LTR, right in RTL */
padding-inline-end: 0.5rem; /* right in LTR, left in RTL */
Date, Number, and Currency Formatting
Use the browser's built-in Intl API for locale-aware formatting.
Dates
const formatDate = (date: Date, locale: string): string =>
new Intl.DateTimeFormat(locale, {
year: "numeric",
month: "long",
day: "numeric",
}).format(date);
// Usage
formatDate(new Date(), "en"); // "February 23, 2026"
formatDate(new Date(), "es"); // "23 de febrero de 2026"
Numbers
const formatNumber = (value: number, locale: string): string =>
new Intl.NumberFormat(locale).format(value);
formatNumber(1234567.89, "en"); // "1,234,567.89"
formatNumber(1234567.89, "es"); // "1.234.567,89"
Currency
const formatCurrency = (
amount: number,
locale: string,
currency = "USD",
): string =>
new Intl.NumberFormat(locale, {
style: "currency",
currency,
}).format(amount);
formatCurrency(9.99, "en", "USD"); // "$9.99"
formatCurrency(9.99, "es", "EUR"); // "9,99 €"
Middleware for Locale Routing
Next.js middleware can detect and redirect to the correct locale.
// middleware.ts
import { NextRequest, NextResponse } from "next/server";
const SUPPORTED_LOCALES = ["en", "es"];
const DEFAULT_LOCALE = "en";
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Skip if already has locale prefix
const hasLocale = SUPPORTED_LOCALES.some(
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`,
);
if (hasLocale) return NextResponse.next();
// Detect from Accept-Language header
const acceptLang = request.headers.get("Accept-Language") ?? "";
const preferred = acceptLang.split(",")[0]?.split("-")[0] ?? DEFAULT_LOCALE;
const locale = SUPPORTED_LOCALES.includes(preferred)
? preferred
: DEFAULT_LOCALE;
request.nextUrl.pathname = `/${locale}${pathname}`;
return NextResponse.redirect(request.nextUrl);
}
export const config = {
matcher: ["/((?!_next|api|favicon.ico).*)"],
};
Topic Guides
Advanced Translation Patterns
- Pluralization rules
- Interpolation with variables
- Context-based translations
- Lazy loading namespaces
(See inline content above for patterns)
Translation Workflow
- Managing translation files across locales
- Key extraction tools
- Translation services (Crowdin, Phrase)
- QA and testing
Complete Guide: references/translation-workflow.md
Working Example
- Full Next.js app with EN and ES translations
- Language switcher
- Middleware routing
Example: assets/examples/nextjs-i18n/
Navigation Guide
| Need to... | Read this |
|---|---|
| Set up next-i18next | Quick Start section above |
| Structure translation files | Translation File Structure section above |
| Add a language switcher | Language Switcher Component section above |
| Support Arabic/Hebrew/RTL | RTL Support section above |
| Format dates or currencies | Date, Number, and Currency Formatting above |
| Pluralization and interpolation | Advanced Translation Patterns section above |
| Manage translations with a team | translation-workflow.md |
| See a working Next.js example | assets/examples/nextjs-i18n/ |
Core Principles
- Namespaces by domain: Split translations into focused namespaces to control load size
- Keys are semantic: Name keys by meaning, not by position or style
- Never hardcode text: Every user-visible string lives in a translation file
- Intl API for formatting: Use browser-native
Intlfor dates, numbers, and currency - Logical CSS properties: Use
margin-inline-startovermargin-leftfor RTL safety - Load only what you need: Pass only required namespaces to
serverSideTranslations
Related Skills
- frontend-dev-guidelines: React/Next.js component patterns that consume translations
- app-builder: Full-stack app scaffolding that can integrate i18n from the start
Source: ollieb89/viflo — distributed by TomeVault.