link,[object Object]
Skip to content

Internationalization (i18n) ​

Overview ​

AcqMarketplace supports multilingual content with Romanian (RO) as the primary language and English (EN) for international users. The implementation uses react-i18next for static content and database-level localization for dynamic content.

Technology Stack ​

  • react-i18next: Frontend translation management
  • i18next: Core internationalization framework
  • i18next-browser-languagedetector: Automatic language detection
  • Database Localization: Multilingual columns in PostgreSQL

Setup and Configuration ​

i18next Configuration ​

File: src/lib/i18n.ts

typescript
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';

i18n
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    fallbackLng: 'ro',
    debug: process.env.NODE_ENV === 'development',
    
    detection: {
      order: ['localStorage', 'navigator', 'htmlTag'],
      caches: ['localStorage'],
    },
    
    interpolation: {
      escapeValue: false,
    },
    
    resources: {
      ro: {
        translation: require('../locales/ro.json'),
      },
      en: {
        translation: require('../locales/en.json'),
      },
    },
  });

export default i18n;

Translation Provider ​

File: src/contexts/translation/TranslationProvider.tsx

typescript
interface TranslationContextType {
  language: 'ro' | 'en';
  setLanguage: (lang: 'ro' | 'en') => void;
  t: (key: string, options?: any) => string;
}

export function TranslationProvider({ children }: { children: ReactNode }) {
  const { i18n } = useTranslation();
  const [language, setLanguageState] = useState<'ro' | 'en'>(
    (i18n.language as 'ro' | 'en') || 'ro'
  );
  
  const setLanguage = useCallback((lang: 'ro' | 'en') => {
    setLanguageState(lang);
    i18n.changeLanguage(lang);
    localStorage.setItem('language', lang);
  }, [i18n]);
  
  const t = useCallback((key: string, options?: any) => {
    return i18n.t(key, options);
  }, [i18n]);
  
  return (
    <TranslationContext.Provider value={{ language, setLanguage, t }}>
      {children}
    </TranslationContext.Provider>
  );
}

Static Content Translation ​

Translation Files Structure ​

src/locales/
├── ro.json          # Romanian translations
└── en.json          # English translations

Translation File Format ​

File: src/locales/ro.json

json
{
  "common": {
    "save": "Salvează",
    "cancel": "Anulează",
    "delete": "Șterge",
    "edit": "Editează",
    "loading": "Se încarcă...",
    "error": "A apărut o eroare"
  },
  "navigation": {
    "home": "Acasă",
    "explore": "Explorează",
    "dashboard": "Dashboard",
    "messages": "Mesaje",
    "profile": "Profil"
  },
  "listings": {
    "title": "Listări",
    "createNew": "Creează o listare nouă",
    "filters": {
      "category": "Categorie",
      "priceRange": "Interval de preÈ›",
      "sortBy": "Sortează după"
    }
  },
  "auth": {
    "login": "Autentifică-te",
    "register": "Înregistrează-te",
    "forgotPassword": "Ai uitat parola?",
    "emailPlaceholder": "Adresa ta de email",
    "passwordPlaceholder": "Parola ta"
  }
}

Using Translations in Components ​

Basic Usage ​

typescript
import { useTranslation } from 'react-i18next';

function LoginForm() {
  const { t } = useTranslation();
  
  return (
    <form>
      <h1>{t('auth.login')}</h1>
      <input placeholder={t('auth.emailPlaceholder')} />
      <input placeholder={t('auth.passwordPlaceholder')} />
      <button>{t('auth.login')}</button>
    </form>
  );
}

With Interpolation ​

typescript
function WelcomeMessage({ userName }: { userName: string }) {
  const { t } = useTranslation();
  
  return (
    <h1>{t('common.welcome', { name: userName })}</h1>
  );
}

// Translation file
{
  "common": {
    "welcome": "Bun venit, {{name}}!"
  }
}

Custom Hook Usage ​

typescript
import { useTranslation } from '@/contexts/translation/TranslationProvider';

