# Managing Translations

> How to add new messages, create namespaces, add languages, and configure the language selector for your users.

*Canonical: https://makerkit.dev/docs/nextjs-prisma/internationalization/managing-translations*

---

This guide covers the workflow for managing translations: adding new keys, creating namespaces, adding languages, and configuring the user-facing language selector.

## Adding New Translation Keys

### 1. Find the Right Namespace

Translation files live in `apps/web/i18n/messages/{locale}/`:

| File | Purpose |
|------|---------|
| `common.json` | Shared UI elements, navigation, roles |
| `auth.json` | Authentication flows |
| `account.json` | User profile management |
| `organizations.json` | Team management |
| `billing.json` | Payments and subscriptions |
| `marketing.json` | Public marketing pages |
| `settings.json` | Settings and preferences |
| `goodbye.json` | Account deletion |
| `errors.json` | Error messages |

### 2. Add the Key

Open the appropriate namespace and add your key:

```json title="apps/web/i18n/messages/en/common.json"
{
  "existingKey": "Existing translation",
  "newFeature": {
    "title": "New Feature",
    "description": "Description of the new feature",
    "submitButton": "Get Started"
  }
}
```

### 3. Add to All Locales

If you support multiple languages, add the key to each locale:

```json title="apps/web/i18n/messages/es/common.json"
{
  "existingKey": "Traducción existente",
  "newFeature": {
    "title": "Nueva Funcionalidad",
    "description": "Descripción de la nueva funcionalidad",
    "submitButton": "Comenzar"
  }
}
```

### Key Naming Conventions

| Pattern | Example | Use Case |
|---------|---------|----------|
| camelCase | `submitButton` | Simple keys |
| Nested objects | `form.errors.required` | Related translations |
| Verb-based | `createProject`, `deleteAccount` | Actions |
| Suffix for context | `titleLabel`, `titlePlaceholder` | Form fields |

**Good structure:**

```json
{
  "createProject": {
    "title": "Create a New Project",
    "description": "Start a new project to organize your work",
    "form": {
      "nameLabel": "Project Name",
      "namePlaceholder": "Enter project name",
      "descriptionLabel": "Description"
    },
    "submitButton": "Create Project",
    "cancelButton": "Cancel",
    "errors": {
      "nameRequired": "Project name is required",
      "nameTooLong": "Project name must be less than 100 characters"
    }
  }
}
```

## Creating New Namespaces

For new features, create a dedicated namespace:

### 1. Update the Namespaces Array

Edit `apps/web/i18n/request.ts`:

```typescript title="apps/web/i18n/request.ts"
const namespaces = [
  'common',
  'auth',
  'account',
  'organizations',
  'billing',
  'marketing',
  'settings',
  'goodbye',
  'errors',
  'projects', // Add your new namespace
] as const;
```

### 2. Create the Namespace File

Create the file for each locale:

```bash
# Create for default locale
touch apps/web/i18n/messages/en/projects.json
```

```json title="apps/web/i18n/messages/en/projects.json"
{
  "pageTitle": "Projects",
  "pageDescription": "Manage your projects",
  "createProject": "Create Project",
  "noProjects": "No projects yet. Create your first project to get started.",
  "deleteConfirmation": "Are you sure you want to delete this project? This action cannot be undone.",
  "table": {
    "name": "Name",
    "status": "Status",
    "created": "Created",
    "actions": "Actions"
  }
}
```

### 3. Create for Other Locales

```bash
# Spanish
touch apps/web/i18n/messages/es/projects.json

# French
touch apps/web/i18n/messages/fr/projects.json
```

Then translate each file.

## Adding a New Language

### 1. Enable the Locale

Edit `packages/i18n/src/locales.tsx`:

```typescript title="packages/i18n/src/locales.tsx"
import { defaultLocale } from './default-locale';

export const locales: string[] = [
  defaultLocale,
  'es', // Spanish
  'fr', // French
  'de', // German
  'pt', // Portuguese
];
```

