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โ
| Code | Language | Native Name |
|---|---|---|
en | English | English |
de | German | Deutsch |
es | Spanish | Espaรฑol |
fr | French | Franรงais |
pt | Portuguese | Portuguรชs |
zh-CN | Simplified Chinese | ไธญๆ (็ฎไฝ) |
pseudo | Debug | ๐ค 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 managementcharacter.json- Character managementroster.json- Roster operationsauth.json- Login, callback, account settingsposts.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.creatednotmsg1 - 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.deletefor clarity
DON'T โโ
- Hard-code strings - Even in English, use translation keys
- Concatenate strings - Use interpolation instead
- Duplicate keys - Share common translations via
commonnamespace - Use generic keys -
button1is meaningless, useactions.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:
- localStorage - Previously selected language
- Browser navigator - Browser language setting
- 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โ
- Start with high-traffic pages (dashboard, guild list)
- Extract as you touch files (no need for big-bang migration)
- 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:
- Developer adds key:
t('guild.newFeature') - Service detects missing translations
- Translators fill in via web UI
- Service creates PR with updated JSON files
- Merge and deploy
Troubleshootingโ
Translation Not Showingโ
- Check key exists in JSON file
- Verify namespace is loaded in component
- Check i18n initialization in
main.tsx - Look for typos in translation key
TypeScript Errorsโ
- Restart TypeScript server in IDE
- Ensure
i18n/types.tsis properly configured - Verify translation files are imported in
i18n/config.ts
Language Not Changingโ
- Check
i18n.changeLanguage()is called correctly - Verify language code matches resources (
en,es, etc.) - Check browser console for i18n errors
- Clear localStorage if old language is stuck
Examplesโ
See apps/web/src/examples/I18nExamples.tsx for complete usage examples.