Home Under the hood Internationalization

Internationalization

Last updated on Jun 12, 2026

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.