Skip to main content

Internationalization (i18n)

The application uses i18next and react-i18next for internationalization, following industry-standard frontend translation management.

Overviewโ€‹

Translations are stored as JSON files in the codebase, providing:

  • Zero latency (instant loading)
  • Type safety with TypeScript
  • Git-based workflow (translations versioned with code)
  • Offline development support
  • Reliable, no network dependencies

Project Structureโ€‹

apps/web/src/
โ”œโ”€โ”€ i18n/
โ”‚ โ”œโ”€โ”€ config.ts # i18next configuration
โ”‚ โ””โ”€โ”€ types.ts # TypeScript type definitions
โ””โ”€โ”€ locales/
โ”œโ”€โ”€ en/ # English (default)
โ”œโ”€โ”€ de/ # German
โ”œโ”€โ”€ es/ # Spanish
โ”œโ”€โ”€ fr/ # French
โ”œโ”€โ”€ pt/ # Portuguese
โ”œโ”€โ”€ zh-CN/ # Simplified Chinese
โ””โ”€โ”€ pseudo/ # Pseudo locale (debug)
# Each directory contains:
โ”œโ”€โ”€ common.json # Shared translations (buttons, errors, admin, discord, theme)
โ”œโ”€โ”€ guild.json # Guild-specific translations
โ”œโ”€โ”€ character.json # Character-specific translations
โ”œโ”€โ”€ roster.json # Roster-specific translations
โ”œโ”€โ”€ auth.json # Login and account settings
โ”œโ”€โ”€ posts.json # Guild posts (TOC)
โ””โ”€โ”€ events.json # Events (types, roster, signup, manage, confirmations)

Supported Languagesโ€‹

CodeLanguageNative Name
enEnglishEnglish
deGermanDeutsch
esSpanishEspaรฑol
frFrenchFranรงais
ptPortuguesePortuguรชs
zh-CNSimplified Chineseไธญๆ–‡ (็ฎ€ไฝ“)
pseudoDebug๐Ÿ”ค Pseudo

Configurationโ€‹

Main Setup (apps/web/src/i18n/config.ts)โ€‹

import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';

i18n
.use(LanguageDetector) // Auto-detect from browser
.use(initReactI18next) // React integration
.init({
resources,
defaultNS: 'common',
fallbackLng: 'en',
detection: {
order: ['localStorage', 'navigator'],
caches: ['localStorage'],
},
});

TypeScript Types (apps/web/src/i18n/types.ts)โ€‹

Provides type-safe translation keys with autocomplete in your IDE.

Basic Usageโ€‹

Simple Translationsโ€‹

import { useTranslation } from 'react-i18next';

function MyComponent() {
const { t } = useTranslation();

return (
<div>
<h1>{t('app.name')}</h1>
<button>{t('actions.save')}</button>
<button>{t('actions.cancel')}</button>
</div>
);
}

Namespaced Translationsโ€‹

// Use guild namespace
const { t } = useTranslation('guild');

return (
<div>
<h2>{t('title')}</h2>
<p>{t('messages.created')}</p>
</div>
);

Multiple Namespacesโ€‹

const { t } = useTranslation(['common', 'guild']);

return (
<div>
<button>{t('common:actions.save')}</button>
<p>{t('guild:messages.created')}</p>
</div>
);

Advanced Featuresโ€‹

Interpolation (Dynamic Values)โ€‹

// Translation JSON
{
"guild": {
"createdBy": "Created by {{name}}",
"memberCount": "{{count}} members"
}
}

// Component
const { t } = useTranslation('guild');
<p>{t('createdBy', { name: 'John' })}</p>
<p>{t('memberCount', { count: 42 })}</p>

Pluralizationโ€‹

i18next automatically handles plurals:

{
"member_one": "{{count}} member",
"member_other": "{{count}} members"
}
const { t } = useTranslation('guild');
<p>{t('member', { count: 1 })}</p> // "1 member"
<p>{t('member', { count: 5 })}</p> // "5 members"

Change Languageโ€‹

const { i18n } = useTranslation();

const changeLanguage = (lng: string) => {
i18n.changeLanguage(lng);
};

<button onClick={() => changeLanguage('en')}>English</button>
<button onClick={() => changeLanguage('es')}>Espaรฑol</button>

Translation File Structureโ€‹

Common (locales/en/common.json)โ€‹

Shared translations used across the entire app:

  • Actions (save, cancel, delete, etc.)
  • Validation messages
  • Error messages
  • Status labels

Domain-Specific Filesโ€‹

Each major feature has its own namespace:

  • guild.json - Guild management
  • character.json - Character management
  • roster.json - Roster operations
  • auth.json - Login, callback, account settings
  • posts.json - Guild posts (create, edit, delete, history)
  • events.json - Events (types, statuses, manage actions, roster, voice attendance, confirmations)

