Managing Translations

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

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:

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

3. Use the Translation

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

ConventionExampleUse Case
camelCasepageTitleAll keys
Nested objectsdialog.titleRelated translations
Descriptive namescreateProjectButtonSelf-documenting keys
Component prefixinviteDialog.titleComponent-specific text

Example structure:

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

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

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:

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

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

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

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:

CodeLanguageNative Name
enEnglishEnglish
esSpanishEspanol
frFrenchFrancais
deGermanDeutsch
itItalianItaliano
ptPortuguesePortugues
jaJapaneseNihongo
zhChineseZhongwen
koKoreanHangugeo
arArabicAl-Arabiyyah

Language Selector UI

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

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:

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

MechanismValueNotes
locale cookieen, es, ...Set by the language selector; read by detectLocale()
Accept-Language headerbrowser localesFallback when no cookie is present
defaultLocaleenFinal 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:

// Don't concatenate translated strings
t('hello') + ' ' + t('world')

Good:

{
"greeting": "Hello, {name}!"
}
t('greeting', { name: 'World' })

Hardcoded Text in Components

Bad:

<Button>Submit</Button>

Good:

<Button>{t('submitButton')}</Button>

Inconsistent Key Naming

Bad:

{
"page_title": "...",
"PageDescription": "...",
"button-label": "..."
}

Good:

{
"pageTitle": "...",
"pageDescription": "...",
"buttonLabel": "..."
}