Internationalization (i18n) Guide
Quick Reference for RFC 1003: next-intl Internationalization System
This guide provides practical examples for implementing multi-language support in the arolariu.ro frontend.
Supported Languages: English (en), Romanian (ro), French (fr)
Quick Start
1. Add Translation Keys
Add keys to all translation files: messages/en.json, messages/ro.json, and messages/fr.json:
Tip: Run
npm run generate:i18nafter adding keys toen.jsonto automatically detect missing keys in other locales.
{
"MyPage": {
"__metadata__": {
"title": "My Page Title",
"description": "SEO description for my page"
},
"heading": "Welcome to My Page",
"description": "This page displays {itemCount} items",
"actions": {
"save": "Save Changes",
"cancel": "Cancel"
}
}
}
Key Rules:
- Use
__metadata__for SEO/metadata translations - Nest keys logically by feature/component
- Use variables with
{variableName}syntax
2. Use Translations in Server Components
import {getTranslations} from "next-intl/server";
export default async function MyPage() {
const t = await getTranslations("MyPage");
return (
<div>
<h1>{t("heading")}</h1>
<p>{t("description", {itemCount: 42})}</p>
<button>{t("actions.save")}</button>
</div>
);
}
3. Use Translations in Client Components
"use client";
import {useTranslations} from "next-intl";
export function MyComponent() {
const t = useTranslations("MyPage");
return (
<div>
<h1>{t("heading")}</h1>
<button>{t("actions.save")}</button>
</div>
);
}
Common Patterns
Server Component with Metadata
import {createMetadata} from "@/metadata";
import {getLocale, getTranslations} from "next-intl/server";
import type {Metadata} from "next";
export async function generateMetadata(): Promise<Metadata> {
const t = await getTranslations("MyPage.__metadata__");
const locale = await getLocale();
return createMetadata({
locale,
title: t("title"),
description: t("description"),
});
}
export default async function MyPage() {
const t = await getTranslations("MyPage");
return <h1>{t("heading")}</h1>;
}
Client Component with Multiple Namespaces
"use client";
import {useTranslations} from "next-intl";
export function InvoiceCard() {
const tInvoice = useTranslations("Invoices");
const tCommon = useTranslations("Common");
return (
<div>
<h2>{tInvoice("title")}</h2>
<button>{tCommon("actions.delete")}</button>
</div>
);
}
Pluralization
{
"items": "{count, plural, =0 {No items} =1 {One item} other {# items}}"
}
const t = useTranslations("Namespace");
t("items", {count: 0}); // "No items"
t("items", {count: 1}); // "One item"
t("items", {count: 42}); // "42 items"
Rich Text with Components
import {useTranslations} from "next-intl";
import Link from "next/link";
export function Message() {
const t = useTranslations("Messages");
return (
<p>
{t.rich("learnMore", {
link: (chunks) => <Link href="/learn">{chunks}</Link>,
bold: (chunks) => <strong>{chunks}</strong>,
})}
</p>
);
}
Translation file:
{
"Messages": {
"learnMore": "Visit our <link><bold>learning center</bold></link> for tutorials"
}
}
Date and Number Formatting
import {useFormatter} from "next-intl";
export function FormattedData() {
const format = useFormatter();
const date = new Date();
const number = 1234.56;
return (
<div>
<p>{format.dateTime(date, {dateStyle: "full"})}</p>
<p>{format.number(number, {style: "currency", currency: "USD"})}</p>
</div>
);
}
Language Switching
Get Current Locale
import {getLocale} from "next-intl/server";
export default async function Page() {
const locale = await getLocale(); // "en", "ro", or "fr"
return <div>Current language: {locale}</div>;
}
Switch Language (Client Component)
"use client";
import {setCookie} from "@/lib/actions/cookies";
import {useRouter} from "next/navigation";
export function LanguageSwitcher() {
const router = useRouter();
const switchToEnglish = async () => {
await setCookie("locale", "en");
router.refresh();
};
const switchToRomanian = async () => {
await setCookie("locale", "ro");
router.refresh();
};
const switchToFrench = async () => {
await setCookie("locale", "fr");
router.refresh();
};
return (
<div>
<button onClick={switchToEnglish}>English</button>
<button onClick={switchToRomanian}>Română</button>
<button onClick={switchToFrench}>Français</button>
</div>
);
}
Type Safety
Type-Safe Translation Keys
TypeScript types are auto-generated from messages/en.json:
// ✅ Valid - key exists
t("Invoices.title");
// ❌ Compile error - key doesn't exist
t("Invoices.nonExistent");
Regenerate Types
After updating translation files:
npm run dev # Types auto-generate in development
Or manually:
npm run generate:i18n
Best Practices
✅ Do: Organize by Feature
{
"Invoices": {
"__metadata__": { },
"list": { },
"details": { },
"actions": { }
},
"Merchants": {
"__metadata__": { },
"list": { },
"details": { }
}
}
✅ Do: Use Variables for Dynamic Content
{
"greeting": "Hello, {userName}!",
"itemCount": "Showing {count} of {total} items"
}
t("greeting", {userName: "Alex"});
t("itemCount", {count: 10, total: 100});
✅ Do: Keep Keys Semantic
{
"actions": {
"save": "Save",
"cancel": "Cancel",
"delete": "Delete"
}
}
❌ Don't: Hardcode Text
// ❌ Bad
<button>Save Changes</button>
// ✅ Good
<button>{t("actions.save")}</button>
❌ Don't: Duplicate Keys
{
"Invoices": {
"save": "Save" // ❌ Duplicate
},
"Merchants": {
"save": "Save" // ❌ Duplicate
}
}
Instead, use common namespace:
{
"Common": {
"actions": {
"save": "Save"
}
}
}
Troubleshooting
Issue: Translation Key Not Found
Error: Missing message for key "MyPage.title"
Solution:
- Check key exists in all locale files:
en.json,ro.json, andfr.json - Verify correct namespace:
useTranslations("MyPage") - Run
npm run generate:i18nto detect and add missing keys - Run
npm run devto regenerate types
Issue: Translations Not Updating
Solution:
- Restart dev server:
Ctrl+C, thennpm run dev - Clear
.nextcache:rm -rf .next && npm run dev - Hard refresh browser:
Ctrl+Shift+R
Issue: Type Errors After Adding Keys
Solution:
# Regenerate TypeScript definitions
npm run generate:i18n
Issue: Wrong Language Displayed
Check locale cookie:
import {getCookie} from "@/lib/actions/cookies";
const locale = await getCookie("locale");
console.log("Current locale:", locale); // Should be "en", "ro", or "fr"
Translation File Structure
messages/
├── en.json # English translations (source of truth)
├── ro.json # Romanian translations
├── fr.json # French translations
└── en.d.json.ts # Auto-generated TypeScript types
Note: Always add new keys to
en.jsonfirst, then runnpm run generate:i18nto synchronize other locales.
Example Translation Structure
{
"Common": {
"actions": {
"save": "Save",
"cancel": "Cancel",
"delete": "Delete"
}
},
"Invoices": {
"__metadata__": {
"title": "Invoices",
"description": "Manage your invoices"
},
"title": "My Invoices",
"empty": "No invoices found",
"list": {
"header": "All Invoices",
"filters": {
"search": "Search invoices...",
"sortBy": "Sort by",
"dateRange": "Date range"
}
}
}
}
Quick Reference
| Task | Server Component | Client Component |
|---|---|---|
| Get translations | await getTranslations("Namespace") | useTranslations("Namespace") |
| Get locale | await getLocale() | const locale = useLocale() |
| Format dates | const format = await useFormatter() | const format = useFormatter() |
| Generate metadata | generateMetadata() with getTranslations() | N/A |
| Sync translations | npm run generate:i18n | N/A |
Supported Locales
| Locale | Language | File |
|---|---|---|
en | English | messages/en.json (source of truth) |
ro | Romanian | messages/ro.json |
fr | French | messages/fr.json |
Additional Resources
- RFC 1003: Complete i18n system documentation
- next-intl Docs: https://next-intl-docs.vercel.app/
- ICU Message Format: https://formatjs.io/docs/core-concepts/icu-syntax/
- Translation Files:
sites/arolariu.ro/messages/ - i18n Script:
scripts/generate.i18n.ts
// was this page useful?