function CustomComponent() {
  const { t, language, setLanguage } = useTranslation();
  
  return (
    <div>
      <p>{t('common.currentLanguage')}: {language}</p>
      <button onClick={() => setLanguage(language === 'ro' ? 'en' : 'ro')}>
        {t('common.switchLanguage')}
      </button>
    </div>
  );
}

Dynamic Content Localization ​

Database Schema Pattern ​

Most content tables include language-specific columns:

sql
-- Example: listings table
CREATE TABLE listings (
  id UUID PRIMARY KEY,
  title TEXT,              -- Default/Romanian
  title_ro TEXT,           -- Romanian explicit
  title_en TEXT,           -- English
  description TEXT,        -- Default/Romanian
  description_ro TEXT,     -- Romanian explicit
  description_en TEXT,     -- English
  -- ... other columns
);

Multilingual Content Retrieval ​

Helper Function for Content Selection ​

typescript
function getLocalizedContent(
  content: { 
    default?: string; 
    ro?: string; 
    en?: string; 
  }, 
  language: 'ro' | 'en'
): string {
  if (language === 'en' && content.en) return content.en;
  if (language === 'ro' && content.ro) return content.ro;
  return content.default || content.ro || content.en || '';
}

Usage in Components ​

typescript
function ListingCard({ listing }: { listing: Listing }) {
  const { language } = useTranslation();
  
  const title = getLocalizedContent({
    default: listing.title,
    ro: listing.title_ro,
    en: listing.title_en,
  }, language);
  
  const description = getLocalizedContent({
    default: listing.description,
    ro: listing.description_ro,
    en: listing.description_en,
  }, language);
  
  return (
    <div>
      <h3>{title}</h3>
      <p>{description}</p>
    </div>
  );
}

Custom Hook for Localized Content ​

typescript
function useLocalizedContent<T extends Record<string, any>>(
  content: T,
  field: keyof T
) {
  const { language } = useTranslation();
  
  return useMemo(() => {
    const base = content[field];
    const localized = content[`${String(field)}_${language}`];
    const fallback = content[`${String(field)}_ro`] || content[`${String(field)}_en`];
    
    return localized || base || fallback || '';
  }, [content, field, language]);
}

// Usage
function ListingDetail({ listing }: { listing: Listing }) {
  const title = useLocalizedContent(listing, 'title');
  const description = useLocalizedContent(listing, 'description');
  
  return (
    <div>
      <h1>{title}</h1>
      <p>{description}</p>
    </div>
  );
}

Language Detection and Switching ​

Automatic Language Detection ​

Based on i18next-browser-languagedetector configuration:

  1. localStorage: Previously saved language preference
  2. navigator: Browser language settings
  3. htmlTag: HTML lang attribute

Language Switcher Component ​

typescript
function LanguageSwitcher() {
  const { language, setLanguage } = useTranslation();
  
  return (
    <Select value={language} onValueChange={setLanguage}>
      <SelectTrigger className="w-20">
        <SelectValue />
      </SelectTrigger>
      <SelectContent>
        <SelectItem value="ro">
          <div className="flex items-center gap-2">
            🇷🇴 <span>RO</span>
          </div>
        </SelectItem>
        <SelectItem value="en">
          <div className="flex items-center gap-2">
            🇺🇸 <span>EN</span>
          </div>
        </SelectItem>
      </SelectContent>
    </Select>
  );
}

Persisting Language Preference ​

typescript
// In TranslationProvider
useEffect(() => {
  // Update user profile with language preference
  if (user && language !== user.language_preference) {
    updateUserProfile({ language_preference: language });
  }
}, [language, user]);

// On user login, set saved language
useEffect(() => {
  if (user?.language_preference) {
    setLanguage(user.language_preference);
  }
}, [user]);

SEO and URL Localization ​

Language-specific Meta Tags ​

typescript
import { Helmet } from 'react-helmet-async';

function LocalizedSEO({ 
  title, 
  description,
  language 
}: {
  title: { ro: string; en: string };
  description: { ro: string; en: string };
  language: 'ro' | 'en';
}) {
  return (
    <Helmet>
      <html lang={language} />
      <title>{title[language]}</title>
      <meta name="description" content={description[language]} />
      <meta property="og:title" content={title[language]} />
      <meta property="og:description" content={description[language]} />
      <link 
        rel="alternate" 
        hrefLang="ro" 
        href={`${window.location.origin}/ro${window.location.pathname}`} 
      />
      <link 
        rel="alternate" 
        hrefLang="en" 
        href={`${window.location.origin}/en${window.location.pathname}`} 
      />
    </Helmet>
  );
}