### 2. Create Translation Files

```bash
# Create the locale folder
mkdir apps/web/i18n/messages/de

# Copy English files as templates
cp apps/web/i18n/messages/en/*.json apps/web/i18n/messages/de/
```

### 3. Translate Each File

Open each JSON file and translate the values (not the keys):

```json title="apps/web/i18n/messages/de/auth.json"
{
  "signUpHeading": "Konto erstellen",
  "signUp": "Registrieren",
  "signInHeading": "Anmelden",
  "signIn": "Anmelden"
}
```

### 4. Add Email Template Translations

If your app sends transactional emails, add translations for email templates. See [Email Translations](./email-translations) for details.

### ISO 639-1 Language Codes

Use standard two-letter codes:

| Code | Language | Code | Language |
|------|----------|------|----------|
| `en` | English | `ja` | Japanese |
| `es` | Spanish | `zh` | Chinese |
| `fr` | French | `ko` | Korean |
| `de` | German | `ar` | Arabic |
| `it` | Italian | `hi` | Hindi |
| `pt` | Portuguese | `ru` | Russian |

## Language Selector

Users can change their language preference in settings.

### How It Works

The `LanguagePreferenceCard` component from `@kit/ui/language-selector` provides the UI:

```typescript title="apps/web/app/[locale]/(internal)/settings/preferences/page.tsx"
import { LanguagePreferenceCard } from '@kit/ui/language-selector';
import { locales } from '@kit/i18n/routing';

export default function PreferencesPage() {
  return (
    <LanguagePreferenceCard locales={locales} />
  );
}
```

**Behavior:**

- Shows button grid for 3 or fewer languages
- Shows dropdown selector for 4+ languages
- Displays native language names (e.g., "Español" for Spanish, "Deutsch" for German)
- Handles loading state during locale transition
- **Hidden when only one locale** is configured

### How Language Preference is Stored

**1. URL Routing (Primary)**

The locale is part of the URL:

- `/dashboard` for English (default, no prefix)
- `/es/dashboard` for Spanish
- `/fr/dashboard` for French

When users navigate, the URL determines the locale.

**2. Cookie Storage (Server-side)**

For operations like sending emails where there's no URL context, a `NEXT_LOCALE` cookie stores the preference:

```typescript
// The cookie is set automatically by next-intl middleware
// when the user visits a localized URL
```

This cookie is used by:

- Email templates
- Server-side operations triggered by webhooks
- API routes

### Programmatic Locale Switching

Switch locale in client code:

```typescript
'use client';

import { useRouter, usePathname } from '@kit/i18n/navigation';
import { useTransition } from 'react';

function LanguageSwitcher() {
  const router = useRouter();
  const pathname = usePathname();
  const [isPending, startTransition] = useTransition();

  function changeLocale(newLocale: string) {
    startTransition(() => {
      router.replace(pathname, { locale: newLocale });
    });
  }

  return (
    <button onClick={() => changeLocale('es')} disabled={isPending}>
      {isPending ? 'Switching...' : 'Switch to Spanish'}
    </button>
  );
}
```

## Translation Workflow Tips

### Use a Translation Management Tool

For projects with many translations, consider:

- **Crowdin** or **Lokalise** for professional translation workflows
- **i18n Ally** VS Code extension for inline editing
- Export/import JSON files to translation services

### Keep Keys Organized

```json
// Good: grouped by feature
{
  "dashboard": {
    "title": "Dashboard",
    "stats": { ... },
    "charts": { ... }
  }
}

// Avoid: flat and hard to navigate
{
  "dashboardTitle": "Dashboard",
  "dashboardStatsTotal": "Total",
  "dashboardStatsActive": "Active",
  "dashboardChartsLine": "Line Chart"
}
```

### Add Context Comments

JSON doesn't support comments, but you can use a description pattern:

