internationalization - nself-org/nchat GitHub Wiki
Last Updated: January 31, 2026 Version: 1.0.0
nself-chat includes a comprehensive internationalization (i18n) framework that supports multi-language content, RTL (right-to-left) languages, and locale-specific formatting. This guide covers everything you need to know about using and contributing translations.
- Supported Languages
- Architecture
- Using Translations
- Translation Structure
- RTL Support
- Date and Number Formatting
- Contributing Translations
- Testing Translations
- Best Practices
- Troubleshooting
nself-chat currently supports the following languages:
| Language | Code | Direction | Completion | Native Name |
|---|---|---|---|---|
| English | en |
LTR | 100% | English |
| Spanish | es |
LTR | 100% | Espaรฑol |
| French | fr |
LTR | 100% | Franรงais |
| German | de |
LTR | 100% | Deutsch |
| Chinese (Simplified) | zh |
LTR | 100% | ไธญๆ |
| Arabic | ar |
RTL | 100% | ุงูุนุฑุจูุฉ |
| Japanese | ja |
LTR | 95% | ๆฅๆฌ่ช |
| Portuguese | pt |
LTR | 95% | Portuguรชs |
| Russian | ru |
LTR | 95% | ะ ัััะบะธะน |
- Hebrew (he) - RTL support
- Korean (ko)
- Italian (it)
- Dutch (nl)
- Turkish (tr)
- Hindi (hi)
The i18n system consists of several key components:
src/
โโโ lib/i18n/
โ โโโ locales.ts # Locale configurations
โ โโโ translator.ts # Translation engine
โ โโโ i18n-config.ts # Configuration
โ โโโ plurals.ts # Pluralization rules
โ โโโ date-formats.ts # Date/time formatting
โ โโโ number-formats.ts # Number formatting
โ โโโ rtl.ts # RTL support
โ โโโ language-detector.ts # Browser detection
โ โโโ index.ts # Public API
โโโ stores/
โ โโโ locale-store.ts # Zustand state management
โโโ components/i18n/
โ โโโ LocaleProvider.tsx # React context
โ โโโ LanguageSwitcher.tsx # UI component
โ โโโ TranslatedText.tsx # Text component
โ โโโ FormattedDate.tsx # Date formatting
โ โโโ FormattedNumber.tsx # Number formatting
โ โโโ RTLWrapper.tsx # RTL layout
โโโ locales/
โโโ en/ # English translations
โ โโโ common.json
โ โโโ chat.json
โ โโโ settings.json
โ โโโ admin.json
โโโ es/ # Spanish translations
โโโ fr/ # French translations
โโโ de/ # German translations
โโโ zh/ # Chinese translations
โโโ ar/ # Arabic translations
โโโ ja/ # Japanese translations
โโโ pt/ # Portuguese translations
โโโ ru/ # Russian translations
Translations are loaded dynamically using code-splitting:
// Dynamic import
const translations = await import(`@/locales/${locale}/${namespace}.json`)This ensures:
- Small initial bundle size
- On-demand loading of languages
- Automatic code-splitting per locale
- Faster page loads
import { translate, t } from '@/lib/i18n/translator'
// Using translate function
const text = translate('common:app.name')
// Using shorthand
const text = t('common:app.name')
// With default namespace (assumes 'common')
const text = t('app.name')// Simple interpolation
const text = t('time.ago', {
values: { time: '5 minutes' },
})
// Output: "5 minutes ago"
// Multiple values
const text = t('validation.minLength', {
values: { min: 8 },
})
// Output: "Must be at least 8 characters"// Pluralization (automatic based on count)
const text = t('time.minutes', {
count: 1,
})
// Output: "1 minute"
const text = t('time.minutes', {
count: 5,
})
// Output: "5 minutes"import { useTranslation } from '@/hooks/use-translation';
function MyComponent() {
const { t, locale, setLocale } = useTranslation();
return (
<div>
<h1>{t('app.welcome')}</h1>
<p>Current locale: {locale}</p>
</div>
);
}import { TranslatedText } from '@/components/i18n/TranslatedText';
import { FormattedDate } from '@/components/i18n/FormattedDate';
import { FormattedNumber } from '@/components/i18n/FormattedNumber';
function MyComponent() {
return (
<div>
{/* Simple translation */}
<TranslatedText i18nKey="app.name" />
{/* With interpolation */}
<TranslatedText
i18nKey="time.ago"
values={{ time: '5 minutes' }}
/>
{/* Date formatting */}
<FormattedDate date={new Date()} format="long" />
{/* Number formatting */}
<FormattedNumber value={1234.56} style="currency" currency="USD" />
</div>
);
}Translations are organized into namespaces:
{
"app": {
"name": "nChat",
"tagline": "Team Communication Platform",
"loading": "Loading...",
"error": "An error occurred"
},
"navigation": { ... },
"time": { ... },
"validation": { ... },
"errors": { ... },
"status": { ... },
"notifications": { ... },
"confirmations": { ... },
"empty": { ... },
"accessibility": { ... },
"language": { ... }
}{
"messages": { ... },
"channels": { ... },
"threads": { ... },
"directMessages": { ... },
"mentions": { ... },
"files": { ... },
"search": { ... },
"presence": { ... },
"notifications": { ... },
"members": { ... },
"reactions": { ... },
"formatting": { ... }
}{
"settings": { ... },
"profile": { ... },
"account": { ... },
"appearance": { ... },
"notifications": { ... },
"privacy": { ... },
"language": { ... },
"accessibility": { ... },
"advanced": { ... },
"about": { ... }
}{
"admin": { ... },
"dashboard": { ... },
"users": { ... },
"roles": { ... },
"channels": { ... },
"moderation": { ... },
"analytics": { ... },
"settings": { ... },
"integrations": { ... },
"logs": { ... },
"setup": { ... }
}-
Use dot notation:
section.subsection.key -
Be descriptive:
channels.createPublicnotchannels.cp -
Group related keys: All button text under
buttons.* -
Plural forms: Use
_oneand_othersuffixes{ "messages_one": "{{count}} message", "messages_other": "{{count}} messages" }
Use {{variable}} for variable interpolation:
{
"welcome": "Welcome, {{name}}!",
"itemsSelected": "{{count}} items selected",
"validation": {
"minLength": "Must be at least {{min}} characters"
}
}- Arabic (
ar) - Hebrew (
he) - planned
The i18n system automatically detects RTL languages and applies appropriate styles:
// Automatic detection and application
import { getDirection, applyDocumentDirection } from '@/lib/i18n/rtl'
const direction = getDirection('ar') // 'rtl'
applyDocumentDirection('ar') // Applies dir="rtl" to <html>import { RTLWrapper } from '@/components/i18n/RTLWrapper';
function MyComponent() {
return (
<RTLWrapper>
<div>Content automatically adapts to RTL</div>
</RTLWrapper>
);
}Use logical properties for RTL compatibility:
/* โ Avoid */
.element {
margin-left: 10px;
text-align: left;
}
/* โ
Use logical properties */
.element {
margin-inline-start: 10px;
text-align: start;
}
/* โ
Or use Tailwind RTL utilities */
.element {
@apply ms-2.5; /* margin-start */
@apply text-start;
}import { formatDate, formatRelativeTime } from '@/lib/i18n/date-formats'
// Format date
const formatted = formatDate(new Date(), 'long', 'en')
// Output: "January 31, 2026"
// Relative time
const relative = formatRelativeTime(new Date(), 'en')
// Output: "just now"-
short: "1/31/26" -
medium: "Jan 31, 2026" -
long: "January 31, 2026" -
full: "Friday, January 31, 2026" -
time: "3:45 PM" -
datetime: "Jan 31, 2026, 3:45 PM"
import { formatNumber, formatCurrency } from '@/lib/i18n/number-formats'
// Format number
const num = formatNumber(1234.56, 'en')
// Output: "1,234.56"
// Format currency
const price = formatCurrency(99.99, 'USD', 'en')
// Output: "$99.99"
// Format percentage
const percent = formatNumber(0.85, 'en', { style: 'percent' })
// Output: "85%"We welcome translation contributions from the community! Here's how to contribute:
-
Fork the repository
git clone https://github.com/yourusername/nself-chat.git cd nself-chat -
Choose your language
- Check
/src/locales/for existing languages - Copy
en/folder as template if starting new language
- Check
-
Translate the files
- Edit JSON files in
/src/locales/[your-lang]/ - Keep the keys the same, translate only the values
- Maintain interpolation variables like
{{name}}
- Edit JSON files in
-
Update locale configuration
Edit
/src/lib/i18n/locales.ts:export const SUPPORTED_LOCALES = { // ... existing locales it: { code: 'it', name: 'Italiano', englishName: 'Italian', script: 'Latn', direction: 'ltr', bcp47: 'it-IT', flag: '๐ฎ๐น', dateFnsLocale: 'it', numberLocale: 'it-IT', pluralRule: 'other', isComplete: false, completionPercent: 0, }, }
-
Test your translations
pnpm dev # Navigate to settings and change language -
Submit a pull request
git checkout -b add-italian-translation git add src/locales/it/ git commit -m "Add Italian translation" git push origin add-italian-translation
- Understand the UI context where text appears
- Ask for screenshots if unclear
- Check similar apps in your language for terminology
- Keep interpolation variables:
{{name}},{{count}} - Maintain HTML entities:
,— - Don't translate technical terms like "URL", "API", "OAuth"
English uses _one and _other:
{
"messages_one": "{{count}} message",
"messages_other": "{{count}} messages"
}Some languages need more forms:
-
Arabic:
_zero,_one,_two,_few,_many,_other -
Polish:
_one,_few,_many,_other -
Russian:
_one,_few,_many,_other
- Match the app's tone (professional but friendly)
- Use informal "you" where appropriate (tu vs. vous)
- Be consistent throughout
- Long words (German compounds)
- Short words (Chinese characters)
- RTL text (Arabic/Hebrew)
- Special characters
Before submitting:
- All JSON files are valid (no syntax errors)
- All keys from English version are present
- Interpolation variables are unchanged
- Plural forms follow locale's plural rules
- No machine translation without review
- Tested in UI (if possible)
- Proper character encoding (UTF-8)
- Consistent terminology across files
-
Automated checks:
- JSON syntax validation
- Key completeness check
- Interpolation variable check
-
Community review:
- Native speakers review translations
- Maintainers check for consistency
- UI testing with new translations
-
Approval and merge:
- At least one native speaker approval
- Maintainer approval
- Merge to main branch
- Deploy in next release
-
Change language in UI:
- Go to Settings โ Language
- Select your language
- Navigate through all pages
-
Check for issues:
- Text overflow/truncation
- Misaligned elements
- Missing translations (showing keys)
- Broken layouts (especially RTL)
# Run translation tests
pnpm test src/lib/i18n/__tests__/
# Specific locale test
pnpm test src/lib/i18n/__tests__/locales.test.ts
# Test translator
pnpm test src/lib/i18n/__tests__/translator.test.ts# Validate all translation files
node scripts/validate-translations.js
# Check specific language
node scripts/validate-translations.js --locale=es
# Find missing keys
node scripts/validate-translations.js --find-missing-
Always use translation keys
// โ Don't hardcode strings <button>Save</button> // โ Use translation keys <button>{t('app.save')}</button>
-
Provide context in keys
// โ Vague t('submit') // โ Descriptive t('settings.account.submit')
-
Use interpolation for dynamic content
// โ Don't concatenate const text = userName + ' sent a message' // โ Use interpolation const text = t('messages.userSent', { values: { user: userName } })
-
Handle plurals properly
// โ Manual plural logic const text = count === 1 ? '1 message' : `${count} messages` // โ Use plural keys const text = t('messages.count', { count })
-
Translate meaning, not words
- Adapt idioms to your language
- Use natural phrasing
- Consider cultural context
-
Maintain consistency
- Use same terms for same concepts
- Keep tone consistent
- Follow your language's style guide
-
Test in context
- See how translations look in UI
- Check text length and wrapping
- Verify with native speakers
-
Document ambiguities
- Add comments for unclear terms
- Provide alternatives for review
- Ask questions in pull requests
Problem: Changing language doesn't update UI
Solutions:
// Check if namespace is loaded
const { loadNamespace } = useLocaleStore()
await loadNamespace('chat')
// Force reload
window.location.reload()Problem: Seeing keys like common:app.name instead of translated text
Solutions:
- Check if key exists in translation file
- Verify namespace is loaded
- Check for typos in key name
- Ensure fallback locale (en) has the key
Problem: RTL languages display incorrectly
Solutions:
- Use logical CSS properties (
margin-inline-startvsmargin-left) - Wrap components in
<RTLWrapper> - Check
dirattribute on<html> - Use Tailwind RTL utilities
Problem: Always showing same plural form
Solutions:
// Ensure count is passed
t('messages.count', { count: 5 }); // โ
t('messages.count', { values: { count: 5 } }); // โ
// Check plural keys exist
{
"messages.count_one": "{{count}} message",
"messages.count_other": "{{count}} messages"
}Problem: Dates or numbers not formatted for locale
Solutions:
// Use locale-aware formatting
import { formatDate } from '@/lib/i18n/date-formats';
const formatted = formatDate(date, 'long', locale);
// Or use components
<FormattedDate date={date} format="long" />
<FormattedNumber value={1234.56} />Enable i18n debug mode:
// In i18n-config.ts
export const i18nConfig = {
debug: true, // Enable logging
// ...
}This will log:
- Missing translations
- Fallback usage
- Namespace loading
- Translation lookups
- Translation Discussions: GitHub Discussions
- Report Issues: GitHub Issues
- Request Language: Request Form
All translations are part of the nself-chat project and follow the same license (MIT).
Contributors retain copyright of their translations but grant nself-chat the right to use, modify, and distribute them under the project license.
Need Help?
- ๐ง Email: [email protected]
- ๐ฌ Discord: nself-chat Discord
- ๐ Docs: docs.nself.org/i18n
Want to contribute? See Contributing.md for more information!