ice/src/i18n/index.ts
Claude Code 39ece1b37a Fix TypeScript compilation errors and type safety issues
- Fix i18n string indexing by adding proper type checking
- Add Express Request type extensions for locale and t properties
- Fix ProfanityFilter interface mismatches with proper return types
- Update route signatures to accept union types for fallback filter
- Resolve all TypeScript compilation errors while maintaining functionality

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-07 21:23:36 -04:00

174 lines
No EOL
4.8 KiB
TypeScript

import fs from 'fs';
import path from 'path';
export interface TranslationData {
[key: string]: string | TranslationData;
}
export class I18nService {
private translations: Map<string, TranslationData> = new Map();
private defaultLocale = 'en';
private availableLocales = ['en', 'es-MX'];
constructor() {
this.loadTranslations();
}
/**
* Load all translation files from the locales directory
*/
private loadTranslations(): void {
const localesDir = path.join(__dirname, 'locales');
for (const locale of this.availableLocales) {
try {
const filePath = path.join(localesDir, `${locale}.json`);
const translationData = JSON.parse(fs.readFileSync(filePath, 'utf8'));
this.translations.set(locale, translationData);
console.log(`Loaded translations for locale: ${locale}`);
} catch (error) {
console.error(`Failed to load translations for locale ${locale}:`, error);
}
}
}
/**
* Get a translated string by key path (e.g., 'common.appName')
*/
public t(keyPath: string, locale: string = this.defaultLocale, params?: Record<string, string>): string {
const translations = this.translations.get(locale) || this.translations.get(this.defaultLocale);
if (!translations) {
console.warn(`No translations found for locale: ${locale}`);
return keyPath;
}
const keys = keyPath.split('.');
let value: string | TranslationData = translations;
for (const key of keys) {
if (typeof value === 'string') {
// If we hit a string before traversing all keys, the path is invalid
break;
}
value = value?.[key];
if (value === undefined) {
// Fallback to default locale if key not found
if (locale !== this.defaultLocale) {
return this.t(keyPath, this.defaultLocale, params);
}
console.warn(`Translation key not found: ${keyPath} for locale: ${locale}`);
return keyPath;
}
}
if (typeof value !== 'string') {
console.warn(`Translation value is not a string for key: ${keyPath}`);
return keyPath;
}
// Replace parameters if provided
if (params) {
return this.replaceParams(value, params);
}
return value;
}
/**
* Replace parameters in translation strings (e.g., "Hello {{name}}" with {name: "John"})
*/
private replaceParams(text: string, params: Record<string, string>): string {
return text.replace(/\{\{(\w+)\}\}/g, (match, key) => {
return params[key] || match;
});
}
/**
* Get all translations for a specific locale
*/
public getTranslations(locale: string): TranslationData | null {
return this.translations.get(locale) || null;
}
/**
* Get list of available locales
*/
public getAvailableLocales(): string[] {
return [...this.availableLocales];
}
/**
* Check if a locale is supported
*/
public isLocaleSupported(locale: string): boolean {
return this.availableLocales.includes(locale);
}
/**
* Get the default locale
*/
public getDefaultLocale(): string {
return this.defaultLocale;
}
/**
* Detect user's preferred locale from Accept-Language header
*/
public detectLocale(acceptLanguageHeader?: string): string {
if (!acceptLanguageHeader) {
return this.defaultLocale;
}
// Parse Accept-Language header (e.g., "es-MX,es;q=0.9,en;q=0.8")
const languages = acceptLanguageHeader
.split(',')
.map(lang => {
const parts = lang.trim().split(';');
const code = parts[0];
const quality = parts[1] ? parseFloat(parts[1].split('=')[1]) : 1;
return { code, quality };
})
.sort((a, b) => b.quality - a.quality);
// Find the first supported locale
for (const lang of languages) {
// Check for exact match first (e.g., "es-MX")
if (this.isLocaleSupported(lang.code)) {
return lang.code;
}
// Check for language match (e.g., "es" matches "es-MX")
const languageCode = lang.code.split('-')[0];
const matchingLocale = this.availableLocales.find(locale =>
locale.startsWith(languageCode)
);
if (matchingLocale) {
return matchingLocale;
}
}
return this.defaultLocale;
}
/**
* Get locale display names
*/
public getLocaleDisplayName(locale: string): string {
const displayNames: Record<string, string> = {
'en': 'English',
'es-MX': 'Español (México)'
};
return displayNames[locale] || locale;
}
}
// Create singleton instance
export const i18nService = new I18nService();
// Helper function for easier access
export function t(keyPath: string, locale?: string, params?: Record<string, string>): string {
return i18nService.t(keyPath, locale, params);
}
export default i18nService;