# Managing Translations

> How to add new messages, create namespaces, add languages, and configure language settings.

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

---

This guide covers how to manage translations in your application, from adding new messages to supporting multiple languages.

## Adding New Messages

### 1. Identify the Namespace

Translation files are organized by namespace in `packages/i18n/src/messages/{locale}/`:

```
packages/i18n/src/messages/en/
├── common.json       # Shared UI labels, routes, roles
├── auth.json         # Authentication flows
├── account.json      # Account management
├── organizations.json # Team management
├── billing.json      # Subscriptions and payments
├── marketing.json    # Marketing pages
├── settings.json     # Settings pages
├── goodbye.json      # Account deletion
└── errors.json       # Error messages
```

Choose the namespace that best matches where your translation will be used.

### 2. Add the Translation Key

Open the appropriate namespace file and add your key:

```json
{
  "existingKey": "Existing translation",
  "newFeature": {
    "title": "New Feature",
    "description": "Description of the new feature",
    "button": "Get Started"
  }
}
```

### 3. Use the Translation

```typescript
// In a component
const t = useTranslations('common');
const title = t('newFeature.title');

// Outside React render (e.g. route head())
const t = getTranslator();
const title = t('common.newFeature.title');

// With Trans component
<Trans i18nKey="common.newFeature.title" />
```

## Key Naming Conventions

Follow these conventions for consistent translation keys:

| Convention | Example | Use Case |
|------------|---------|----------|
| camelCase | `pageTitle` | All keys |
| Nested objects | `dialog.title` | Related translations |
| Descriptive names | `createProjectButton` | Self-documenting keys |
| Component prefix | `inviteDialog.title` | Component-specific text |

**Example 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",
      "submitButton": "Create Project",
      "cancelButton": "Cancel"
    },
    "success": "Project created successfully",
    "error": "Failed to create project"
  }
}
```

## Adding New Namespaces

When a feature grows large enough to warrant its own namespace:

### 1. Create the Translation File

Create `packages/i18n/src/messages/en/projects.json`:

```json
{
  "pageTitle": "Projects",
  "pageDescription": "Manage your projects",
  "createProject": "Create Project",
  "noProjects": "No projects yet",
  "deleteConfirmation": "Are you sure you want to delete this project?",
  "table": {
    "name": "Name",
    "status": "Status",
    "created": "Created",
    "actions": "Actions"
  }
}
```

### 2. Register the Namespace

Import the file and add it to the registry in `packages/i18n/src/messages/index.ts`:

```typescript
import enProjects from './en/projects.json';

const registry: Record<string, Messages> = {
  en: {
    common: enCommon,
    auth: enAuth,
    // ...existing namespaces
    projects: enProjects, // Add your new namespace
  },
};
```

Because all of a locale's namespaces are statically imported and provided to the `I18nProvider` at once, registering it here is all that's needed — there is no separate request/loader config.

### 3. Create Files for Other Locales

If you support multiple languages, create the same file for each locale and register it in the matching registry entry:

```bash
# Copy as template
cp packages/i18n/src/messages/en/projects.json packages/i18n/src/messages/es/projects.json
```

Then translate the Spanish version.

## Adding New Languages

### 1. Add Locale to Configuration

Edit `packages/i18n/src/config.ts`:

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

> Adding a non-default locale also activates URL prefix routing for it (`/es/...`) via the router rewrite — see [Overview → URL Prefix Routing](./overview#url-prefix-routing). No per-page changes are needed: existing `<Link>`s localize automatically; use `LocalizedLink` (`@kit/ui/localized-link`) only for dynamic/string hrefs.

### 2. Create Translation Directory

```bash
# Create the locale folder
mkdir -p packages/i18n/src/messages/es

