i18n implementation complete - nself-org/nchat GitHub Wiki
Date: February 1, 2026 Version: 0.9.0 Status: Production-Ready โ
This document details the complete, production-ready internationalization (i18n) system implemented for nself-chat. The system provides comprehensive multi-language support with advanced features including RTL support, pluralization, date/time localization, number formatting, lazy loading, and browser language detection.
- translator.ts - Core translation engine with interpolation, pluralization, and fallbacks
- i18n-config.ts - Central configuration with environment-aware settings
- locales.ts - Locale registry with metadata for 9 supported languages
- language-detector.ts - Auto-detection from browser, cookies, localStorage, URL, and headers
- plurals.ts - CLDR-compliant pluralization rules for all supported languages
- date-formats.ts - Date/time formatting with relative times and locale-aware patterns
- number-formats.ts - Number, currency, percentage, bytes, and compact formatting
- rtl.ts - Right-to-left (RTL) support with automatic layout mirroring
- index.ts - Unified export module
- use-translation.ts - React hook for translations with namespaces and interpolation
- use-locale.ts - React hook for locale management and formatting utilities
- locale-store.ts - Zustand store with persistence and lazy loading
Complete Languages (100% translated, all namespaces):
| Language | Code | Namespaces | RTL | Status |
|---|---|---|---|---|
| English | en |
common, chat, settings, admin, auth, errors | No | โ Complete |
| German | de |
common, chat, settings, admin, auth, errors | No | โ Complete |
| Japanese | ja |
common, chat, settings, admin, auth, errors | No | โ Complete |
| Spanish | es |
common, chat, settings | No | |
| French | fr |
common, chat, settings | No |
Partially Complete Languages (need auth.json, errors.json, admin.json):
| Language | Code | Status | Notes |
|---|---|---|---|
| Arabic | ar |
40% | RTL language, needs auth/errors/admin/settings |
| Chinese (Simplified) | zh |
40% | Needs auth/errors/admin/settings |
| Portuguese | pt |
40% | Needs auth/errors/admin/settings |
| Russian | ru |
40% | Needs auth/errors/admin/settings |
Namespace System:
// Supported namespaces
const namespaces = ['common', 'chat', 'settings', 'admin', 'auth', 'errors']
// Usage examples
t('common:app.name') // "nChat"
t('chat:messages.send') // "Send message"
t('auth:signIn.title') // "Welcome back"
t('errors:network.offline') // "You are offline"Key Categories:
- common.json - General UI, buttons, navigation, validation, status
- chat.json - Messages, channels, threads, reactions, presence
- settings.json - User preferences, profile, account, appearance
- admin.json - Dashboard, users, roles, analytics, moderation
- auth.json - Sign in/up, passwords, 2FA, SSO, sessions
- errors.json - Network, HTTP, validation, permissions, files
// Simple interpolation
t('common:time.ago', { time: '5 minutes' })
// Output: "5 minutes ago"
// Count interpolation
t('messages.count', { count: 42 })
// Output: "42 messages"
// Multiple values
t('validation.minLength', { min: 8 })
// Output: "Must be at least 8 characters"English (2 forms):
{
"messages_one": "{{count}} message",
"messages_other": "{{count}} messages"
}Arabic (6 forms):
{
"messages_zero": "ูุง ุชูุฌุฏ ุฑุณุงุฆู",
"messages_one": "ุฑุณุงูุฉ ูุงุญุฏุฉ",
"messages_two": "ุฑุณุงูุชุงู",
"messages_few": "{{count}} ุฑุณุงุฆู",
"messages_many": "{{count}} ุฑุณุงูุฉ",
"messages_other": "{{count}} ุฑุณุงูุฉ"
}import { useLocale } from '@/hooks/use-locale'
const { formatDate, formatTime, formatRelativeTime } = useLocale()
// Localized date formats
formatDate(new Date(), { format: 'long' })
// en: "January 1, 2026"
// de: "1. Januar 2026"
// ja: "2026ๅนด1ๆ1ๆฅ"
// Relative time
formatRelativeTime(date)
// en: "2 hours ago"
// es: "hace 2 horas"
// fr: "il y a 2 heures"
// Message timestamps
formatMessageTime(date)
// Shows time for today, date for this week, full date for olderconst { formatNumber, formatCurrency, formatBytes } = useLocale()
// Numbers with locale separators
formatNumber(1234567.89)
// en: "1,234,567.89"
// de: "1.234.567,89"
// fr: "1 234 567,89"
// Currency
formatCurrency(99.99, { currency: 'USD' })
// en: "$99.99"
// de: "99,99 $"
// ja: "$99.99"
// File sizes
formatBytes(1024 * 1024 * 5.5)
// en: "5.5 MB"
// de: "5,5 MB"
// ja: "5.5 MB"Auto-detection and application:
import { isRTL, applyDocumentDirection } from '@/lib/i18n/rtl'
// Check if locale is RTL
isRTL('ar') // true
isRTL('en') // false
// Auto-apply direction to document
applyDocumentDirection('ar')
// Sets: <html dir="rtl" lang="ar">
// Applies RTL-specific CSS classesRTL-aware styling:
import { rtlClass, rtlStyles } from '@/lib/i18n/rtl';
// Conditional classes
<div className={rtlClass('text-left', 'text-right')}>
// CSS logical properties
const styles = rtlStyles({
marginLeft: '1rem', // Becomes marginInlineStart
paddingRight: '2rem' // Becomes paddingInlineEnd
})Detection sources (in priority order):
- Cookie (
NCHAT_LOCALE) - LocalStorage (
nchat-locale) - URL query parameter (
?lang=es) - Browser navigator languages
- HTML
langattribute - Default locale (
en)
Server-side detection:
import { detectFromHeaders } from '@/lib/i18n/language-detector'
// In API route or middleware
const result = detectFromHeaders({
'accept-language': 'es-ES,es;q=0.9,en;q=0.8',
cookie: 'NCHAT_LOCALE=es',
})
// Returns: { locale: 'es', source: 'cookie', confidence: 1.0 }Translations are loaded on-demand to reduce initial bundle size:
// Auto-loads on locale change
await setLocale('ja')
// Loads: ja/common.json, ja/chat.json
// Load specific namespace
await loadNamespace('admin')
// Loads: ja/admin.json (if not already loaded)
// Preload all namespaces
await loadAllNamespaces()Translation Hook:
import { useTranslation } from '@/hooks/use-translation';
function MyComponent() {
const { t, locale, exists } = useTranslation('chat');
return (
<div>
<h1>{t('title')}</h1>
<p>{t('messages.count', { count: 5 })}</p>
{exists('newFeature') && <NewFeature />}
</div>
);
}Locale Hook:
import { useLocale } from '@/hooks/use-locale';
function DateDisplay() {
const { locale, formatDate, setLocale, isRTL } = useLocale();
return (
<div dir={isRTL ? 'rtl' : 'ltr'}>
<p>Current: {locale}</p>
<p>{formatDate(new Date())}</p>
<button onClick={() => setLocale('ja')}>
ๆฅๆฌ่ช
</button>
</div>
);
}Convenience hooks for specific namespaces:
import {
useCommonTranslation,
useChatTranslation,
useSettingsTranslation,
useAdminTranslation,
useAuthTranslation,
useErrorsTranslation
} from '@/hooks/use-translation';
function ChatPage() {
const { t } = useChatTranslation();
return <h1>{t('messages.title')}</h1>;
}src/
โโโ lib/i18n/ # Core i18n library
โ โโโ index.ts # Main exports
โ โโโ i18n-config.ts # Configuration
โ โโโ translator.ts # Translation engine
โ โโโ locales.ts # Locale registry
โ โโโ language-detector.ts # Auto-detection
โ โโโ plurals.ts # Pluralization rules
โ โโโ date-formats.ts # Date/time formatting
โ โโโ number-formats.ts # Number formatting
โ โโโ rtl.ts # RTL support
โ โโโ __tests__/ # Unit tests (8 files)
โโโ hooks/
โ โโโ use-translation.ts # Translation hook
โ โโโ use-locale.ts # Locale hook
โ โโโ __tests__/ # Hook tests
โโโ stores/
โ โโโ locale-store.ts # Zustand locale store
โ โโโ __tests__/ # Store tests
โโโ locales/ # Translation files
โโโ README.md # Translation guide
โโโ en/ # English (100%)
โ โโโ common.json # โ
Complete
โ โโโ chat.json # โ
Complete
โ โโโ settings.json # โ
Complete
โ โโโ admin.json # โ
Complete
โ โโโ auth.json # โ
Complete
โ โโโ errors.json # โ
Complete
โโโ de/ # German (100%)
โ โโโ common.json # โ
Complete
โ โโโ chat.json # โ
Complete
โ โโโ settings.json # โ
Complete
โ โโโ admin.json # โ
Complete
โ โโโ auth.json # โ
Complete
โ โโโ errors.json # โ
Complete
โโโ ja/ # Japanese (100%)
โ โโโ common.json # โ
Complete
โ โโโ chat.json # โ
Complete
โ โโโ settings.json # โ
Complete
โ โโโ admin.json # โ
Complete
โ โโโ auth.json # โ
Complete
โ โโโ errors.json # โ
Complete
โโโ es/ # Spanish (60%)
โ โโโ common.json # โ
Complete
โ โโโ chat.json # โ
Complete
โ โโโ settings.json # โ
Complete
โโโ fr/ # French (60%)
โ โโโ common.json # โ
Complete
โ โโโ chat.json # โ
Complete
โ โโโ settings.json # โ
Complete
โโโ ar/ # Arabic (40%, RTL)
โ โโโ common.json # โ
Complete
โ โโโ chat.json # โ
Complete
โโโ zh/ # Chinese (40%)
โ โโโ common.json # โ
Complete
โ โโโ chat.json # โ
Complete
โโโ pt/ # Portuguese (40%)
โ โโโ common.json # โ
Complete
โ โโโ chat.json # โ
Complete
โโโ ru/ # Russian (40%)
โโโ common.json # โ
Complete
โโโ chat.json # โ
Complete
# No special env vars needed - works out of the box!
# Optional: Override defaults in .env.local
NEXT_PUBLIC_DEFAULT_LOCALE=en
NEXT_PUBLIC_FALLBACK_LOCALE=enexport const i18nConfig = {
defaultLocale: 'en',
fallbackLocale: 'en',
supportedLocales: ['en', 'es', 'fr', 'de', 'ar', 'zh', 'ja', 'pt', 'ru'],
// Debugging
debug: process.env.NODE_ENV === 'development',
// Loading strategy
lazyLoad: true,
preloadNamespaces: false,
// Separators
keySeparator: '.', // common.app.name
namespaceSeparator: ':', // chat:messages.send
pluralSeparator: '_', // messages_one
contextSeparator: '_', // greeting_male
// Interpolation
interpolationStart: '{{',
interpolationEnd: '}}',
escapeValue: true,
// Namespaces
defaultNamespace: 'common',
namespaces: ['common', 'chat', 'settings', 'admin', 'auth', 'errors'],
// Persistence
storageKey: 'nchat-locale',
cookieName: 'NCHAT_LOCALE',
cookieMaxAge: 365 * 24 * 60 * 60, // 1 year
persistLocale: true,
// Detection
detectBrowserLocale: true,
detectUrlLocale: false,
urlParamName: 'lang',
}import { useTranslation } from '@/hooks/use-translation'
export function WelcomeMessage() {
const { t } = useTranslation('common')
return (
<div>
<h1>{t('app.name')}</h1>
<p>{t('app.tagline')}</p>
<button>{t('app.getStarted')}</button>
</div>
)
}export function UserGreeting({ name }: { name: string }) {
const { t } = useTranslation()
return (
<h1>{t('common:greeting', { name })}</h1>
// Output: "Hello, John!"
)
}export function MessageCount({ count }: { count: number }) {
const { t } = useChatTranslation()
return (
<span>{t('messages.count', { count })}</span>
// count = 1: "1 message"
// count = 5: "5 messages"
)
}export function MessageTimestamp({ date }: { date: Date }) {
const { formatRelativeTime, formatMessageTime } = useLocale()
return (
<div>
<time>{formatRelativeTime(date)}</time>
{/* "2 hours ago" */}
<span>{formatMessageTime(date)}</span>
{/* "3:45 PM" (today) or "Jan 15" (this year) */}
</div>
)
}import { useLocale } from '@/hooks/use-locale'
import { SUPPORTED_LOCALES } from '@/lib/i18n/locales'
export function LanguageSwitcher() {
const { locale, setLocale, isLoading } = useLocale()
return (
<select value={locale} onChange={(e) => setLocale(e.target.value)} disabled={isLoading}>
{Object.values(SUPPORTED_LOCALES).map(({ code, name, flag }) => (
<option key={code} value={code}>
{flag} {name}
</option>
))}
</select>
)
}import { useIsRTL } from '@/hooks/use-locale'
export function ChatLayout({ children }) {
const isRTL = useIsRTL()
return (
<div dir={isRTL ? 'rtl' : 'ltr'} className={isRTL ? 'rtl-layout' : 'ltr-layout'}>
{children}
</div>
)
}export function FeatureAnnouncement() {
const { t, exists } = useTranslation('common')
return (
<div>
{exists('features.newFeature') ? (
<p>{t('features.newFeature')}</p>
) : (
<p>New feature coming soon!</p>
)}
</div>
)
}import { useErrorsTranslation } from '@/hooks/use-translation'
export function ErrorBoundary({ error }) {
const { t } = useErrorsTranslation()
const errorKey = `errors.${error.code}` || 'errors.unknown'
return (
<div className="error">
<h2>{t('errors.general')}</h2>
<p>{t(errorKey)}</p>
<button>{t('errors.tryAgain')}</button>
</div>
)
}All core i18n modules have comprehensive test coverage:
# Run all i18n tests
pnpm test src/lib/i18n
pnpm test src/hooks/use-translation
pnpm test src/stores/locale-store
# Watch mode
pnpm test:watch src/lib/i18nTest files:
i18n-config.test.tstranslator.test.tslocales.test.tslanguage-detector.test.tsplurals.test.tsdate-formats.test.tsnumber-formats.test.tsrtl.test.tsuse-translation.test.tsuse-locale.test.tslocale-store.test.ts
- Switch between all supported languages
- Test RTL layout with Arabic
- Verify date/time formatting in each locale
- Test pluralization with different counts
- Verify number and currency formatting
- Test lazy loading of namespaces
- Verify browser language detection
- Test persistence across page reloads
- Verify missing translation fallbacks
- Test interpolation with special characters
- Core library: ~15 KB (minified + gzipped)
- Per locale file: ~5-10 KB (lazy loaded)
- Initial bundle: Only default locale (en) loaded
- On-demand: Additional locales loaded when switched
- Lazy Loading - Namespaces loaded on-demand
- Code Splitting - Translation files split by locale
- Caching - Translations cached in memory and localStorage
- Tree Shaking - Unused locales excluded from bundle
- Compression - JSON files compressed by Next.js
-
Screen Readers - Proper
langattribute on<html> - ARIA Labels - Translated aria-label attributes
- RTL Support - Proper text direction for screen readers
- Focus Management - RTL-aware focus order
- Keyboard Navigation - Works with all locales
- โ Chrome 90+
- โ Firefox 88+
- โ Safari 14+
- โ Edge 90+
- โ Mobile browsers (iOS Safari, Chrome Android)
// Old: react-i18next
import { useTranslation } from 'react-i18next'
const { t, i18n } = useTranslation()
t('key', { value: 'x' })
i18n.changeLanguage('es')
// New: nself-chat i18n
import { useTranslation, useLocale } from '@/hooks/use-translation'
const { t } = useTranslation()
const { setLocale } = useLocale()
t('key', { value: 'x' })
setLocale('es')-
Translation Management
- Admin UI for editing translations
- Export/import translation files
- Translation completion tracking
- Crowdsourced translation platform
-
Additional Languages
- Hebrew (RTL)
- Hindi
- Korean
- Italian
- Dutch
-
Advanced Features
- Gender-specific translations
- Regional variants (en-US, en-GB, pt-BR, pt-PT)
- Translation memory
- Machine translation integration
- A/B testing for translations
-
Developer Tools
- VS Code extension for translation keys
- CLI tool for managing translations
- Translation key linting
- Unused key detection
1. Translations not showing
// Check if namespace is loaded
const { isLoading } = useLocale();
if (isLoading) return <Spinner />;
// Manually load namespace
await loadNamespace('admin');2. Locale not persisting
// Check localStorage
console.log(localStorage.getItem('nchat-locale'))
// Check cookie
console.log(document.cookie)
// Clear and reset
clearPersistedLocale()
setLocale('en')3. RTL layout issues
// Ensure direction is applied
import { applyDocumentDirection } from '@/lib/i18n/rtl'
useEffect(() => {
applyDocumentDirection(locale)
}, [locale])4. Missing translations
// Enable debug mode
// In i18n-config.ts
debug: true // Shows warnings for missing keys
// Check fallback chain
// 1. Current locale
// 2. Fallback locale (en)
// 3. Key itself- Full i18n Guide:
/docs/guides/internationalization.md - Translation Guide:
/src/locales/README.md - API Reference:
/docs/api/i18n.md
- date-fns - Date formatting
- Intl.NumberFormat - Number formatting
The nself-chat i18n system is a complete, production-ready internationalization solution that provides:
โ 9 Supported Languages (3 complete, 6 partial) โ RTL Support for Arabic and future Hebrew โ Lazy Loading for optimal performance โ Type-Safe translations with TypeScript โ Comprehensive date, time, and number formatting โ Pluralization following CLDR standards โ Auto-Detection of user language preferences โ Persistent locale across sessions โ Fallback to English for missing translations โ Namespaced for better organization โ Tested with comprehensive unit tests
The system is fully integrated into the application and ready for use. Additional languages can be added by following the guide in /src/locales/README.md.
Maintained by: nself-chat i18n Team Last Updated: February 1, 2026 Version: 0.9.0