Documentation
Documentation
Introduction

Getting Started

Getting StartedInstallationQuick StartProject Structure

Architecture

Architecture OverviewTech StacktRPC MiddlewareDesign Principles

Patterns

Code Patterns & ConventionsFeature ModulesError HandlingType Safety

Database

DatabaseSchema DefinitionDatabase OperationsMigrationsCaching

API

tRPCProceduresRouterstRPC Proxy Setup
APIsOpenAPIREST Endpoints

Auth & Access

AuthenticationConfigurationOAuth ProvidersRolesSession Management
AuthorizationUser RolesPermissions

Routing & i18n

RoutingDeclarative RoutingNavigation
InternationalizationTranslationsLocale Routing

Components & UI

ComponentsButtonsFormsNavigationDialogs
StylesTailwind CSSThemingTypography

Storage

StorageConfigurationUsageBuckets

Configuration

ConfigurationEnvironment VariablesFeature Flags

Templates

Template GuidesCreate New FeatureCreate New PageCreate Database TableCreate tRPC RouterAdd Translations

Development

DevelopmentCommandsAI AgentsBest Practices

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.json

Namespace Organization

Global Namespaces

Shared across the entire application:

NamespacePurposeExample Keys
actionsServer action results (errors/success)errors.generic, success.saved
buttonsButton labels and CTAssave, delete, login
filesFile upload/management UIdragNDrop, uploading
menuNavigation menu labelslinks.PageHome, groups.settings
miscellaneousMisc UI elementsdate.placeholder, copy.copied
commonGeneral shared textwelcome, greeting

Error Namespaces

NamespacePurposeExample Keys
authErrorsAuthentication errorsLogin/registration errors
trpcErrorstRPC/HTTP status codesUNAUTHORIZED, NOT_FOUND
statusCodesHTTP status messages403, 404, 500

Page-Specific Namespaces

Each page with substantial content has its own namespace file.

Naming Convention:

page + [RouteFolder1] + [RouteFolder2] + ...

Examples:

Route PathNamespaceFile
/homepageHomepageHome.json
/loginpageLoginpageLogin.json
/dashboardpageDashboardpageDashboard.json
/dashboard/api-keyspageDashboardApiKeyspageDashboardApiKeys.json
/dashboard/organizations/[slug]pageDashboardOrganizationSlugpageDashboardOrganizationSlug.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

  1. Use full sentences - Avoid string concatenation - translation order varies by language
  2. Keep keys consistent - Use the same JSON structure across all locales
  3. Use ICU format - For pluralization and conditionals
  4. Test missing keys - Missing keys cause errors in production

Always add translations to all locale files. Missing keys will cause runtime errors in production.

On this page

File Structure
Namespace Organization
Global Namespaces
Error Namespaces
Page-Specific Namespaces
Using Translations
In Server Components
In Client Components
With Parameters
ICU Message Format
Pluralization
Select (Conditionals)
Rich Text
Menu Translations
Error Translations
tRPC Errors (trpcErrors.json)
Custom Errors (actions.json)
Zod Validation Messages
Custom Error Messages
Custom Error File
Adding New Translations
Step 1: Add to English first
Step 2: Add to other locales
Step 3: Use in code
Step 4: Test all locales
Example
Best Practices