174 lines
5.1 KiB
TypeScript
174 lines
5.1 KiB
TypeScript
import type {Language} from './languages.js';
|
|
// Static imports for all translation files
|
|
import enTranslations from './locales/en.json' with {type: 'json'};
|
|
import nlTranslations from './locales/nl.json' with {type: 'json'};
|
|
import frTranslations from './locales/fr.json' with {type: 'json'};
|
|
import deTranslations from './locales/de.json' with {type: 'json'};
|
|
import hiTranslations from './locales/hi.json' with {type: 'json'};
|
|
import ptTranslations from './locales/pt.json' with {type: 'json'};
|
|
import bgTranslations from './locales/bg.json' with {type: 'json'};
|
|
import csTranslations from './locales/cs.json' with {type: 'json'};
|
|
import plTranslations from './locales/pl.json' with {type: 'json'};
|
|
import esTranslations from './locales/es.json' with {type: 'json'};
|
|
|
|
export {
|
|
SUPPORTED_LANGUAGES,
|
|
DEFAULT_LANGUAGE,
|
|
isValidLanguageCode,
|
|
getLanguageByCode,
|
|
type Language,
|
|
} from './languages.js';
|
|
|
|
export type TranslationKey = string;
|
|
|
|
export interface Translations {
|
|
pages: {
|
|
unsubscribe: Record<string, string>;
|
|
subscribe: Record<string, string>;
|
|
manage: Record<string, string>;
|
|
common: Record<string, string>;
|
|
};
|
|
email: {
|
|
footer: Record<string, string>;
|
|
};
|
|
}
|
|
|
|
// Static mapping of language codes to translations
|
|
const translationsMap: Record<string, Translations> = {
|
|
en: enTranslations,
|
|
nl: nlTranslations,
|
|
de: deTranslations,
|
|
fr: frTranslations,
|
|
hi: hiTranslations,
|
|
pt: ptTranslations,
|
|
bg: bgTranslations,
|
|
cs: csTranslations,
|
|
pl: plTranslations,
|
|
es: esTranslations,
|
|
};
|
|
|
|
// In-memory cache for loaded translations
|
|
const translationCache = new Map<string, Translations>();
|
|
|
|
/**
|
|
* Load translations for a given language code (async)
|
|
* Uses static imports for reliable resolution
|
|
*/
|
|
export async function loadTranslations(languageCode: string): Promise<Translations> {
|
|
// Check cache first
|
|
if (translationCache.has(languageCode)) {
|
|
return translationCache.get(languageCode)!;
|
|
}
|
|
|
|
// Get translations from static map
|
|
const translations = translationsMap[languageCode];
|
|
|
|
if (translations) {
|
|
translationCache.set(languageCode, translations);
|
|
return translations;
|
|
}
|
|
|
|
// Fallback to English if language not found
|
|
console.warn(`Translation file not found for ${languageCode}, falling back to English`);
|
|
if (languageCode !== 'en') {
|
|
return loadTranslations('en');
|
|
}
|
|
|
|
throw new Error(`Failed to load translations for ${languageCode}`);
|
|
}
|
|
|
|
/**
|
|
* Synchronous translation loader (requires pre-built translations)
|
|
* For API server use (compiled at build time)
|
|
*/
|
|
export function loadTranslationsSync(languageCode: string): Translations {
|
|
if (translationCache.has(languageCode)) {
|
|
return translationCache.get(languageCode)!;
|
|
}
|
|
|
|
// Get translations from static map
|
|
const translations = translationsMap[languageCode];
|
|
|
|
if (translations) {
|
|
translationCache.set(languageCode, translations);
|
|
return translations;
|
|
}
|
|
|
|
// Fallback to English if language not found
|
|
if (languageCode !== 'en') {
|
|
return loadTranslationsSync('en');
|
|
}
|
|
|
|
throw new Error(`Failed to load translations for ${languageCode}`);
|
|
}
|
|
|
|
/**
|
|
* Simple template string replacement
|
|
* Example: interpolate("Hello {name}", { name: "World" }) => "Hello World"
|
|
*/
|
|
export function interpolate(template: string, values: Record<string, string>): string {
|
|
return template.replace(/\{(\w+)\}/g, (_, key) => values[key] || `{${key}}`);
|
|
}
|
|
|
|
/**
|
|
* Get nested translation value by dot notation path
|
|
* Example: getTranslation(translations, "pages.unsubscribe.title")
|
|
*/
|
|
export function getTranslation(translations: Translations, path: string): string {
|
|
const keys = path.split('.');
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
let value: any = translations;
|
|
|
|
for (const key of keys) {
|
|
value = value?.[key];
|
|
if (value === undefined) {
|
|
console.warn(`Translation key not found: ${path}`);
|
|
return path; // Return the key itself as fallback
|
|
}
|
|
}
|
|
|
|
return typeof value === 'string' ? value : path;
|
|
}
|
|
|
|
/**
|
|
* Translation helper class for easier use
|
|
*/
|
|
export class Translator {
|
|
private translations: Translations;
|
|
private languageCode: string;
|
|
|
|
constructor(translations: Translations, languageCode: string) {
|
|
this.translations = translations;
|
|
this.languageCode = languageCode;
|
|
}
|
|
|
|
/**
|
|
* Translate a key with optional interpolation
|
|
* Example: t("pages.unsubscribe.description", { email: "[email protected]" })
|
|
*/
|
|
t(key: string, values?: Record<string, string>): string {
|
|
const translation = getTranslation(this.translations, key);
|
|
return values ? interpolate(translation, values) : translation;
|
|
}
|
|
|
|
getLanguageCode(): string {
|
|
return this.languageCode;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a translator instance for a given language (async)
|
|
*/
|
|
export async function createTranslator(languageCode: string): Promise<Translator> {
|
|
const translations = await loadTranslations(languageCode);
|
|
return new Translator(translations, languageCode);
|
|
}
|
|
|
|
/**
|
|
* Create a translator instance for a given language (sync)
|
|
*/
|
|
export function createTranslatorSync(languageCode: string): Translator {
|
|
const translations = loadTranslationsSync(languageCode);
|
|
return new Translator(translations, languageCode);
|
|
}
|