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 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 Server Component
const t = await getTranslations('common');
const title = t('newFeature.title');
// In a Client Component
const 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:

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. 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 template
cp apps/web/i18n/messages/en/projects.json apps/web/i18n/messages/es/projects.json

Then 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 folder
mkdir -p apps/web/i18n/messages/es
# Copy all English files as templates
cp 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.json

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

  1. useRouter().replace() navigates to the same path with the new locale
  2. The middleware updates the locale cookie
  3. The page re-renders with the new language
// Internal implementation
function 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:

URLLocale
/aboutEnglish (default, no prefix)
/es/aboutSpanish
/fr/aboutFrench

The middleware also sets a NEXT_LOCALE cookie to remember the user's preference for subsequent visits.

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": "..."
}