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 messagesChoose 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 componentconst 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:
{ "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 templatecp packages/i18n/src/messages/en/projects.json packages/i18n/src/messages/es/projects.jsonThen 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; useLocalizedLink(@kit/ui/localized-link) only for dynamic/string hrefs.
2. Create Translation Directory
# Create the locale foldermkdir -p packages/i18n/src/messages/es# Copy all English files as templatescp 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 namespacesconst 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.json5. 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.jsonLanguage 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
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:
| 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
- Add keys in English first in the appropriate namespace
- Use the translation in your component
- Test the UI to verify text displays correctly
- 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 stringst('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": "..."}