Translations
Managing translation files
Translation files are organized by namespace in the src/messages/dictionaries/ directory.
File Structure
src/messages/
├── dictionaries/
│ ├── en/ # English translations
│ │ ├── common.json
│ │ ├── auth.json
│ │ ├── buttons.json
│ │ ├── menu.json
│ │ ├── actions.json
│ │ ├── pageHome.json
│ │ └── ...
│ └── it/ # Italian translations
│ └── ...
└── zod-errors/ # Zod validation messages
├── en.json
└── it.jsonNamespace Organization
Global Namespaces
Shared across the entire application:
| Namespace | Purpose | Example Keys |
|---|---|---|
actions | Server action results (errors/success) | errors.generic, success.saved |
buttons | Button labels and CTAs | save, delete, login |
files | File upload/management UI | dragNDrop, uploading |
menu | Navigation menu labels | links.PageHome, groups.settings |
miscellaneous | Misc UI elements | date.placeholder, copy.copied |
common | General shared text | welcome, greeting |
Error Namespaces
| Namespace | Purpose | Example Keys |
|---|---|---|
authErrors | Authentication errors | Login/registration errors |
trpcErrors | tRPC/HTTP status codes | UNAUTHORIZED, NOT_FOUND |
statusCodes | HTTP status messages | 403, 404, 500 |
Page-Specific Namespaces
Each page with substantial content has its own namespace file.
Naming Convention:
page + [RouteFolder1] + [RouteFolder2] + ...Examples:
| Route Path | Namespace | File |
|---|---|---|
/home | pageHome | pageHome.json |
/login | pageLogin | pageLogin.json |
/dashboard | pageDashboard | pageDashboard.json |
/dashboard/api-keys | pageDashboardApiKeys | pageDashboardApiKeys.json |
/dashboard/organizations/[slug] | pageDashboardOrganizationSlug | pageDashboardOrganizationSlug.json |
The namespace name matches the route's page.info.ts → Route.name property with a lowercase page prefix.
Using Translations
In Server Components
import { getTranslations } from "next-intl/server";
export default async function Page() {
const t = await getTranslations("common");
return <H1>{t("welcome")}</H1>;
}In Client Components
"use client";
import { useTranslations } from "next-intl";
export function MyComponent() {
const t = useTranslations("common");
return <P>{t("greeting")}</P>;
}With Parameters
{
"greeting": "Hello, {name}!",
"range": "Showing {start} to {end} of {total}"
}t("greeting", { name: "John" });
// Output: "Hello, John!"
t("range", { start: 1, end: 10, total: 100 });
// Output: "Showing 1 to 10 of 100"ICU Message Format
Pluralization
{
"items": "{count, plural, =0 {No items} =1 {One item} other {# items}}",
"followers": "{count, plural, =0 {No followers} =1 {1 follower} other {{count} followers}}"
}t("items", { count: 0 }); // "No items"
t("items", { count: 1 }); // "One item"
t("items", { count: 5 }); // "5 items"Select (Conditionals)
{
"greeting": "{gender, select, male {Hello Mr. {name}} female {Hello Ms. {name}} other {Hello {name}}}"
}t("greeting", { gender: "male", name: "Smith" });
// Output: "Hello Mr. Smith"Rich Text
Use RichText for translations with formatted content:
{
"terms": "By signing up, you agree to our <link>Terms of Service</link>.",
"warning": "This action is <strong>irreversible</strong>. Are you sure?"
}import { RichText } from "@/i18n/rich-text";
import { PageTerms } from "@/routes";
<RichText
messageKey="common.terms"
components={{
link: (chunks) => <Link href={PageTerms()}>{chunks}</Link>,
}}
/>
<RichText
messageKey="common.warning"
components={{
strong: (chunks) => <strong className="font-bold">{chunks}</strong>,
}}
/>Menu Translations
Navigation menus use a specific structure in menu.json:
{
"links": {
"PageHome": "Home",
"PageAbout": "About",
"PageDashboard": "Dashboard",
"PageDashboardApiKeys": "API Keys"
},
"groups": {
"content": "Content",
"settings": "Settings",
"admin": "Administration"
}
}Usage:
const t = useTranslations("menu");
const label = t(`links.${PageDashboard.routeName}`);Error Translations
tRPC Errors (trpcErrors.json)
Only standard tRPC/HTTP error codes:
{
"PARSE_ERROR": "There was a problem parsing the data.",
"BAD_REQUEST": "Something is wrong with your request.",
"UNAUTHORIZED": "You are not authorized.",
"FORBIDDEN": "You do not have permission.",
"NOT_FOUND": "Resource not found.",
"TOO_MANY_REQUESTS": "Too many requests. Try again in {seconds} seconds."
}Custom Errors (actions.json)
Platform-specific errors in the errors object:
{
"errors": {
"generic": "An error occurred",
"cannotDeleteSelf": "You cannot delete your own account",
"userAlreadyMember": "This user is already a member",
"invitationAlreadyPending": "This user already has a pending invitation"
},
"success": {
"saved": "Saved Changes",
"deleted": "Deleted successfully"
}
}Important
Custom business logic errors belong in actions.json, NOT trpcErrors.json.
Zod Validation Messages
Zod errors are automatically translated using localized error maps.
Custom Error Messages
export const schema = z.object({
email: z.string().email(),
password: z.string().min(8).max(100),
}).refine((data) => data.password.length >= 8, {
message: "Password must be at least 8 characters",
path: ["password"],
});Custom Error File
// messages/zod-custom/en.json
{
"required": "This field is required",
"password_match": "Passwords must match",
"start_date_before_end_date": "End date must be after start date"
}Adding New Translations
Step 1: Add to English first
Add the key to the English translation file
Step 2: Add to other locales
Add the same key to all other locale files
Step 3: Use in code
Import and use with useTranslations or getTranslations
Step 4: Test all locales
Verify translations display correctly
Example
// src/messages/dictionaries/en/common.json
{
"newFeature": "Try our new feature!"
}// src/messages/dictionaries/it/common.json
{
"newFeature": "Prova la nostra nuova funzionalità!"
}const t = useTranslations("common");
return <P>{t("newFeature")}</P>;Best Practices
- Use full sentences - Avoid string concatenation - translation order varies by language
- Keep keys consistent - Use the same JSON structure across all locales
- Use ICU format - For pluralization and conditionals
- Test missing keys - Missing keys cause errors in production
Always add translations to all locale files. Missing keys will cause runtime errors in production.