Email Localization ​

Multilingual Email Templates ​

typescript
// Email service with language support
export const emailService = {
  async sendWelcomeEmail(
    email: string, 
    userName: string, 
    language: 'ro' | 'en'
  ) {
    const templates = {
      ro: {
        subject: 'Bun venit la AcqMarketplace!',
        body: `Bună ${userName}, bun venit pe platforma noastră!`,
      },
      en: {
        subject: 'Welcome to AcqMarketplace!',
        body: `Hello ${userName}, welcome to our platform!`,
      },
    };
    
    const template = templates[language];
    
    return await supabase.functions.invoke('send-email', {
      body: {
        to: email,
        subject: template.subject,
        html: template.body,
        language,
      },
    });
  },
};

Content Management ​

Admin Interface for Translations ​

typescript
function TranslationManager() {
  const [translations, setTranslations] = useState<Translation[]>([]);
  
  const { data: dbTranslations } = useQuery({
    queryKey: ['translations'],
    queryFn: async () => {
      const { data } = await supabase
        .from('translations')
        .select('*')
        .order('key');
      return data;
    },
  });
  
  return (
    <div>
      <h2>Manage Translations</h2>
      {dbTranslations?.map((translation) => (
        <div key={translation.id} className="grid grid-cols-3 gap-4">
          <div>{translation.key}</div>
          <input 
            value={translation.value_ro}
            onChange={(e) => updateTranslation(translation.id, 'ro', e.target.value)}
          />
          <input 
            value={translation.value_en}
            onChange={(e) => updateTranslation(translation.id, 'en', e.target.value)}
          />
        </div>
      ))}
    </div>
  );
}

Dynamic Translation Loading ​

typescript
function useDynamicTranslations() {
  const { data: dynamicTranslations } = useQuery({
    queryKey: ['dynamic-translations'],
    queryFn: async () => {
      const { data } = await supabase
        .from('translations')
        .select('key, value_ro, value_en, category');
      return data;
    },
  });
  
  useEffect(() => {
    if (dynamicTranslations) {
      // Add dynamic translations to i18next
      dynamicTranslations.forEach((t) => {
        i18n.addResource('ro', 'dynamic', t.key, t.value_ro);
        i18n.addResource('en', 'dynamic', t.key, t.value_en);
      });
    }
  }, [dynamicTranslations]);
}

Best Practices ​

1. Translation Key Organization ​

typescript
// Use hierarchical keys
t('features.listings.create.title')
t('forms.validation.required')
t('notifications.success.saved')

// Group related translations
t('buttons.save')
t('buttons.cancel')
t('buttons.delete')

2. Fallback Strategies ​

typescript
// Always provide fallbacks
const title = listing.title_en || listing.title_ro || listing.title || 'Untitled';

// Use default language as fallback
function getLocalizedField(object: any, field: string, language: string) {
  return object[`${field}_${language}`] || 
         object[`${field}_ro`] || 
         object[field] || 
         '';
}

3. Content Validation ​

typescript
// Ensure required translations exist
const requiredTranslations = ['title', 'description'];

function validateListingTranslations(listing: Listing, language: 'ro' | 'en') {
  return requiredTranslations.every(field => {
    const value = listing[`${field}_${language}` as keyof Listing];
    return value && value.length > 0;
  });
}

4. Performance Optimization ​

typescript
// Lazy load translations
const LazyTranslatedComponent = lazy(() => 
  import('./TranslatedComponent').then(module => ({
    default: module.TranslatedComponent
  }))
);

// Memoize localized content
const LocalizedContent = memo(({ content, language }: {
  content: LocalizedContent;
  language: string;
}) => {
  const localizedText = useMemo(() => 
    getLocalizedContent(content, language),
    [content, language]
  );
  
  return <p>{localizedText}</p>;
});

Related Documentation: