Skip to main content
Glama
I18nTestUtils.ts15.9 kB
/** * Internationalization Test Utilities * * Provides utilities for testing internationalization features with configurable * locales and timezones to ensure tests work in any environment. */ export interface I18nTestConfig { locale: string; timezone: string; dateFormat?: string; currency?: string; rtl?: boolean; } export interface LocaleTestCase { name: string; config: I18nTestConfig; expectedFormats: { date?: string; time?: string; currency?: string; number?: string; }; } /** * Common locale configurations for testing */ export const TEST_LOCALES: Record<string, I18nTestConfig> = { // English locales 'en-US': { locale: 'en-US', timezone: 'America/New_York', dateFormat: 'MM/dd/yyyy', currency: 'USD' }, 'en-GB': { locale: 'en-GB', timezone: 'Europe/London', dateFormat: 'dd/MM/yyyy', currency: 'GBP' }, // Asian locales 'ko-KR': { locale: 'ko-KR', timezone: 'Asia/Seoul', dateFormat: 'yyyy-MM-dd', currency: 'KRW' }, 'ja-JP': { locale: 'ja-JP', timezone: 'Asia/Tokyo', dateFormat: 'yyyy/MM/dd', currency: 'JPY' }, 'zh-CN': { locale: 'zh-CN', timezone: 'Asia/Shanghai', dateFormat: 'yyyy-MM-dd', currency: 'CNY' }, // European locales 'de-DE': { locale: 'de-DE', timezone: 'Europe/Berlin', dateFormat: 'dd.MM.yyyy', currency: 'EUR' }, 'fr-FR': { locale: 'fr-FR', timezone: 'Europe/Paris', dateFormat: 'dd/MM/yyyy', currency: 'EUR' }, 'es-ES': { locale: 'es-ES', timezone: 'Europe/Madrid', dateFormat: 'dd/MM/yyyy', currency: 'EUR' }, // RTL locales 'ar-SA': { locale: 'ar-SA', timezone: 'Asia/Riyadh', dateFormat: 'dd/MM/yyyy', currency: 'SAR', rtl: true }, 'he-IL': { locale: 'he-IL', timezone: 'Asia/Jerusalem', dateFormat: 'dd/MM/yyyy', currency: 'ILS', rtl: true }, // Default/Universal 'UTC': { locale: 'en-US', timezone: 'UTC', dateFormat: 'yyyy-MM-dd', currency: 'USD' } }; /** * Common timezone test cases */ export const TEST_TIMEZONES = [ 'UTC', 'America/New_York', 'America/Los_Angeles', 'Europe/London', 'Europe/Berlin', 'Asia/Seoul', 'Asia/Tokyo', 'Asia/Shanghai', 'Australia/Sydney' ]; /** * I18n Test Environment Manager */ export class I18nTestEnvironment { private originalLocale: string; private originalTimezone: string; private originalIntlLocale: string; constructor() { // Store original environment settings this.originalLocale = process.env.LANG || 'en-US'; this.originalTimezone = process.env.TZ || Intl.DateTimeFormat().resolvedOptions().timeZone; this.originalIntlLocale = Intl.DateTimeFormat().resolvedOptions().locale; } /** * Set test environment to specific locale configuration */ setTestEnvironment(config: I18nTestConfig): void { process.env.LANG = config.locale; process.env.TZ = config.timezone; // Mock Intl.DateTimeFormat for consistent testing const originalDateTimeFormat = Intl.DateTimeFormat; jest.spyOn(Intl, 'DateTimeFormat').mockImplementation((locales?: any, options?: any) => { return new originalDateTimeFormat(config.locale, { ...options, timeZone: config.timezone }); }); } /** * Restore original environment settings */ restoreEnvironment(): void { process.env.LANG = this.originalLocale; process.env.TZ = this.originalTimezone; // Restore Intl mocks if (jest.isMockFunction(Intl.DateTimeFormat)) { (Intl.DateTimeFormat as jest.MockedFunction<any>).mockRestore(); } } /** * Get current system locale for fallback testing */ getSystemLocale(): I18nTestConfig { return { locale: this.originalIntlLocale, timezone: this.originalTimezone }; } } /** * Locale-aware test utilities */ export class LocaleTestUtils { /** * Format date according to locale configuration */ static formatDate(date: Date | string, config: I18nTestConfig): string { const dateObj = typeof date === 'string' ? new Date(date) : date; return new Intl.DateTimeFormat(config.locale, { timeZone: config.timezone, year: 'numeric', month: '2-digit', day: '2-digit' }).format(dateObj); } /** * Format time according to locale configuration */ static formatTime(date: Date | string, config: I18nTestConfig): string { const dateObj = typeof date === 'string' ? new Date(date) : date; return new Intl.DateTimeFormat(config.locale, { timeZone: config.timezone, hour: '2-digit', minute: '2-digit', second: '2-digit' }).format(dateObj); } /** * Format currency according to locale configuration */ static formatCurrency(amount: number, config: I18nTestConfig): string { return new Intl.NumberFormat(config.locale, { style: 'currency', currency: config.currency || 'USD' }).format(amount); } /** * Get timezone offset for specific timezone */ static getTimezoneOffset(timezone: string, date?: Date): number { const testDate = date || new Date(); // Create two dates: one in UTC, one in the target timezone const utcDate = new Date(testDate.toLocaleString('en-US', { timeZone: 'UTC' })); const timezoneDate = new Date(testDate.toLocaleString('en-US', { timeZone: timezone })); return (utcDate.getTime() - timezoneDate.getTime()) / (1000 * 60); // Return offset in minutes } /** * Check if date/time is valid in timezone context */ static isValidInTimezone(date: Date, timezone: string): boolean { try { const formatted = date.toLocaleString('en-US', { timeZone: timezone }); return !isNaN(Date.parse(formatted)); } catch { return false; } } /** * Convert date between timezones */ static convertTimezone(date: Date, fromTimezone: string, toTimezone: string): Date { // Get the time in the source timezone const sourceTime = new Date(date.toLocaleString('en-US', { timeZone: fromTimezone })); // Calculate the offset difference const sourceOffset = this.getTimezoneOffset(fromTimezone, date); const targetOffset = this.getTimezoneOffset(toTimezone, date); const offsetDiff = targetOffset - sourceOffset; // Apply the offset difference return new Date(sourceTime.getTime() + (offsetDiff * 60 * 1000)); } } /** * Test case generator for parameterized locale testing */ export class LocaleTestCaseGenerator { /** * Generate test cases for multiple locales */ static generateLocaleTestCases( baseTestCase: Omit<LocaleTestCase, 'name' | 'config'>, locales: string[] = ['en-US', 'ko-KR', 'de-DE', 'ja-JP'] ): LocaleTestCase[] { return locales.map(localeKey => ({ ...baseTestCase, name: `${baseTestCase.name || 'test'} (${localeKey})`, config: TEST_LOCALES[localeKey] || TEST_LOCALES['UTC'] })); } /** * Generate timezone-specific test cases */ static generateTimezoneTestCases( baseTestCase: Omit<LocaleTestCase, 'name' | 'config'>, timezones: string[] = ['UTC', 'America/New_York', 'Asia/Seoul', 'Europe/London'] ): LocaleTestCase[] { return timezones.map(timezone => ({ ...baseTestCase, name: `${baseTestCase.name || 'test'} (${timezone})`, config: { locale: 'en-US', timezone, dateFormat: 'yyyy-MM-dd' } })); } /** * Generate comprehensive test cases (locales + timezones) */ static generateComprehensiveTestCases( baseTestCase: Omit<LocaleTestCase, 'name' | 'config'>, options: { locales?: string[]; timezones?: string[]; combinations?: 'minimal' | 'standard' | 'comprehensive'; } = {} ): LocaleTestCase[] { const { combinations = 'standard' } = options; let locales: string[]; let timezones: string[]; switch (combinations) { case 'minimal': locales = ['en-US', 'ko-KR']; timezones = ['UTC', 'Asia/Seoul']; break; case 'comprehensive': locales = Object.keys(TEST_LOCALES); timezones = TEST_TIMEZONES; break; default: // standard locales = ['en-US', 'ko-KR', 'de-DE', 'ja-JP']; timezones = ['UTC', 'America/New_York', 'Asia/Seoul', 'Europe/Berlin']; } // Override with provided options if (options.locales) locales = options.locales; if (options.timezones) timezones = options.timezones; const testCases: LocaleTestCase[] = []; for (const localeKey of locales) { for (const timezone of timezones) { const baseConfig = TEST_LOCALES[localeKey] || TEST_LOCALES['UTC']; testCases.push({ ...baseTestCase, name: `${baseTestCase.name || 'test'} (${localeKey}/${timezone})`, config: { ...baseConfig, timezone // Override timezone for cross-combinations } }); } } return testCases; } } /** * Mock I18n Service for testing */ export interface MockI18nService { getCurrentLocale(): string; getCurrentTimezone(): string; setLocale(locale: string): void; setTimezone(timezone: string): void; formatDate(date: Date | string, options?: Intl.DateTimeFormatOptions): string; formatTime(date: Date | string, options?: Intl.DateTimeFormatOptions): string; formatCurrency(amount: number, currency?: string): string; translate(key: string, params?: Record<string, any>): string; detectSystemLocale(): I18nTestConfig; isRTL(): boolean; } export class MockI18nServiceFactory { static create(initialConfig: I18nTestConfig = TEST_LOCALES['en-US']): jest.Mocked<MockI18nService> { let currentConfig = { ...initialConfig }; return { getCurrentLocale: jest.fn().mockImplementation(() => currentConfig.locale), getCurrentTimezone: jest.fn().mockImplementation(() => currentConfig.timezone), setLocale: jest.fn().mockImplementation((locale: string) => { currentConfig.locale = locale; // Update related config if we have predefined locale const predefinedConfig = TEST_LOCALES[locale]; if (predefinedConfig) { currentConfig = { ...currentConfig, ...predefinedConfig }; } }), setTimezone: jest.fn().mockImplementation((timezone: string) => { currentConfig.timezone = timezone; }), formatDate: jest.fn().mockImplementation((date: Date | string, options?: Intl.DateTimeFormatOptions) => { return LocaleTestUtils.formatDate(date, currentConfig); }), formatTime: jest.fn().mockImplementation((date: Date | string, options?: Intl.DateTimeFormatOptions) => { return LocaleTestUtils.formatTime(date, currentConfig); }), formatCurrency: jest.fn().mockImplementation((amount: number, currency?: string) => { const configWithCurrency = { ...currentConfig, currency: currency || currentConfig.currency }; return LocaleTestUtils.formatCurrency(amount, configWithCurrency); }), translate: jest.fn().mockImplementation((key: string, params?: Record<string, any>) => { // Simple mock translation - in real implementation would use translation dictionaries const translations: Record<string, Record<string, string>> = { 'en-US': { 'hello': 'Hello', 'goodbye': 'Goodbye', 'error': 'Error occurred' }, 'ko-KR': { 'hello': '안녕하세요', 'goodbye': '안녕히 가세요', 'error': '오류가 발생했습니다' }, 'de-DE': { 'hello': 'Hallo', 'goodbye': 'Auf Wiedersehen', 'error': 'Fehler aufgetreten' } }; const localeTranslations = translations[currentConfig.locale] || translations['en-US']; let translation = localeTranslations[key] || key; // Simple parameter substitution if (params) { Object.entries(params).forEach(([paramKey, value]) => { translation = translation.replace(`{${paramKey}}`, String(value)); }); } return translation; }), detectSystemLocale: jest.fn().mockImplementation(() => { return TEST_LOCALES[Intl.DateTimeFormat().resolvedOptions().locale] || TEST_LOCALES['en-US']; }), isRTL: jest.fn().mockImplementation(() => { return currentConfig.rtl || false; }) } as jest.Mocked<MockI18nService>; } } /** * Test assertion helpers for internationalization */ export class I18nTestAssertions { /** * Assert that a date string is valid in the given timezone */ static assertValidDateInTimezone(dateString: string, timezone: string): void { const date = new Date(dateString); expect(date).toBeInstanceOf(Date); expect(date.getTime()).not.toBeNaN(); expect(LocaleTestUtils.isValidInTimezone(date, timezone)).toBe(true); } /** * Assert that formatted date matches locale expectations */ static assertDateFormatMatchesLocale( formattedDate: string, config: I18nTestConfig, originalDate?: Date | string ): void { expect(formattedDate).toBeTruthy(); expect(typeof formattedDate).toBe('string'); // Basic format validation based on common patterns const formatPatterns: Record<string, RegExp> = { 'MM/dd/yyyy': /^\d{2}\/\d{2}\/\d{4}$/, 'dd/MM/yyyy': /^\d{2}\/\d{2}\/\d{4}$/, 'yyyy-MM-dd': /^\d{4}-\d{2}-\d{2}$/, 'dd.MM.yyyy': /^\d{2}\.\d{2}\.\d{4}$/, 'yyyy/MM/dd': /^\d{4}\/\d{2}\/\d{2}$/ }; if (config.dateFormat && formatPatterns[config.dateFormat]) { expect(formattedDate).toMatch(formatPatterns[config.dateFormat]); } } /** * Assert that time format matches locale expectations */ static assertTimeFormatMatchesLocale( formattedTime: string, config: I18nTestConfig, expectedFormat: '12h' | '24h' = '24h' ): void { expect(formattedTime).toBeTruthy(); expect(typeof formattedTime).toBe('string'); if (expectedFormat === '24h') { // Expect 24-hour format (00:00:00 to 23:59:59) expect(formattedTime).toMatch(/^([01]?\d|2[0-3]):[0-5]\d:[0-5]\d$/); } else { // Expect 12-hour format with AM/PM expect(formattedTime).toMatch(/^(1[0-2]|0?[1-9]):[0-5]\d:[0-5]\d\s?(AM|PM)$/i); } } /** * Assert currency format matches locale expectations */ static assertCurrencyFormatMatchesLocale( formattedCurrency: string, config: I18nTestConfig, amount: number ): void { expect(formattedCurrency).toBeTruthy(); expect(typeof formattedCurrency).toBe('string'); // Should contain currency code or symbol if (config.currency) { const currencySymbols: Record<string, string> = { 'USD': ['$', 'US$'], 'EUR': ['€', 'EUR'], 'GBP': ['£', 'GBP'], 'JPY': ['¥', 'JPY'], 'KRW': ['₩', 'KRW'], 'CNY': ['¥', 'CNY'], 'SAR': ['SR', 'SAR'], 'ILS': ['₪', 'ILS'] }; const symbols = currencySymbols[config.currency]; if (symbols) { const hasSymbol = symbols.some(symbol => formattedCurrency.includes(symbol)); expect(hasSymbol).toBe(true); } } // Should contain the amount in some form expect(formattedCurrency).toContain(Math.abs(amount).toString().charAt(0)); } } /** * Default export with all utilities */ export const I18nTestUtils = { TEST_LOCALES, TEST_TIMEZONES, I18nTestEnvironment, LocaleTestUtils, LocaleTestCaseGenerator, MockI18nServiceFactory, I18nTestAssertions }; export default I18nTestUtils;

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Ghostseller/CastPlan_mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server