Managing Translations
How to add new messages, create namespaces, add languages, and configure the language selector for your users.
This guide covers the workflow for managing translations: adding new keys, creating namespaces, adding languages, and configuring the user-facing language selector.
Adding New Translation Keys
1. Find the Right Namespace
Translation files live in apps/web/i18n/messages/{locale}/:
| File | Purpose |
|---|---|
common.json | Shared UI elements, navigation, roles |
auth.json | Authentication flows |
account.json | User profile management |
organizations.json | Team management |
billing.json | Payments and subscriptions |
marketing.json | Public marketing pages |
settings.json | Settings and preferences |
goodbye.json | Account deletion |
errors.json | Error messages |
2. Add the Key
Open the appropriate namespace and add your key:
{ "existingKey": "Existing translation", "newFeature": { "title": "New Feature", "description": "Description of the new feature", "submitButton": "Get Started" }}3. Add to All Locales
If you support multiple languages, add the key to each locale:
{ "existingKey": "Traducción existente", "newFeature": { "title": "Nueva Funcionalidad", "description": "Descripción de la nueva funcionalidad", "submitButton": "Comenzar" }}Key Naming Conventions
| Pattern | Example | Use Case |
|---|---|---|
| camelCase | submitButton | Simple keys |
| Nested objects | form.errors.required | Related translations |
| Verb-based | createProject, deleteAccount | Actions |
| Suffix for context | titleLabel, titlePlaceholder | Form fields |
Good structure:
{ "createProject": { "title": "Create a New Project", "description": "Start a new project to organize your work", "form": { "nameLabel": "Project Name", "namePlaceholder": "Enter project name", "descriptionLabel": "Description" }, "submitButton": "Create Project", "cancelButton": "Cancel", "errors": { "nameRequired": "Project name is required", "nameTooLong": "Project name must be less than 100 characters" } }}Creating New Namespaces
For new features, create a dedicated namespace:
1. Update the Namespaces Array
Edit apps/web/i18n/request.ts:
const namespaces = [ 'common', 'auth', 'account', 'organizations', 'billing', 'marketing', 'settings', 'goodbye', 'errors', 'projects', // Add your new namespace] as const;2. Create the Namespace File
Create the file for each locale:
# Create for default localetouch apps/web/i18n/messages/en/projects.json{ "pageTitle": "Projects", "pageDescription": "Manage your projects", "createProject": "Create Project", "noProjects": "No projects yet. Create your first project to get started.", "deleteConfirmation": "Are you sure you want to delete this project? This action cannot be undone.", "table": { "name": "Name", "status": "Status", "created": "Created", "actions": "Actions" }}3. Create for Other Locales
# Spanishtouch apps/web/i18n/messages/es/projects.json# Frenchtouch apps/web/i18n/messages/fr/projects.jsonThen translate each file.
Adding a New Language
1. Enable the Locale
Edit packages/i18n/src/locales.tsx:
import { defaultLocale } from './default-locale';export const locales: string[] = [ defaultLocale, 'es', // Spanish 'fr', // French 'de', // German 'pt', // Portuguese];2. Create Translation Files
# Create the locale foldermkdir apps/web/i18n/messages/de# Copy English files as templatescp apps/web/i18n/messages/en/*.json apps/web/i18n/messages/de/3. Translate Each File
Open each JSON file and translate the values (not the keys):
{ "signUpHeading": "Konto erstellen", "signUp": "Registrieren", "signInHeading": "Anmelden", "signIn": "Anmelden"}4. Add Email Template Translations
If your app sends transactional emails, add translations for email templates. See Email Translations for details.
ISO 639-1 Language Codes
Use standard two-letter codes:
| Code | Language | Code | Language |
|---|---|---|---|
en | English | ja | Japanese |
es | Spanish | zh | Chinese |
fr | French | ko | Korean |
de | German | ar | Arabic |
it | Italian | hi | Hindi |
pt | Portuguese | ru | Russian |
Language Selector
Users can change their language preference in settings.
How It Works
The LanguagePreferenceCard component from @kit/ui/language-selector provides the UI:
import { LanguagePreferenceCard } from '@kit/ui/language-selector';import { locales } from '@kit/i18n/routing';export default function PreferencesPage() { return ( <LanguagePreferenceCard locales={locales} /> );}Behavior:
- Shows button grid for 3 or fewer languages
- Shows dropdown selector for 4+ languages
- Displays native language names (e.g., "Español" for Spanish, "Deutsch" for German)
- Handles loading state during locale transition
- Hidden when only one locale is configured
How Language Preference is Stored
1. URL Routing (Primary)
The locale is part of the URL:
/dashboardfor English (default, no prefix)/es/dashboardfor Spanish/fr/dashboardfor French
When users navigate, the URL determines the locale.
2. Cookie Storage (Server-side)
For operations like sending emails where there's no URL context, a NEXT_LOCALE cookie stores the preference:
// The cookie is set automatically by next-intl middleware// when the user visits a localized URLThis cookie is used by:
- Email templates
- Server-side operations triggered by webhooks
- API routes
Programmatic Locale Switching
Switch locale in client code:
'use client';import { useRouter, usePathname } from '@kit/i18n/navigation';import { useTransition } from 'react';function LanguageSwitcher() { const router = useRouter(); const pathname = usePathname(); const [isPending, startTransition] = useTransition(); function changeLocale(newLocale: string) { startTransition(() => { router.replace(pathname, { locale: newLocale }); }); } return ( <button onClick={() => changeLocale('es')} disabled={isPending}> {isPending ? 'Switching...' : 'Switch to Spanish'} </button> );}Translation Workflow Tips
Use a Translation Management Tool
For projects with many translations, consider:
- Crowdin or Lokalise for professional translation workflows
- i18n Ally VS Code extension for inline editing
- Export/import JSON files to translation services
Keep Keys Organized
// Good: grouped by feature{ "dashboard": { "title": "Dashboard", "stats": { ... }, "charts": { ... } }}// Avoid: flat and hard to navigate{ "dashboardTitle": "Dashboard", "dashboardStatsTotal": "Total", "dashboardStatsActive": "Active", "dashboardChartsLine": "Line Chart"}Add Context Comments
JSON doesn't support comments, but you can use a description pattern:
{ "_deleteConfirmation_context": "Shown when user clicks delete button", "deleteConfirmation": "Are you sure? This cannot be undone."}Or maintain a separate translation notes file.
Test All Locales
Regularly check that:
- No keys are missing in any locale
- Interpolation values render correctly
- Pluralization works for all languages
- Text doesn't overflow UI elements (German text is often 30% longer than English)
Handle Right-to-Left Languages
For RTL languages like Arabic or Hebrew:
- Add the locale to your array
- Update your layout to conditionally set
dir="rtl":
<html lang={locale} dir={locale === 'ar' ? 'rtl' : 'ltr'}>- Use Tailwind's RTL utilities or CSS logical properties
Debugging Missing Translations
Development Warnings
In development, missing translations log warnings:
[Dev Only] i18n error: Missing message: projects.newKeyFinding Missing Keys
Create a script to compare locale files:
# Quick diff between localesdiff <(jq 'keys' apps/web/i18n/messages/en/common.json | sort) \ <(jq 'keys' apps/web/i18n/messages/es/common.json | sort)Fallback Behavior
Missing translations return the key name. Configure this in apps/web/i18n/request.ts:
getMessageFallback(info) { // Log for monitoring console.warn(`Missing translation: ${info.namespace}.${info.key}`); // Return key as fallback (or customize) return info.key;}Best Practices Summary
- One namespace per feature keeps files manageable
- Nest related keys for better organization
- Use descriptive key names that indicate context
- Add keys to all locales immediately to avoid missing translations
- Test with longer languages (German, French) to catch UI overflow
- Use ICU format for plurals and complex formatting
- Keep translations close to code using the same namespace naming
- Review translations in context not just the JSON files
Frequently Asked Questions
How do I add a new language to MakerKit?
Where does the language selector appear?
How is the user's language preference stored?
What naming convention should I use for translation keys?
How do I handle right-to-left (RTL) languages like Arabic?
How can I find missing translations?
Previous: Using Translations | Next: Email Translations