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

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:

FilePurposeExamples
actions.jsonServer action results"saved", "deleted", "error"
buttons.jsonButton labels"save", "cancel", "delete"
menu.jsonNavigation menu labels"home", "dashboard", "settings"
miscellaneous.jsonMisc UI elements"loading", "error", "noResults"

Page Namespaces

One JSON file per page, named after the route:

RouteNamespaceFile
/pageHomepageHome.json
/dashboardpageDashboardpageDashboard.json
/dashboard/api-keyspageDashboardApiKeyspageDashboardApiKeys.json
/settings/profilepageSettingsProfilepageSettingsProfile.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 subtitle

Forms

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 confirmation

Lists & Tables

list.empty.title         - Empty state title
list.empty.description   - Empty state description
list.empty.action        - Empty state CTA
list.columns.<column>    - Table column header

Actions & Toasts

toasts.created    - Success message after create
toasts.updated    - Success message after update
toasts.deleted    - Success message after delete
toasts.error      - Generic error message

Content Sections

content.<section>.title         - Section title
content.<section>.description   - Section description
content.<section>.items.<item>  - Individual item

Complete 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

  1. File Creation

    • Translation files created for all locales
    • File names match namespace convention
    • JSON is valid (no syntax errors)
  2. Structure

    • Consistent key structure across locales
    • All dynamic params present ({name}, {count})
    • Pluralization uses correct ICU format
    • Rich text tags match across locales
  3. Completeness

    • No missing translations in any locale
    • All keys have values (no empty strings)
    • SEO fields completed for pages
    • Validation messages added (if needed)
  4. 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.

On this page

Overview
Prerequisites
Step 1: Namespace
Step 2: Keys
Step 3: Locales
Translation File Locations
Namespace Organization
Global Namespaces
Page Namespaces
Step 1: Create Translation Files
For New Page
For Existing Namespace
Step 2: Standard Page Structure
Step 3: Menu Translations
Dynamic Parameters
Pluralization
Rich Text Formatting
Zod Validation Errors
Usage in Components
Server Components
Client Components
Multiple Namespaces
Translation Key Naming Conventions
Page Structure
Forms
Lists & Tables
Actions & Toasts
Content Sections
Complete Example
Post-Creation Checklist
Common Mistakes to Avoid
Real-World Examples