Skip to main content

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:i18n after adding keys to en.json to 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:

  1. Check key exists in all locale files: en.json, ro.json, and fr.json
  2. Verify correct namespace: useTranslations("MyPage")
  3. Run npm run generate:i18n to detect and add missing keys
  4. Run npm run dev to regenerate types

Issue: Translations Not Updating

Solution:

  1. Restart dev server: Ctrl+C, then npm run dev
  2. Clear .next cache: rm -rf .next && npm run dev
  3. 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.json first, then run npm run generate:i18n to 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

TaskServer ComponentClient Component
Get translationsawait getTranslations("Namespace")useTranslations("Namespace")
Get localeawait getLocale()const locale = useLocale()
Format datesconst format = await useFormatter()const format = useFormatter()
Generate metadatagenerateMetadata() with getTranslations()N/A
Sync translationsnpm run generate:i18nN/A

Supported Locales

LocaleLanguageFile
enEnglishmessages/en.json (source of truth)
roRomanianmessages/ro.json
frFrenchmessages/fr.json

Additional Resources

// was this page useful?