Appearance
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 translationsTranslation 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:
- localStorage: Previously saved language preference
- navigator: Browser language settings
- 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:
- Architecture Overview - Overall system structure
- Database Schema - Multilingual table structure
- User Interface - UI component localization