# Adding new translations

> Learn how to add new languages, create translation files, and organize namespaces in your Tanstack Start Supabase SaaS application.

*Canonical: https://makerkit.dev/docs/tanstack-supabase/translations/adding-translations*

---

This guide covers adding new languages, creating translation files, and organizing your translations into namespaces.

{% sequence title="Steps to add new translations" description="Learn how to add new translations to your Tanstack Start Supabase SaaS project." %}

[Create language files](#1-create-language-files)

[Register the language](#2-register-the-language)

[Add custom namespaces](#3-add-custom-namespaces)

[Translate email templates](#4-translate-email-templates)

{% /sequence %}

## 1. Create Language Files

Translation files live in `packages/i18n/src/messages/{locale}/`. Each language needs its own folder with JSON files matching your namespaces.

### Create the Language Folder

Create a new folder using the [ISO 639-1 language code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes):

```bash
mkdir packages/i18n/src/messages/es
```

Common language codes:
- `de` - German
- `es` - Spanish
- `fr` - French
- `it` - Italian
- `ja` - Japanese
- `pt` - Portuguese
- `zh` - Chinese

### Regional Language Codes

For regional variants like `es-ES` (Spanish - Spain) or `pt-BR` (Portuguese - Brazil), use lowercase with a hyphen:

```bash
# Correct
mkdir packages/i18n/src/messages/es-es
mkdir packages/i18n/src/messages/pt-br

# Incorrect - will not work
mkdir packages/i18n/src/messages/es-ES
```

The system normalizes language codes to lowercase internally.

### Copy and Translate Files

Copy the English files as a starting point:

```bash
cp packages/i18n/src/messages/en/*.json packages/i18n/src/messages/es/
```

Then translate each JSON file. Here's an example for `common.json`:

```json title="packages/i18n/src/messages/es/common.json"
{
  "homeTabLabel": "Inicio",
  "cancel": "Cancelar",
  "clear": "Limpiar",
  "goBack": "Volver",
  "tryAgain": "Intentar de nuevo",
  "loading": "Cargando. Por favor espere...",
  "routes": {
    "home": "Inicio",
    "account": "Cuenta",
    "billing": "Facturacion"
  }
}
```

Keep the same key structure as the English files. Only translate the values.

## 2. Register the Language

Add your new language to the locales configuration:

```tsx title="packages/i18n/src/config.ts" {6}
/**
 * The list of supported locales.
 * Add more locales here as needed.
 */
export const locales: string[] = ['en', 'es', 'de', 'fr'];
```

The order matters for fallback behavior:
1. First locale is the default fallback
2. When a translation is missing, the system falls back through this list

### Verify the Registration

After adding a language, verify it works:

1. Restart the development server
2. Switch to the new language with the Language Selector in account settings (shown automatically once more than one locale is registered)
3. You should see your translations appear

## 3. Add Custom Namespaces

Namespaces organize translations by feature or domain. Each namespace is a JSON file imported into the message registry at `packages/i18n/src/messages/index.ts`:

```tsx title="packages/i18n/src/messages/index.ts"
import enAccount from './en/account.json';
import enAuth from './en/auth.json';
import enBilling from './en/billing.json';
import enCommon from './en/common.json';
import enMarketing from './en/marketing.json';
import enTeams from './en/teams.json';

const registry: Record<string, Messages> = {
  en: {
    common: enCommon,      // Shared UI elements
    auth: enAuth,          // Authentication flows
    account: enAccount,    // Account settings
    teams: enTeams,        // Team management
    billing: enBilling,    // Billing and subscriptions
    marketing: enMarketing, // Marketing pages
  },
};
```

### Create a New Namespace

#### Create the JSON file for each language:

```bash
# Create for English
touch packages/i18n/src/messages/en/projects.json

# Create for other languages
touch packages/i18n/src/messages/es/projects.json
```

#### Add your translations

```json title="packages/i18n/src/messages/en/projects.json"
{
  "title": "Projects",
  "createProject": "Create Project",
  "projectName": "Project Name",
  "projectDescription": "Description",
  "deleteProject": "Delete Project",
  "confirmDelete": "Are you sure you want to delete this project?",
  "status": {
    "active": "Active",
    "archived": "Archived",
    "draft": "Draft"
  }
}
```

#### Register the namespace:

Import the JSON file and add it to the registry for each locale:

```tsx title="packages/i18n/src/messages/index.ts" {2,6}
// ...existing imports
import enProjects from './en/projects.json';

const registry: Record<string, Messages> = {
  en: {
    // ...existing namespaces
    projects: enProjects, // Your new namespace
  },
};
```

#### Use the namespace in your components:

```tsx title="apps/web/src/routes/_authenticated/dashboard/projects/index.tsx"
import { Trans } from '@kit/ui/trans';

function ProjectsPage() {
  return (
    <div>
      <h1>
        <Trans i18nKey="projects.title" />
      </h1>

      <button>
        <Trans i18nKey="projects.createProject" />
      </button>
    </div>
  );
}
```

### Namespace Best Practices

**Keep namespaces focused**: Each namespace should cover a single feature or domain.

```
Good:
- projects.json (project management)
- invoices.json (invoicing feature)
- notifications.json (notification system)

Avoid:
- misc.json (too vague)
- page1.json (not semantic)
```

**Use consistent key naming**:

```json
{
  "title": "Page title",
  "description": "Page description",
  "actions": {
    "create": "Create",
    "edit": "Edit",
    "delete": "Delete"
  },
  "status": {
    "loading": "Loading...",
    "error": "An error occurred",
    "success": "Success!"
  }
}
```

**Avoid duplicating common strings**: Use the `common` namespace for shared strings like "Cancel", "Save", "Loading".

## 4. Translate Email Templates

Email templates have their own translation system in `packages/email-templates/src/locales/`.

### Email Translation Structure

```
packages/email-templates/src/locales/
└── en/
    ├── account-delete-email.json
    ├── invite-email.json
    └── otp-email.json
```

### Add Email Translations for a New Language

#### Create the language folder:

```bash
mkdir packages/email-templates/src/locales/es
```

#### Copy and translate the email files:

```bash
cp packages/email-templates/src/locales/en/*.json packages/email-templates/src/locales/es/
```

#### Translate the content:

```json title="packages/email-templates/src/locales/es/invite-email.json"
{
  "subject": "Has sido invitado a unirte a un equipo",
  "heading": "Unete a {teamName} en {productName}",
  "hello": "Hola {invitedUserEmail},",
  "mainText": "<strong>{inviter}</strong> te ha invitado al equipo <strong>{teamName}</strong> en <strong>{productName}</strong>.",
  "joinTeam": "Unirse a {teamName}",
  "copyPasteLink": "o copia y pega esta URL en tu navegador:",
  "invitationIntendedFor": "Esta invitacion es para {invitedUserEmail}."
}
```

Email templates support interpolation with `{variable}` syntax and basic HTML tags.

## Organizing Large Translation Files

For applications with many translations, consider splitting by feature:

```
packages/i18n/src/messages/en/
├── common.json           # 50-100 keys max
├── auth.json
├── account.json
├── billing/
│   ├── subscriptions.json
│   ├── invoices.json
│   └── checkout.json
└── features/
    ├── projects.json
    ├── analytics.json
    └── integrations.json
```

Import each nested file and register it under a namespace key in the registry:

```tsx title="packages/i18n/src/messages/index.ts"
import enBillingSubscriptions from './en/billing/subscriptions.json';
import enBillingInvoices from './en/billing/invoices.json';
import enFeaturesProjects from './en/features/projects.json';

const registry: Record<string, Messages> = {
  en: {
    common: enCommon,
    auth: enAuth,
    account: enAccount,
    'billing/subscriptions': enBillingSubscriptions,
    'billing/invoices': enBillingInvoices,
    'features/projects': enFeaturesProjects,
  },
};
```

## Translation Workflow Tips

### Use Placeholders During Development

When adding new features, start with English placeholders:

```json
{
  "newFeature": "[TODO] New feature title",
  "newFeatureDescription": "[TODO] Description of the new feature"
}
```

This makes untranslated strings visible and searchable.

### Maintain Translation Parity

Keep all language files in sync. When adding a key to one language, add it to all:

```bash
# Check for missing keys (example script)
diff <(jq -r 'keys[]' messages/en/common.json | sort) \
     <(jq -r 'keys[]' messages/es/common.json | sort)
```

### Consider Translation Services

For production applications, integrate with translation services:

- [Crowdin](https://crowdin.com/)
- [Lokalise](https://lokalise.com/)
- [Phrase](https://phrase.com/)

These services can:
- Sync with your JSON files via CLI or CI/CD
- Provide translator interfaces
- Handle pluralization rules per language
- Track translation coverage

## RTL Language Support

For right-to-left languages like Arabic (`ar`) or Hebrew (`he`):

1. Add the language as normal to `packages/i18n/src/config.ts`
2. Create a client component to detect the current locale and set the `dir` attribute:

```tsx title="apps/web/src/components/rtl-provider.tsx"
import { useEffect } from 'react';
import { useLocale } from 'use-intl';

const rtlLanguages = ['ar', 'he', 'fa', 'ur'];

export function RtlProvider({ children }: { children: React.ReactNode }) {
  const locale = useLocale();

  useEffect(() => {
    const isRtl = rtlLanguages.includes(locale);
    document.documentElement.dir = isRtl ? 'rtl' : 'ltr';
    document.documentElement.lang = locale;
  }, [locale]);

  return children;
}
```

3. Wrap your app with the provider in `RootProviders`:

```tsx title="apps/web/src/components/root-providers.tsx"
import { RtlProvider } from './rtl-provider';

export function RootProviders({ children }: { children: React.ReactNode }) {
  return (
    <RtlProvider>
      {children}
    </RtlProvider>
  );
}
```

4. Use Tailwind's RTL utilities (`rtl:` prefix) for layout adjustments:

```tsx
<div className="ml-4 rtl:ml-0 rtl:mr-4">
  {/* Content flows correctly in both directions */}
</div>
```

{% faq
   title="Frequently Asked Questions"
   items=[
     {"question": "How do I verify my translations are working?", "answer": "Use the Language Selector in account settings to switch languages. It appears automatically once more than one locale is registered in packages/i18n/src/config.ts."},
     {"question": "Do I need to translate every single key?", "answer": "No. Missing translations fall back to the default language (usually English). During development, you can translate incrementally. For production, ensure all user-facing strings are translated."},
     {"question": "Can I use nested folders for namespaces?", "answer": "Yes. Create subfolders like billing/subscriptions.json, import the file, and register it under the 'billing/subscriptions' key in the registry in packages/i18n/src/messages/index.ts."},
     {"question": "How do I handle pluralization in different languages?", "answer": "use-intl uses ICU message format for pluralization. Define plural rules like {count, plural, one {# item} other {# items}}. ICU format automatically handles language-specific plural rules (e.g., Russian's complex plural categories)."},
     {"question": "Should translation files be committed to git?", "answer": "Yes, translation JSON files should be version controlled. If using a translation management service, configure it to sync with your repository via pull requests."}
   ]
/%}

## Related Documentation

- [Using Translations](/docs/tanstack-supabase/translations/using-translations) - Use translations in your components
- [Language Selector](/docs/tanstack-supabase/translations/language-selector) - Add a language switcher
- [Email Translations](/docs/tanstack-supabase/translations/email-translations) - Translate email templates
