Adding new translations

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

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

Steps to add new translations

Learn how to add new translations to your Tanstack Start Supabase SaaS project.

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:

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:

# 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:

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

Then translate each JSON file. Here's an example for 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:

/**
* 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:

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:

# 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

{
"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:

// ...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:

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:

{
"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:

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

Copy and translate the email files:

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

Translate the content:

{
"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:

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:

{
"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:

# 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:

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:
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;
}
  1. Wrap your app with the provider in RootProviders:
import { RtlProvider } from './rtl-provider';
export function RootProviders({ children }: { children: React.ReactNode }) {
return (
<RtlProvider>
{children}
</RtlProvider>
);
}
  1. Use Tailwind's RTL utilities (rtl: prefix) for layout adjustments:
<div className="ml-4 rtl:ml-0 rtl:mr-4">
{/* Content flows correctly in both directions */}
</div>

Frequently Asked Questions

How do I verify my translations are working?
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.
Do I need to translate every single key?
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.
Can I use nested folders for namespaces?
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.
How do I handle pluralization in different languages?
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).
Should translation files be committed to git?
Yes, translation JSON files should be version controlled. If using a translation management service, configure it to sync with your repository via pull requests.