mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 05:01:08 +02:00
290 lines
7.2 KiB
TypeScript
290 lines
7.2 KiB
TypeScript
/**
|
|
* 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, string | number>): 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();
|
|
}
|