# Copy all English files as templates
cp packages/i18n/src/messages/en/*.json packages/i18n/src/messages/es/
```

### 3. Register the Locale in the Message Registry

Import the new locale's namespaces and add a registry entry in `packages/i18n/src/messages/index.ts`:

```typescript
import esCommon from './es/common.json';
import esAuth from './es/auth.json';
// ...the rest of the namespaces

const registry: Record<string, Messages> = {
  en: { /* ... */ },
  es: {
    common: esCommon,
    auth: esAuth,
    // ...the rest
  },
};
```

`getMessages(locale)` falls back to the default locale if a locale is not present in the registry.

### 4. Translate Files

Translate each JSON file in the new locale directory:

```
packages/i18n/src/messages/es/
├── common.json      # Translate
├── auth.json        # Translate
├── account.json     # Translate
├── organizations.json
├── billing.json
├── marketing.json
├── settings.json
├── goodbye.json
└── errors.json
```

### 5. Add Email Template Translations (Optional)

If your app sends transactional emails, add translations for email templates under `packages/email-templates/src/locales/{locale}/`:

```
packages/email-templates/src/locales/es/
├── email-verification-email.json
├── reset-password-email.json
└── magic-link-email.json
```

### Language Codes

Use standard ISO 639-1 language codes:

| Code | Language | Native Name |
|------|----------|-------------|
| `en` | English | English |
| `es` | Spanish | Espanol |
| `fr` | French | Francais |
| `de` | German | Deutsch |
| `it` | Italian | Italiano |
| `pt` | Portuguese | Portugues |
| `ja` | Japanese | Nihongo |
| `zh` | Chinese | Zhongwen |
| `ko` | Korean | Hangugeo |
| `ar` | Arabic | Al-Arabiyyah |

## Language Selector UI

The TanStack Start Prisma SaaS Kit includes a language selector component that automatically appears when multiple languages are configured.

### Location

The language preference is available at `/settings/preferences`. The card only renders when 2 or more languages are configured.

Route file: `apps/web/src/routes/_authenticated/settings/preferences.tsx`

### LanguagePreferenceCard Component

The `@kit/ui/language-selector` package provides the language selector:

```typescript
import { LanguagePreferenceCard } from '@kit/ui/language-selector';
import { locales } from '@kit/i18n/config';

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

**Behavior:**

- Shows a button grid for 3 or fewer languages
- Shows a dropdown selector for 4+ languages
- Displays native language names using `Intl.DisplayNames`

### How Language Switching Works

When a user selects a new language, the selector writes the `locale` cookie and reloads the page. On the next request, `detectLocale()` reads the cookie and the server re-renders with the new language:

```typescript
// Internal implementation (packages/ui/src/makerkit/language-selector.tsx)
import { LOCALE_COOKIE } from '@kit/i18n/config';

function useChangeLocale() {
  return useCallback((locale: string) => {
    // Persist the chosen locale and reload so the server re-renders messages
    document.cookie = `${LOCALE_COOKIE}=${locale};path=/;max-age=31536000;samesite=lax`;

    window.location.reload();
  }, []);
}
```

### Visibility Rules

The language card only renders when `locales.length > 1`. With a single language configured (the default), the preference card is hidden.

## How Language is Stored

Language preference is stored in a cookie — there is no locale in the URL for the application surface:

| Mechanism | Value | Notes |
|-----------|-------|-------|
| `locale` cookie | `en`, `es`, ... | Set by the language selector; read by `detectLocale()` |
| `Accept-Language` header | browser locales | Fallback when no cookie is present |
| `defaultLocale` | `en` | Final fallback |

The cookie key is exported as `LOCALE_COOKIE` from `@kit/i18n/config`.

## Translation Workflow

### Development Workflow

1. **Add keys in English first** in the appropriate namespace
2. **Use the translation** in your component
3. **Test the UI** to verify text displays correctly
4. **Add translations** for other supported languages

### Production Considerations

- **Missing translations**: Fall back to the key name and log warnings in development
- **Long text**: Some languages require more space (German is typically 30% longer than English)
- **RTL languages**: Consider layout implications for Arabic, Hebrew, etc.
- **Pluralization**: Test plural forms with different counts

### Translation Management Tools

For larger projects, consider using translation management platforms:

- **Crowdin** - Collaborative translation
- **Lokalise** - Developer-focused TMS
- **Phrase** - Enterprise translation management

These tools can sync with your JSON files and provide translator interfaces.

## Best Practices

### Organization

- **Keep namespaces focused**: One namespace per feature area
- **Use nested objects**: Group related translations
- **Consistent naming**: Follow the same patterns across namespaces
- **Descriptive keys**: Keys should indicate where they're used

### Content

- **Avoid concatenation**: Don't build sentences from fragments
- **Include context**: Add comments for translators when meaning is ambiguous
- **Handle plurals properly**: Use ICU format for count-dependent text
- **Test all locales**: Verify translations display correctly

### Technical

- **Validate JSON**: Ensure all translation files are valid JSON
- **Keep files in sync**: All locales should have the same keys
- **Use TypeScript**: Let the compiler catch missing keys
- **Test with long strings**: Some languages are more verbose

## Common Mistakes

### String Concatenation

**Bad:**
```typescript
// Don't concatenate translated strings
t('hello') + ' ' + t('world')
```

**Good:**
```json
{
  "greeting": "Hello, {name}!"
}
```

```typescript
t('greeting', { name: 'World' })
```

### Hardcoded Text in Components

**Bad:**
```typescript
<Button>Submit</Button>
```

**Good:**
```typescript
<Button>{t('submitButton')}</Button>
```

### Inconsistent Key Naming

**Bad:**
```json
{
  "page_title": "...",
  "PageDescription": "...",
  "button-label": "..."
}
```

**Good:**
```json
{
  "pageTitle": "...",
  "pageDescription": "...",
  "buttonLabel": "..."
}
```
