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

FilePurpose
common.jsonShared UI elements, navigation, roles
auth.jsonAuthentication flows
account.jsonUser profile management
organizations.jsonTeam management
billing.jsonPayments and subscriptions
marketing.jsonPublic marketing pages
settings.jsonSettings and preferences
goodbye.jsonAccount deletion
errors.jsonError 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

PatternExampleUse Case
camelCasesubmitButtonSimple keys
Nested objectsform.errors.requiredRelated translations
Verb-basedcreateProject, deleteAccountActions
Suffix for contexttitleLabel, titlePlaceholderForm 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 locale
touch 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

# Spanish
touch apps/web/i18n/messages/es/projects.json
# French
touch apps/web/i18n/messages/fr/projects.json

Then 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 folder
mkdir apps/web/i18n/messages/de
# Copy English files as templates
cp 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:

CodeLanguageCodeLanguage
enEnglishjaJapanese
esSpanishzhChinese
frFrenchkoKorean
deGermanarArabic
itItalianhiHindi
ptPortugueseruRussian

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:

  • /dashboard for English (default, no prefix)
  • /es/dashboard for Spanish
  • /fr/dashboard for 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 URL

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

  1. No keys are missing in any locale
  2. Interpolation values render correctly
  3. Pluralization works for all languages
  4. 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:

  1. Add the locale to your array
  2. Update your layout to conditionally set dir="rtl":
<html lang={locale} dir={locale === 'ar' ? 'rtl' : 'ltr'}>
  1. 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.newKey

Finding Missing Keys

Create a script to compare locale files:

# Quick diff between locales
diff <(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

  1. One namespace per feature keeps files manageable
  2. Nest related keys for better organization
  3. Use descriptive key names that indicate context
  4. Add keys to all locales immediately to avoid missing translations
  5. Test with longer languages (German, French) to catch UI overflow
  6. Use ICU format for plurals and complex formatting
  7. Keep translations close to code using the same namespace naming
  8. Review translations in context not just the JSON files

Frequently Asked Questions

How do I add a new language to MakerKit?
1) Add the locale code to packages/i18n/src/locales.tsx, 2) Create a folder at apps/web/i18n/messages/{locale}/, 3) Copy all JSON files from the en/ folder and translate them, 4) Restart the dev server. The language selector automatically appears when multiple locales are configured.
Where does the language selector appear?
The language selector appears in the user settings at /settings/preferences. It uses the LanguagePreferenceCard component from @kit/ui/language-selector. It only shows when you have 2 or more languages configured in your locales array.
How is the user's language preference stored?
Language preference is stored in two ways: 1) The URL path determines the current locale (e.g., /es/dashboard for Spanish), 2) A NEXT_LOCALE cookie stores the preference for server-side operations like sending emails. The middleware sets this cookie automatically.
What naming convention should I use for translation keys?
Use camelCase for keys (submitButton, pageTitle). Group related translations in nested objects. Use verb-based names for actions (createProject, deleteAccount). Add suffixes for context (nameLabel, namePlaceholder). Keep keys descriptive but concise.
How do I handle right-to-left (RTL) languages like Arabic?
Add the locale to your locales array, then update your root layout to set dir='rtl' conditionally: <html lang={locale} dir={locale === 'ar' ? 'rtl' : 'ltr'}>. Use Tailwind's RTL utilities or CSS logical properties for layout.
How can I find missing translations?
In development, missing translations log warnings to the console. You can also compare locale files using diff commands: diff <(jq 'keys' en/common.json | sort) <(jq 'keys' es/common.json | sort). Consider using i18n Ally VS Code extension for visual tracking.

Previous: Using Translations | Next: Email Translations