Add Translations
Step-by-step guide for adding i18n translation keys across locale files
Overview
This guide walks you through adding internationalization (i18n) translations across all locale files. The application uses next-intl for type-safe translations with support for multiple languages.
All UI text should be internationalized, even if you only support one language initially. This makes adding languages later trivial.
Prerequisites
Before adding translations:
Step 1: Namespace
Determine the translation namespace:
- Global:
buttons,actions,miscellaneous,menu - Page-specific:
page<PageName>(e.g.,pageDashboard,pageApiKeys)
Step 2: Keys
List all translation keys needed with their English values
Step 3: Locales
Identify which locales to update (default: en and it)
Translation File Locations
All translation files are in src/messages/dictionaries/<locale>/:
src/messages/dictionaries/
├── en/ # English
│ ├── actions.json
│ ├── buttons.json
│ ├── menu.json
│ ├── miscellaneous.json
│ ├── pageDashboard.json
│ ├── pageHome.json
│ └── ...
└── it/ # Italian
├── actions.json
├── buttons.json
└── ...Namespace Organization
Global Namespaces
Used across multiple pages and components:
| File | Purpose | Examples |
|---|---|---|
actions.json | Server action results | "saved", "deleted", "error" |
buttons.json | Button labels | "save", "cancel", "delete" |
menu.json | Navigation menu labels | "home", "dashboard", "settings" |
miscellaneous.json | Misc UI elements | "loading", "error", "noResults" |
Page Namespaces
One JSON file per page, named after the route:
| Route | Namespace | File |
|---|---|---|
/ | pageHome | pageHome.json |
/dashboard | pageDashboard | pageDashboard.json |
/dashboard/api-keys | pageDashboardApiKeys | pageDashboardApiKeys.json |
/settings/profile | pageSettingsProfile | pageSettingsProfile.json |
Naming Convention: Page namespaces use PascalCase route names without slashes or hyphens.
/dashboard/api-keys → pageDashboardApiKeys
Step 1: Create Translation Files
For New Page
Create src/messages/dictionaries/en/page<PageName>.json:
{
"seo": {
"title": "Page Title",
"description": "Page description for search engines and social media"
},
"heading": {
"title": "Page Title",
"description": "Subtitle or description shown on the page"
}
}Repeat for each locale (e.g., it/page<PageName>.json).
For Existing Namespace
Add keys to the existing file.
Step 2: Standard Page Structure
Use this template for page translations:
{
"seo": {
"title": "Page Title",
"description": "SEO description"
},
"heading": {
"title": "Page Heading",
"description": "Page subtitle or description"
},
"content": {
"section1": {
"title": "Section Title",
"description": "Section description",
"items": {
"item1": "First item",
"item2": "Second item"
}
}
},
"form": {
"fields": {
"name": {
"label": "Name",
"placeholder": "Enter name...",
"hint": "Optional hint text"
},
"email": {
"label": "Email Address",
"placeholder": "you@example.com"
},
"description": {
"label": "Description",
"placeholder": "Enter description...",
"hint": "Markdown supported"
}
},
"validation": {
"required": "This field is required",
"invalidEmail": "Invalid email address",
"tooShort": "Must be at least {min} characters",
"tooLong": "Must be at most {max} characters"
},
"dialog": {
"create": "Create Item",
"update": "Update Item",
"delete": "Delete Item",
"deleteConfirmation": "Are you sure you want to delete {name}?"
}
},
"list": {
"empty": {
"title": "No items yet",
"description": "Create your first item to get started",
"action": "Create Item"
},
"columns": {
"name": "Name",
"status": "Status",
"createdAt": "Created",
"actions": "Actions"
}
},
"toasts": {
"created": "Item created successfully",
"updated": "Item updated successfully",
"deleted": "Item deleted successfully",
"error": "An error occurred"
}
}Step 3: Menu Translations
In src/messages/dictionaries/en/menu.json:
{
"links": {
"PageHome": "Home",
"PageAbout": "About",
"PageDashboard": "Dashboard",
"PageDashboardApiKeys": "API Keys",
"PageDashboardSettings": "Settings",
"Page<PageName>": "Your Page Title"
}
}Update for all locales:
src/messages/dictionaries/it/menu.json:
{
"links": {
"PageHome": "Home",
"PageAbout": "Chi Siamo",
"PageDashboard": "Dashboard",
"PageDashboardApiKeys": "Chiavi API",
"Page<PageName>": "Titolo Pagina"
}
}Dynamic Parameters
Use curly braces {paramName} for interpolation:
{
"welcome": "Welcome, {name}!",
"itemCount": "You have {count} items",
"greeting": "Hello, {firstName} {lastName}",
"deleteConfirmation": "Are you sure you want to delete {name}?"
}Usage in code:
// Server component
const t = await getTranslations("namespace");
t("welcome", { name: user.name });
// Output: "Welcome, John!"
// Client component
const t = useTranslations("namespace");
t("deleteConfirmation", { name: item.name });
// Output: "Are you sure you want to delete My Item?"Pluralization
Use ICU message format for plurals:
{
"items": "{count, plural, =0 {no items} =1 {one item} other {# items}}",
"minutes": "{count, plural, =1 {# minute} other {# minutes}}",
"notifications": "{count, plural, =0 {No notifications} =1 {1 notification} other {# notifications}}"
}Usage:
t("items", { count: 0 }); // "no items"
t("items", { count: 1 }); // "one item"
t("items", { count: 5 }); // "5 items"ICU Syntax: # is replaced with the actual number in the other case.
Rich Text Formatting
For text with inline components:
{
"terms": "By signing up, you agree to our <link>Terms of Service</link>.",
"welcome": "Welcome! <strong>Get started</strong> by creating your first item.",
"privacy": "Read our <privacyLink>Privacy Policy</privacyLink> and <termsLink>Terms</termsLink>."
}Usage with t.rich():
// Server component
const t = await getTranslations("namespace");
t.rich("terms", {
link: (chunks) => <Link href="/terms">{chunks}</Link>
});
t.rich("privacy", {
privacyLink: (chunks) => <Link href="/privacy">{chunks}</Link>,
termsLink: (chunks) => <Link href="/terms">{chunks}</Link>,
});// Client component
const t = useTranslations("namespace");
t.rich("welcome", {
strong: (chunks) => <strong>{chunks}</strong>
});Zod Validation Errors
Custom validation messages in src/messages/zod-custom/<locale>.json:
{
"required": "This field is required",
"invalid_email": "Invalid email address",
"password_too_short": "Password must be at least 8 characters",
"password_match": "Passwords must match",
"invalid_permissions": "Invalid permissions selected",
"api_key_expired": "API key has expired",
"unique_name": "This name is already taken"
}Usage in Zod schema:
import { z } from "zod";
const schema = z.object({
email: z.string().email({ params: { i18n: "invalid_email" } }),
password: z.string()
.min(8, { params: { i18n: "password_too_short" } }),
});Or with .refine():
const schema = z.object({
password: z.string(),
confirmPassword: z.string(),
}).refine(
(data) => data.password === data.confirmPassword,
{ params: { i18n: "password_match" }, path: ["confirmPassword"] }
);Usage in Components
Server Components
import { getTranslations } from "next-intl/server";
export default async function Page() {
const t = await getTranslations("pageDashboard");
return (
<div>
<h1>{t("heading.title")}</h1>
<p>{t("heading.description")}</p>
<p>{t("welcome", { name: "John" })}</p>
</div>
);
}Client Components
"use client";
import { useTranslations } from "next-intl";
export function MyComponent() {
const t = useTranslations("pageDashboard");
return (
<div>
<h1>{t("heading.title")}</h1>
<button>{t("actions.create")}</button>
</div>
);
}Multiple Namespaces
"use client";
import { useTranslations } from "next-intl";
export function MyComponent() {
const t = useTranslations("pageDashboard");
const tButtons = useTranslations("buttons");
const tActions = useTranslations("actions");
return (
<div>
<h1>{t("heading.title")}</h1>
<button>{tButtons("save")}</button>
<p>{tActions("saved")}</p>
</div>
);
}Translation Key Naming Conventions
Follow these patterns for consistency:
Page Structure
seo.title - SEO title
seo.description - SEO description
heading.title - Page heading
heading.description - Page subtitleForms
form.fields.<field>.label - Field label
form.fields.<field>.placeholder - Placeholder text
form.fields.<field>.hint - Help text
form.validation.<error> - Validation error
form.dialog.create - Create dialog title
form.dialog.update - Update dialog title
form.dialog.delete - Delete dialog title
form.dialog.deleteConfirmation - Delete confirmationLists & Tables
list.empty.title - Empty state title
list.empty.description - Empty state description
list.empty.action - Empty state CTA
list.columns.<column> - Table column headerActions & Toasts
toasts.created - Success message after create
toasts.updated - Success message after update
toasts.deleted - Success message after delete
toasts.error - Generic error messageContent Sections
content.<section>.title - Section title
content.<section>.description - Section description
content.<section>.items.<item> - Individual itemComplete Example
src/messages/dictionaries/en/pageDashboardApiKeys.json:
{
"seo": {
"title": "API Keys",
"description": "Manage your API keys for programmatic access"
},
"heading": {
"title": "API Keys",
"description": "Create and manage API keys for accessing your data"
},
"form": {
"fields": {
"name": {
"label": "Key Name",
"placeholder": "e.g., Production Server",
"hint": "A descriptive name for this API key"
},
"permissions": {
"label": "Permissions",
"placeholder": "Select permissions...",
"hint": "Choose what this key can access"
},
"expiresAt": {
"label": "Expiration Date",
"placeholder": "Never",
"hint": "When this key should expire"
}
},
"dialog": {
"create": "Create API Key",
"update": "Update API Key",
"delete": "Delete API Key",
"deleteConfirmation": "Are you sure you want to delete {name}? This action cannot be undone."
}
},
"list": {
"empty": {
"title": "No API keys yet",
"description": "Create your first API key to start using the API",
"action": "Create API Key"
},
"columns": {
"name": "Name",
"prefix": "Key",
"permissions": "Permissions",
"expiresAt": "Expires",
"createdAt": "Created",
"actions": "Actions"
}
},
"toasts": {
"created": "API key created successfully",
"updated": "API key updated successfully",
"deleted": "API key deleted successfully",
"copied": "API key copied to clipboard"
},
"warnings": {
"expiringSoon": "This key expires in {days, plural, =1 {1 day} other {# days}}",
"expired": "This key has expired"
}
}Italian version src/messages/dictionaries/it/pageDashboardApiKeys.json:
{
"seo": {
"title": "Chiavi API",
"description": "Gestisci le tue chiavi API per l'accesso programmatico"
},
"heading": {
"title": "Chiavi API",
"description": "Crea e gestisci chiavi API per accedere ai tuoi dati"
},
"form": {
"fields": {
"name": {
"label": "Nome Chiave",
"placeholder": "es., Server Produzione",
"hint": "Un nome descrittivo per questa chiave API"
},
"permissions": {
"label": "Permessi",
"placeholder": "Seleziona permessi...",
"hint": "Scegli cosa può accedere questa chiave"
},
"expiresAt": {
"label": "Data Scadenza",
"placeholder": "Mai",
"hint": "Quando questa chiave dovrebbe scadere"
}
},
"dialog": {
"create": "Crea Chiave API",
"update": "Aggiorna Chiave API",
"delete": "Elimina Chiave API",
"deleteConfirmation": "Sei sicuro di voler eliminare {name}? Questa azione non può essere annullata."
}
},
"list": {
"empty": {
"title": "Nessuna chiave API ancora",
"description": "Crea la tua prima chiave API per iniziare a usare l'API",
"action": "Crea Chiave API"
},
"columns": {
"name": "Nome",
"prefix": "Chiave",
"permissions": "Permessi",
"expiresAt": "Scade",
"createdAt": "Creata",
"actions": "Azioni"
}
},
"toasts": {
"created": "Chiave API creata con successo",
"updated": "Chiave API aggiornata con successo",
"deleted": "Chiave API eliminata con successo",
"copied": "Chiave API copiata negli appunti"
},
"warnings": {
"expiringSoon": "Questa chiave scade tra {days, plural, =1 {1 giorno} other {# giorni}}",
"expired": "Questa chiave è scaduta"
}
}Post-Creation Checklist
-
File Creation
- Translation files created for all locales
- File names match namespace convention
- JSON is valid (no syntax errors)
-
Structure
- Consistent key structure across locales
- All dynamic params present (
{name},{count}) - Pluralization uses correct ICU format
- Rich text tags match across locales
-
Completeness
- No missing translations in any locale
- All keys have values (no empty strings)
- SEO fields completed for pages
- Validation messages added (if needed)
-
Testing
- Translations display correctly in UI
- Dynamic parameters work
- Pluralization works
- Rich text renders properly
- Switch between locales works
Common Mistakes to Avoid
DON'T:
- Hardcode text in components
- Use different keys across locales
- Forget to translate for all locales
- Use nested parameters (
{user.name}instead of{userName}) - Mix single and plural forms
DO:
- Use consistent key naming
- Keep structure identical across locales
- Use ICU format for plurals
- Test with different parameter values
- Document complex translations
Real-World Examples
See these translation files for reference:
- src/messages/dictionaries/en/menu.json - Menu labels
- src/messages/dictionaries/en/pageDashboardApiKeys.json - Complete page
- src/messages/dictionaries/en/buttons.json - Button labels
Translations added! Your content is now internationalized and ready for multiple languages.