Benefits:

  • Smaller file sizes (only load what you need)
  • Better organization
  • Easier to maintain
  • Multiple people can work on different namespaces

Backend API and translation keysโ€‹

When the API returns errors, it can include a messageKey (and optional messageParams) so the frontend can show a translated message. See API Translation Keys for the convention and how to consume messageKey in toasts and error UIs.

Integration with Toast Notificationsโ€‹

import { useTranslation } from 'react-i18next';
import { useToastMutations } from '@/hooks/useToastMutations';

function CreateGuild() {
const { t } = useTranslation('guild');
const { toastPromise } = useToastMutations();

const handleCreate = async (data) => {
await toastPromise(createGuild(data).unwrap(), {
loading: t('messages.creating'),
success: t('messages.created'),
error: t('messages.createError'),
});
};
}

Adding a New Languageโ€‹

1. Create Translation Filesโ€‹

mkdir -p apps/web/src/locales/ja
cp apps/web/src/locales/en/*.json apps/web/src/locales/ja/

2. Translate JSON Filesโ€‹

Edit each .json file in locales/ja/ with Japanese translations.

3. Update i18n Configโ€‹

// apps/web/src/i18n/config.ts
import commonJA from '../locales/ja/common.json';
import guildJA from '../locales/ja/guild.json';
// ... other imports (one per namespace)

export const resources = {
en: {
/* ... */
},
// existing languages ...
ja: {
common: commonJA,
guild: guildJA,
character: characterJA,
roster: rosterJA,
auth: authJA,
posts: postsJA,
events: eventsJA,
},
} as const;

4. Add to LanguageSelectorโ€‹

// apps/web/src/components/LanguageSelector.tsx
const LANGUAGES = [
// ... existing entries
{ code: 'ja', label: 'ๆ—ฅๆœฌ่ชž' },
] as const;

Best Practicesโ€‹

DO โœ…โ€‹

  • Extract all user-facing strings - Even English text should use t()
  • Use meaningful keys - guild.messages.created not msg1
  • Group related keys - Use nested objects for organization
  • Use interpolation - t('welcome', { name }) instead of string concatenation
  • Namespace by feature - Separate guild, character, roster
  • Add context - Use namespaces like guild:actions.delete for clarity

DON'T โŒโ€‹

  • Hard-code strings - Even in English, use translation keys
  • Concatenate strings - Use interpolation instead
  • Duplicate keys - Share common translations via common namespace
  • Use generic keys - button1 is meaningless, use actions.save
  • Mix languages - Keep one language per file
  • Nest too deeply - 2-3 levels max for readability

Type Safetyโ€‹

TypeScript provides autocomplete and validation:

const { t } = useTranslation('guild');

t('messages.created'); // โœ… Valid key - autocomplete works
t('messages.invalid'); // โŒ TypeScript error - key doesn't exist

Language Detectionโ€‹

The app auto-detects user language:

  1. localStorage - Previously selected language
  2. Browser navigator - Browser language setting
  3. Fallback - English (en)

Users can manually change language, which is saved to localStorage.

Performanceโ€‹

  • Zero network requests - All translations bundled with app
  • Code splitting - Only load namespaces you use
  • Instant language switching - No loading states needed
  • Caching - Selected language persists across sessions

Migration Strategyโ€‹

From Hard-Coded Stringsโ€‹

// Before
<button>Create Guild</button>
<p>Guild created successfully!</p>

// After
<button>{t('guild:create')}</button>
<p>{t('guild:messages.created')}</p>

Extract Incrementallyโ€‹

  1. Start with high-traffic pages (dashboard, guild list)
  2. Extract as you touch files (no need for big-bang migration)
  3. Search for hard-coded strings: grep -r "Guild created" src/

Translation Management Services (Future)โ€‹

When you scale to 3+ languages and need non-developers to translate:

Recommended Services:

  • Lokalise - Developer-friendly, GitHub integration
  • Crowdin - Community translations, good for open source
  • Phrase - Enterprise-grade with automation

Workflow:

  1. Developer adds key: t('guild.newFeature')
  2. Service detects missing translations
  3. Translators fill in via web UI
  4. Service creates PR with updated JSON files
  5. Merge and deploy

Troubleshootingโ€‹

Translation Not Showingโ€‹

  1. Check key exists in JSON file
  2. Verify namespace is loaded in component
  3. Check i18n initialization in main.tsx
  4. Look for typos in translation key

TypeScript Errorsโ€‹

  1. Restart TypeScript server in IDE
  2. Ensure i18n/types.ts is properly configured
  3. Verify translation files are imported in i18n/config.ts

Language Not Changingโ€‹

  1. Check i18n.changeLanguage() is called correctly
  2. Verify language code matches resources (en, es, etc.)
  3. Check browser console for i18n errors
  4. Clear localStorage if old language is stuck

Examplesโ€‹

See apps/web/src/examples/I18nExamples.tsx for complete usage examples.