/** * i18n utility functions */ import { getLanguageInfo } from './languages'; /** * Detect user's preferred locale from browser * Works in browser environment only */ export function detectBrowserLocale( supportedLocales: readonly string[], defaultLocale = 'en' ): string { if (typeof navigator === 'undefined') { return defaultLocale; } // Try navigator.language first const browserLang = navigator.language; // Check exact match (e.g., 'pt-BR') if (supportedLocales.includes(browserLang)) { return browserLang; } // Check base language (e.g., 'pt' from 'pt-BR') const baseLang = browserLang.split('-')[0]; if (supportedLocales.includes(baseLang)) { return baseLang; } // Try navigator.languages array if (navigator.languages) { for (const lang of navigator.languages) { if (supportedLocales.includes(lang)) { return lang; } const base = lang.split('-')[0]; if (supportedLocales.includes(base)) { return base; } } } return defaultLocale; } /** * Get locale from localStorage with validation */ export function getStoredLocale( storageKey: string, supportedLocales: readonly string[] ): string | null { if (typeof localStorage === 'undefined') { return null; } const stored = localStorage.getItem(storageKey); if (stored && supportedLocales.includes(stored)) { return stored; } return null; } /** * Store locale in localStorage */ export function storeLocale(storageKey: string, locale: string): void { if (typeof localStorage === 'undefined') { return; } localStorage.setItem(storageKey, locale); } /** * Get initial locale with priority: * 1. localStorage * 2. Browser language * 3. Default locale */ export function getInitialLocale( storageKey: string, supportedLocales: readonly string[], defaultLocale = 'en' ): string { // Check localStorage first const stored = getStoredLocale(storageKey, supportedLocales); if (stored) { return stored; } // Fall back to browser language return detectBrowserLocale(supportedLocales, defaultLocale); } /** * Normalize locale code to standard format * Examples: 'en-us' -> 'en-US', 'pt_BR' -> 'pt-BR' */ export function normalizeLocale(locale: string): string { const parts = locale.replace('_', '-').split('-'); if (parts.length === 1) { return parts[0].toLowerCase(); } return `${parts[0].toLowerCase()}-${parts[1].toUpperCase()}`; } /** * Get base language from locale code * Examples: 'pt-BR' -> 'pt', 'en-US' -> 'en' */ export function getBaseLanguage(locale: string): string { return locale.split('-')[0].toLowerCase(); } /** * Check if locale matches a language (including variants) * Examples: matchesLanguage('pt-BR', 'pt') -> true */ export function matchesLanguage(locale: string, language: string): boolean { const normalizedLocale = normalizeLocale(locale); const normalizedLanguage = language.toLowerCase(); return ( normalizedLocale === normalizedLanguage || getBaseLanguage(normalizedLocale) === normalizedLanguage ); } /** * Find best matching locale from supported list */ export function findBestMatch( preferredLocale: string, supportedLocales: readonly string[], defaultLocale = 'en' ): string { const normalized = normalizeLocale(preferredLocale); // Exact match if (supportedLocales.includes(normalized)) { return normalized; } // Base language match const base = getBaseLanguage(normalized); if (supportedLocales.includes(base)) { return base; } // Find any variant of the same language const variant = supportedLocales.find((loc) => getBaseLanguage(loc) === base); if (variant) { return variant; } return defaultLocale; } /** * Format number according to locale */ export function formatLocalizedNumber( value: number, locale = 'en', options?: Intl.NumberFormatOptions ): string { return new Intl.NumberFormat(locale, options).format(value); } /** * Format date according to locale */ export function formatLocalizedDate( date: Date | string | number, locale = 'en', options?: Intl.DateTimeFormatOptions ): string { const dateObj = date instanceof Date ? date : new Date(date); return new Intl.DateTimeFormat(locale, options).format(dateObj); } /** * Format relative time according to locale */ export function formatRelativeTime( date: Date | string | number, locale = 'en', style: 'long' | 'short' | 'narrow' = 'long' ): string { const dateObj = date instanceof Date ? date : new Date(date); const now = new Date(); const diffMs = dateObj.getTime() - now.getTime(); const diffSecs = Math.round(diffMs / 1000); const diffMins = Math.round(diffSecs / 60); const diffHours = Math.round(diffMins / 60); const diffDays = Math.round(diffHours / 24); const diffWeeks = Math.round(diffDays / 7); const diffMonths = Math.round(diffDays / 30); const diffYears = Math.round(diffDays / 365); const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto', style }); if (Math.abs(diffSecs) < 60) { return rtf.format(diffSecs, 'second'); } else if (Math.abs(diffMins) < 60) { return rtf.format(diffMins, 'minute'); } else if (Math.abs(diffHours) < 24) { return rtf.format(diffHours, 'hour'); } else if (Math.abs(diffDays) < 7) { return rtf.format(diffDays, 'day'); } else if (Math.abs(diffWeeks) < 4) { return rtf.format(diffWeeks, 'week'); } else if (Math.abs(diffMonths) < 12) { return rtf.format(diffMonths, 'month'); } else { return rtf.format(diffYears, 'year'); } } /** * Get plural form category */ export function getPluralCategory(count: number, locale = 'en'): Intl.LDMLPluralRule { const pr = new Intl.PluralRules(locale); return pr.select(count); } /** * Interpolate values into a translation string * Example: interpolate("Hello {name}!", { name: "World" }) -> "Hello World!" */ export function interpolate(text: string, values: Record): string { return text.replace(/\{(\w+)\}/g, (match, key) => { return key in values ? String(values[key]) : match; }); } /** * Interface for PillDropdown items (matches shared-ui type) */ export interface LanguageDropdownItem { id: string; label: string; icon?: string; onClick: () => void; active?: boolean; } /** * Create PillDropdown items for language selection * @param supportedLocales - Array of supported locale codes (e.g., ['de', 'en', 'it', 'fr', 'es']) * @param currentLocale - Currently selected locale * @param onLocaleChange - Callback when locale changes * @returns Array of items compatible with PillDropdown */ export function getLanguageDropdownItems( supportedLocales: readonly string[], currentLocale: string, onLocaleChange: (locale: string) => void ): LanguageDropdownItem[] { return supportedLocales.map((locale) => { const info = getLanguageInfo(locale); return { id: locale, label: info ? `${info.emoji} ${info.nativeName}` : locale.toUpperCase(), onClick: () => onLocaleChange(locale), active: currentLocale === locale, }; }); } /** * Get current language label for PillDropdown trigger * @param currentLocale - Currently selected locale * @returns Label with flag emoji and native name (e.g., "🇩🇪 Deutsch") */ export function getCurrentLanguageLabel(currentLocale: string): string { const info = getLanguageInfo(currentLocale); if (info) { return `${info.emoji} ${info.nativeName}`; } return currentLocale.toUpperCase(); }