i18n
Rules
- Next.js App Router: use
next-intl — useTranslations() hook, NextIntlClientProvider, middleware for locale detection
- React SPA: use
react-i18next — useTranslation() hook, i18n.init() with i18next-http-backend for lazy loading
- Locale-aware routing:
/en/about, /fr/about — use middleware to detect Accept-Language header and redirect
- Translation files: JSON per locale per namespace —
messages/en/common.json, messages/fr/common.json
- Namespace splitting: one namespace per feature/page —
auth.json, dashboard.json, settings.json — load only what the page needs
- Pluralization: use ICU message format —
{count, plural, one {# item} other {# items}}
- Interpolation:
t('greeting', { name }) — never concatenate translated strings
- Date/number/currency: use
Intl.DateTimeFormat, Intl.NumberFormat — never hardcode formats
- RTL support: use
dir="rtl" on <html>, CSS logical properties (margin-inline-start not margin-left), test with Arabic/Hebrew
Patterns
import createMiddleware from "next-intl/middleware";
export default createMiddleware({
locales: ["en", "fr", "de", "ar"],
defaultLocale: "en",
});
import { useTranslations } from "next-intl";
function ProductCard({ price, date }: { price: number; date: Date }) {
const t = useTranslations("products");
const format = useFormatter();
return (
<div>
<h2>{t("title")}</h2>
<p>{format.number(price, { style: "currency", currency: "USD" })}</p>
<p>{format.dateTime(date, { dateStyle: "medium" })}</p>
</div>
);
}
Avoid
- Hardcoding user-facing strings — every string must go through
t()
- String concatenation for translated text — use interpolation or ICU format
- Loading all locales upfront — lazy-load per locale and namespace
- CSS
left/right for layout — use logical properties for RTL compatibility
- Storing locale in localStorage only — use URL-based locale for SEO and shareability