Internationalization
Understanding the multi-language system in CHRIS.
Overview
CHRIS supports four languages:
| Language | Code | Flag | Direction | |----------|------|------|-----------| | Croatian | hr | 🇭🇷 | LTR | | English |
en | 🇬🇧 | LTR | | Russian | ru | 🇷🇺 | LTR | | Hindi | hi | 🇮🇳 | LTR |
Architecture
Database-Driven Translations
Unlike file-based i18n, CHRIS stores translations in the database:
CREATE TABLE translations (
id UUID PRIMARY KEY,
key TEXT UNIQUE NOT NULL,
croatian TEXT NOT NULL,
english TEXT NOT NULL,
russian TEXT,
hindi TEXT,
category TEXT,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
Advantages:
- Edit translations without code changes
- Non-developers can manage translations
- Runtime updates without deployment
LanguageContext
Context Provider
The LanguageContext provides translation functions:
// src/contexts/LanguageContext.tsx
const LanguageContext = createContext<{
language: string;
setLanguage: (lang: string) => void;
t: (key: string) => string;
formatDate: (date: Date) => string;
}>({...});
Using the Context
import { useLanguage } from '@/contexts/LanguageContext';
function MyComponent() {
const { t, language, setLanguage } = useLanguage();
return (
<div>
<h1>{t('common.welcome')}</h1>
<button onClick={() => setLanguage('hr')}>
🇭🇷 Hrvatski
</button>
</div>
);
}
Translation Function
Basic Usage
// Returns translated string
t('common.save') // "Save" (en) or "Spremi" (hr)
Key Structure
Keys follow the pattern: category.descriptiveName
common.save
leaveRequests.submitRequest
employees.addEmployee
settings.smtpHost
Fallback Chain
If a translation is missing:
1. Try requested language (e.g., Hindi)
↓ (if null)
2. Try English
↓ (if null)
3. Try Croatian
↓ (if null)
4. Return the key itself
Implementation:
function t(key: string): string {
const translation = translations.find(t => t.key === key);
if (!translation) return key;
const langMap = {
hr: 'croatian',
en: 'english',
ru: 'russian',
hi: 'hindi'
};
return (
translation[langMap[language]] ||
translation.english ||
translation.croatian ||
key
);
}
Translation Categories
Translations are organized by category:
| Category | Description | |----------|-------------| | common | Shared UI elements | | auth | Authentication | |
employees | Employee management | | leaveRequests | Leave workflow | | teams | Team management | | settings | Settings
pages | | holidays | Holiday management | | contracts | Contract management | | audit | Audit logging | | profile | User
profile |
Querying by Category
SELECT key, english, croatian
FROM translations
WHERE category = 'common'
ORDER BY key;
Date Formatting
Locale-Aware Dates
const { formatDate } = useLanguage();
// Returns date in current locale format
formatDate(new Date('2025-01-15'))
// "15.01.2025" (hr) or "01/15/2025" (en)
Implementation
function formatDate(date: Date): string {
const locales = {
hr: 'hr-HR',
en: 'en-US',
ru: 'ru-RU',
hi: 'hi-IN'
};
return date.toLocaleDateString(locales[language], {
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
}
Language Picker
UI Component
function LanguagePicker() {
const { language, setLanguage } = useLanguage();
const languages = [
{ code: 'hr', flag: '🇭🇷', name: 'Hrvatski' },
{ code: 'en', flag: '🇬🇧', name: 'English' },
{ code: 'ru', flag: '🇷🇺', name: 'Русский' },
{ code: 'hi', flag: '🇮🇳', name: 'हिन्दी' },
];
return (
<Select value={language} onValueChange={setLanguage}>
{languages.map(lang => (
<SelectItem key={lang.code} value={lang.code}>
{lang.flag} {lang.name}
</SelectItem>
))}
</Select>
);
}
Language Persistence
Selected language is stored in localStorage:
// On change
localStorage.setItem('preferred-language', newLanguage);
// On load
const saved = localStorage.getItem('preferred-language');
setLanguage(saved || 'hr'); // Default to Croatian
Adding Translations
Via Database Migration
INSERT INTO translations (key, croatian, english, russian, hindi, category)
VALUES
('common.newFeature', 'Nova značajka', 'New Feature', 'Новая функция', 'नई सुविधा', 'common')
ON CONFLICT (key) DO UPDATE SET
croatian = EXCLUDED.croatian,
english = EXCLUDED.english,
russian = EXCLUDED.russian,
hindi = EXCLUDED.hindi;
Via Admin UI
1. Navigate to Settings > Translations
2. Click Add Translation
3. Enter key and translations for each language
4. Save
Best Practices
Key Naming
✓ common.save (good)
✓ leaveRequests.submit (good)
✗ save (too generic)
✗ saveButton (avoid 'Button')
✗ COMMON_SAVE (wrong format)
Translation Quality
1. Be consistent: Use same term for same concept
2. Consider context: "Save" vs "Save Changes"
3. Watch text length: Some languages are longer
4. Use native speakers: For accuracy
Missing Translations
Check for missing translations:
-- Find keys missing Russian
SELECT key
FROM translations
WHERE russian IS NULL OR russian = '';
-- Find keys missing Hindi
SELECT key
FROM translations
WHERE hindi IS NULL OR hindi = '';
Adding a New Language
Step 1: Database Schema
ALTER TABLE translations
ADD COLUMN german TEXT;
Step 2: Update Types
// src/integrations/supabase/types.ts
translations: {
Row: {
// ...existing columns
german: string | null;
}
}
Step 3: Update LanguageContext
const langMap = {
hr: 'croatian',
en: 'english',
ru: 'russian',
hi: 'hindi',
de: 'german', // Add new language
};
const languages = [
// ...existing
{ code: 'de', flag: '🇩🇪', name: 'Deutsch' },
];
Step 4: Add Translations
Bulk update all translation keys with German text.
Performance
Caching Strategy
Translations are fetched once and cached:
const { data: translations } = useQuery({
queryKey: ['translations'],
queryFn: fetchTranslations,
staleTime: Infinity, // Never refetch automatically
});
Cache Invalidation
Force refresh when translations change:
queryClient.invalidateQueries(['translations']);
Bundle Size
Since translations are database-driven, they don't increase JavaScript bundle size.