You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
354 lines
9.0 KiB
354 lines
9.0 KiB
/**
|
|
* ============================================================================
|
|
* INTERNATIONALIZATION (i18n) - Utilities
|
|
* ============================================================================
|
|
*
|
|
* Sistema de internacionalización para textos multiidioma
|
|
*
|
|
* ============================================================================
|
|
*/
|
|
import type {SupportedLocale, I18nString, I18nTranslations, LocaleInfo} from "../types/i18n.ts";
|
|
import { DEFAULT_LOCALE } from "../types/i18n.ts";
|
|
|
|
// ============================================================================
|
|
// SUPPORTED LOCALES
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Lista de todos los locales soportados
|
|
*/
|
|
export const SUPPORTED_LOCALES: readonly SupportedLocale[] = [
|
|
'es',
|
|
'en',
|
|
'de',
|
|
'fr',
|
|
'it',
|
|
'pt',
|
|
'ca',
|
|
'eu',
|
|
'gl'
|
|
] as const;
|
|
|
|
|
|
// ============================================================================
|
|
// LOCALE UTILITIES
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Verificar si un string es un locale soportado
|
|
*/
|
|
export function isSupportedLocale(locale: string): locale is SupportedLocale {
|
|
return SUPPORTED_LOCALES.includes(locale as SupportedLocale);
|
|
}
|
|
|
|
/**
|
|
* Obtener locale válido con fallback
|
|
*/
|
|
export function getValidLocale(
|
|
locale: string | undefined,
|
|
fallback: SupportedLocale = DEFAULT_LOCALE
|
|
): SupportedLocale {
|
|
if (!locale) return fallback;
|
|
return isSupportedLocale(locale) ? locale : fallback;
|
|
}
|
|
|
|
/**
|
|
* Extraer código de idioma de un locale (ej: "es-ES" → "es")
|
|
*/
|
|
export function extractLanguageCode(locale: string): string {
|
|
return locale.split('-')[0].toLowerCase();
|
|
}
|
|
|
|
// ============================================================================
|
|
// TRANSLATION UTILITIES
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Obtener texto traducido
|
|
*
|
|
* @param text - String i18n (simple o con traducciones)
|
|
* @param locale - Locale objetivo
|
|
* @param fallback - Locale de respaldo si no existe la traducción
|
|
* @returns Texto en el idioma solicitado
|
|
*
|
|
* @example
|
|
* const text: I18nString = { es: "Hola", en: "Hello" };
|
|
* t(text, 'es') // → "Hola"
|
|
* t(text, 'fr') // → "Hola" (fallback a español)
|
|
* t("OK", 'es') // → "OK" (string simple)
|
|
*/
|
|
export function t(
|
|
text: I18nString,
|
|
locale: SupportedLocale,
|
|
fallback: SupportedLocale = DEFAULT_LOCALE
|
|
): string {
|
|
// Si es string simple, devolverlo directamente
|
|
if (typeof text === 'string') {
|
|
return text;
|
|
}
|
|
|
|
// Intentar con el locale solicitado
|
|
if (text[locale]) {
|
|
return text[locale];
|
|
}
|
|
|
|
// Fallback al locale de respaldo
|
|
if (text[fallback]) {
|
|
return text[fallback];
|
|
}
|
|
|
|
// Último recurso: primer valor disponible
|
|
const firstAvailable = Object.values(text)[0];
|
|
return firstAvailable || '';
|
|
}
|
|
|
|
/**
|
|
* Alias de `t` para compatibilidad con librerías i18n
|
|
*/
|
|
export const translate = t;
|
|
|
|
/**
|
|
* Verificar si un I18nString tiene traducción para un locale
|
|
*/
|
|
export function hasTranslation(
|
|
text: I18nString,
|
|
locale: SupportedLocale
|
|
): boolean {
|
|
if (typeof text === 'string') return true;
|
|
return text[locale] !== undefined;
|
|
}
|
|
|
|
/**
|
|
* Obtener todos los locales disponibles en un I18nString
|
|
*/
|
|
export function getAvailableLocales(text: I18nString): SupportedLocale[] {
|
|
if (typeof text === 'string') return [...SUPPORTED_LOCALES];
|
|
return Object.keys(text) as SupportedLocale[];
|
|
}
|
|
|
|
/**
|
|
* Verificar si un I18nString está completo (tiene todas las traducciones)
|
|
*/
|
|
export function isComplete(text: I18nString): boolean {
|
|
if (typeof text === 'string') return true;
|
|
return SUPPORTED_LOCALES.every(locale => text[locale] !== undefined);
|
|
}
|
|
|
|
/**
|
|
* Obtener locales faltantes en un I18nString
|
|
*/
|
|
export function getMissingLocales(text: I18nString): SupportedLocale[] {
|
|
if (typeof text === 'string') return [];
|
|
return SUPPORTED_LOCALES.filter(locale => text[locale] === undefined);
|
|
}
|
|
|
|
// ============================================================================
|
|
// I18N BUILDERS
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Crear I18nString desde objeto parcial
|
|
* Garantiza que al menos existe el locale por defecto
|
|
*/
|
|
export function createI18nString(
|
|
translations: Partial<Record<SupportedLocale, string>>
|
|
): I18nString {
|
|
if (!translations[DEFAULT_LOCALE]) {
|
|
throw new Error(`I18nString must have at least '${DEFAULT_LOCALE}' translation`);
|
|
}
|
|
return translations as I18nTranslations;
|
|
}
|
|
|
|
/**
|
|
* Crear I18nString simple (mismo texto en todos los idiomas)
|
|
*/
|
|
export function createSimpleI18nString(text: string): I18nString {
|
|
return text;
|
|
}
|
|
|
|
/**
|
|
* Convertir string simple a objeto de traducciones
|
|
* (útil para migración)
|
|
*/
|
|
export function expandToTranslations(
|
|
text: I18nString,
|
|
defaultLocale: SupportedLocale = DEFAULT_LOCALE
|
|
): I18nTranslations {
|
|
if (typeof text === 'string') {
|
|
return { [defaultLocale]: text } as I18nTranslations;
|
|
}
|
|
return text;
|
|
}
|
|
|
|
/**
|
|
* Combinar múltiples I18nStrings
|
|
* (útil para concatenar textos)
|
|
*/
|
|
export function combineI18nStrings(
|
|
strings: I18nString[],
|
|
separator: string = ' '
|
|
): I18nString {
|
|
// Si todos son strings simples, concatenar directamente
|
|
if (strings.every(s => typeof s === 'string')) {
|
|
return strings.join(separator);
|
|
}
|
|
|
|
// Crear objeto con todas las traducciones
|
|
const combined: Partial<Record<SupportedLocale, string>> = {};
|
|
|
|
for (const locale of SUPPORTED_LOCALES) {
|
|
const parts = strings.map(s => t(s, locale));
|
|
combined[locale] = parts.join(separator);
|
|
}
|
|
|
|
return combined as I18nTranslations;
|
|
}
|
|
|
|
// ============================================================================
|
|
// LOCALE METADATA
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Metadata de locales soportados
|
|
*/
|
|
export const LOCALE_INFO: Record<SupportedLocale, LocaleInfo> = {
|
|
es: {
|
|
code: 'es',
|
|
name: 'Spanish',
|
|
nativeName: 'Español',
|
|
flag: '🇪🇸'
|
|
},
|
|
en: {
|
|
code: 'en',
|
|
name: 'English',
|
|
nativeName: 'English',
|
|
flag: '🇬🇧'
|
|
},
|
|
de: {
|
|
code: 'de',
|
|
name: 'German',
|
|
nativeName: 'Deutsch',
|
|
flag: '🇩🇪'
|
|
},
|
|
fr: {
|
|
code: 'fr',
|
|
name: 'French',
|
|
nativeName: 'Français',
|
|
flag: '🇫🇷'
|
|
},
|
|
it: {
|
|
code: 'it',
|
|
name: 'Italian',
|
|
nativeName: 'Italiano',
|
|
flag: '🇮🇹'
|
|
},
|
|
pt: {
|
|
code: 'pt',
|
|
name: 'Portuguese',
|
|
nativeName: 'Português',
|
|
flag: '🇵🇹'
|
|
},
|
|
ca: {
|
|
code: 'ca',
|
|
name: 'Catalan',
|
|
nativeName: 'Català',
|
|
flag: '🏴'
|
|
},
|
|
eu: {
|
|
code: 'eu',
|
|
name: 'Basque',
|
|
nativeName: 'Euskara',
|
|
flag: '🏴'
|
|
},
|
|
gl: {
|
|
code: 'gl',
|
|
name: 'Galician',
|
|
nativeName: 'Galego',
|
|
flag: '🏴'
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Obtener información de un locale
|
|
*/
|
|
export function getLocaleInfo(locale: SupportedLocale): LocaleInfo {
|
|
return LOCALE_INFO[locale];
|
|
}
|
|
|
|
// ============================================================================
|
|
// VALIDATION
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Validar que un I18nString es correcto
|
|
*/
|
|
export function validateI18nString(text: unknown): text is I18nString {
|
|
if (typeof text === 'string') return true;
|
|
|
|
if (typeof text !== 'object' || text === null) return false;
|
|
|
|
const obj = text as Record<string, unknown>;
|
|
|
|
// Debe tener al menos el locale por defecto
|
|
if (typeof obj[DEFAULT_LOCALE] !== 'string') return false;
|
|
|
|
// Todos los valores deben ser strings
|
|
return Object.values(obj).every(v => typeof v === 'string');
|
|
}
|
|
|
|
/**
|
|
* Error para I18nString inválido
|
|
*/
|
|
export class I18nStringError extends Error {
|
|
constructor(message: string) {
|
|
super(`[I18n] ${message}`);
|
|
this.name = 'I18nStringError';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validar y lanzar error si es inválido
|
|
*/
|
|
export function assertValidI18nString(text: unknown): asserts text is I18nString {
|
|
if (!validateI18nString(text)) {
|
|
throw new I18nStringError(
|
|
`Invalid I18nString. Must be a string or object with at least '${DEFAULT_LOCALE}' key.`
|
|
);
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// EXPORTS
|
|
// ============================================================================
|
|
|
|
export default {
|
|
// Constantes y Metadata
|
|
SUPPORTED_LOCALES,
|
|
|
|
// Utilities de Locale
|
|
isSupportedLocale,
|
|
getValidLocale,
|
|
extractLanguageCode,
|
|
getLocaleInfo,
|
|
|
|
// Traducción
|
|
t,
|
|
translate,
|
|
hasTranslation,
|
|
getAvailableLocales,
|
|
isComplete,
|
|
getMissingLocales,
|
|
|
|
// Builders
|
|
createI18nString,
|
|
createSimpleI18nString,
|
|
expandToTranslations,
|
|
combineI18nStrings,
|
|
|
|
// Validación y Errores
|
|
validateI18nString,
|
|
assertValidI18nString,
|
|
I18nStringError
|
|
};
|
|
|