ice/src/i18n/index.ts
Claude Code 8b1787ec47 Add comprehensive internationalization (i18n) with Spanish language support
This major feature introduces complete internationalization architecture with Spanish (Mexico) as the first additional language:

## New Features:
- **I18n Architecture**: Complete client-side and server-side internationalization system
- **Spanish (Mexico) Support**: Full es-MX translations for all user-facing text
- **Language Selector**: Dynamic language switching UI component in header
- **API Endpoints**: RESTful endpoints for serving translations (/api/i18n)
- **Progressive Enhancement**: Language detection via Accept-Language header and cookies

## Technical Implementation:
- **Frontend**: Client-side i18n.js with automatic DOM translation and language selector
- **Backend**: Server-side i18n service with locale detection middleware
- **Build Process**: Automated copying of translation files to dist/ directory
- **Responsive Design**: Language selector integrated into header controls layout

## Files Added:
- public/i18n.js - Client-side internationalization library
- src/i18n/index.ts - Server-side i18n service
- src/i18n/locales/en.json - English translations
- src/i18n/locales/es-MX.json - Spanish (Mexico) translations
- src/routes/i18n.ts - API endpoints for translations

## Files Modified:
- package.json - Updated build process to include i18n files
- public/index.html - Added i18n attributes and language selector
- public/app.js - Integrated dynamic translation updates
- src/server.ts - Added locale detection middleware
- src/scss/pages/_index.scss - Language selector styling

This implementation supports easy addition of future languages and maintains backward compatibility while providing a seamless multilingual experience.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-07 12:25:44 -04:00

170 lines
No EOL
4.6 KiB
TypeScript

import fs from 'fs';
import path from 'path';
export interface TranslationData {
[key: string]: any;
}
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: any = translations;
for (const key of keys) {
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;