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 apps/web/i18n/messages/{locale}/:
apps/web/i18n/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 Server Componentconst t = await getTranslations('common');const title = t('newFeature.title');// In a Client Componentconst t = useTranslations('common');const title = t('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. Register the Namespace
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 Translation File
Create apps/web/i18n/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" }}3. Create Files for Other Locales
If you support multiple languages, create the same file for each locale:
# Copy as templatecp apps/web/i18n/messages/en/projects.json apps/web/i18n/messages/es/projects.jsonThen translate the Spanish version.
Adding New Languages
1. Add Locale to Configuration
Edit packages/i18n/src/locales.tsx:
import { defaultLocale } from './default-locale';export const locales: string[] = [ defaultLocale, 'es', // Spanish 'fr', // French 'de', // German];2. Create Translation Directory
# Create the locale foldermkdir -p apps/web/i18n/messages/es# Copy all English files as templatescp apps/web/i18n/messages/en/*.json apps/web/i18n/messages/es/3. Translate Files
Translate each JSON file in the new locale directory:
apps/web/i18n/messages/es/├── common.json # Translate├── auth.json # Translate├── account.json # Translate├── organizations.json├── billing.json├── marketing.json├── settings.json├── goodbye.json└── errors.json4. Add Email Template Translations (Optional)
If your app sends transactional emails, add translations for email templates:
packages/email-templates/src/locales/es/├── welcome-email.json├── password-reset-email.json└── verification-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. This page only appears when 2 or more languages are configured.
Page file: apps/web/app/[locale]/(internal)/settings/preferences/page.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/routing';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 using
Intl.DisplayNames - Uses React transitions for smooth loading states
How Language Switching Works
When a user selects a new language:
useRouter().replace()navigates to the same path with the new locale- The middleware updates the locale cookie
- The page re-renders with the new language
// Internal implementationfunction useChangeLocale() { const pathname = usePathname(); const router = useRouter(); return useCallback((locale: string) => { startTransition(() => { router.replace(pathname, { locale }); }); }, [router, pathname]);}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 uses URL-based routing:
| URL | Locale |
|---|---|
/about | English (default, no prefix) |
/es/about | Spanish |
/fr/about | French |
The middleware also sets a NEXT_LOCALE cookie to remember the user's preference for subsequent visits.
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": "..."}