```json
{
  "_deleteConfirmation_context": "Shown when user clicks delete button",
  "deleteConfirmation": "Are you sure? This cannot be undone."
}
```

Or maintain a separate translation notes file.

### Test All Locales

Regularly check that:

1. No keys are missing in any locale
2. Interpolation values render correctly
3. Pluralization works for all languages
4. Text doesn't overflow UI elements (German text is often 30% longer than English)

### Handle Right-to-Left Languages

For RTL languages like Arabic or Hebrew:

1. Add the locale to your array
2. Update your layout to conditionally set `dir="rtl"`:

```typescript
<html lang={locale} dir={locale === 'ar' ? 'rtl' : 'ltr'}>
```

3. Use Tailwind's RTL utilities or CSS logical properties

## Debugging Missing Translations

### Development Warnings

In development, missing translations log warnings:

```
[Dev Only] i18n error: Missing message: projects.newKey
```

### Finding Missing Keys

Create a script to compare locale files:

```bash
# Quick diff between locales
diff <(jq 'keys' apps/web/i18n/messages/en/common.json | sort) \
     <(jq 'keys' apps/web/i18n/messages/es/common.json | sort)
```

### Fallback Behavior

Missing translations return the key name. Configure this in `apps/web/i18n/request.ts`:

```typescript
getMessageFallback(info) {
  // Log for monitoring
  console.warn(`Missing translation: ${info.namespace}.${info.key}`);

  // Return key as fallback (or customize)
  return info.key;
}
```

## Best Practices Summary

1. **One namespace per feature** keeps files manageable
2. **Nest related keys** for better organization
3. **Use descriptive key names** that indicate context
4. **Add keys to all locales** immediately to avoid missing translations
5. **Test with longer languages** (German, French) to catch UI overflow
6. **Use ICU format** for plurals and complex formatting
7. **Keep translations close to code** using the same namespace naming
8. **Review translations in context** not just the JSON files

{% faq
   title="Frequently Asked Questions"
   items=[
     {"question": "How do I add a new language to MakerKit?", "answer": "1) Add the locale code to packages/i18n/src/locales.tsx, 2) Create a folder at apps/web/i18n/messages/{locale}/, 3) Copy all JSON files from the en/ folder and translate them, 4) Restart the dev server. The language selector automatically appears when multiple locales are configured."},
     {"question": "Where does the language selector appear?", "answer": "The language selector appears in the user settings at /settings/preferences. It uses the LanguagePreferenceCard component from @kit/ui/language-selector. It only shows when you have 2 or more languages configured in your locales array."},
     {"question": "How is the user's language preference stored?", "answer": "Language preference is stored in two ways: 1) The URL path determines the current locale (e.g., /es/dashboard for Spanish), 2) A NEXT_LOCALE cookie stores the preference for server-side operations like sending emails. The middleware sets this cookie automatically."},
     {"question": "What naming convention should I use for translation keys?", "answer": "Use camelCase for keys (submitButton, pageTitle). Group related translations in nested objects. Use verb-based names for actions (createProject, deleteAccount). Add suffixes for context (nameLabel, namePlaceholder). Keep keys descriptive but concise."},
     {"question": "How do I handle right-to-left (RTL) languages like Arabic?", "answer": "Add the locale to your locales array, then update your root layout to set dir='rtl' conditionally: <html lang={locale} dir={locale === 'ar' ? 'rtl' : 'ltr'}>. Use Tailwind's RTL utilities or CSS logical properties for layout."},
     {"question": "How can I find missing translations?", "answer": "In development, missing translations log warnings to the console. You can also compare locale files using diff commands: diff <(jq 'keys' en/common.json | sort) <(jq 'keys' es/common.json | sort). Consider using i18n Ally VS Code extension for visual tracking."}
   ]
/%}

---

**Previous:** [Using Translations](./using-translations) | **Next:** [Email Translations](./email-translations)
