Refactor i18n setup and improve structure for maintainability

- Relocated `i18n` configuration files to `src/lib/i18n` for better organization.
- Removed obsolete `request.ts` and `routing.ts` files, simplifying `i18n` setup within the project.
- Added extensive tests for `i18n/utils` to validate locale-related utilities, including locale name, native name, and flag retrieval.
- Introduced a detailed `I18N_IMPLEMENTATION_PLAN.md` to document implementation phases, decisions, and recommendations for future extensions.
- Enhanced TypeScript definitions and modularity across i18n utilities for improved developer experience.
This commit is contained in:
Felipe Cardoso
2025-11-18 07:23:54 +01:00
parent fe6a98c379
commit 55ae92c460
8 changed files with 776 additions and 29 deletions

View File

@@ -0,0 +1,242 @@
/**
* Tests for i18n utility functions
*/
import {
getLocaleName,
getLocaleNativeName,
getLocaleFlag,
formatRelativeTime,
} from '@/lib/i18n/utils';
describe('i18n Utility Functions', () => {
describe('getLocaleName', () => {
it('should return correct name for English locale', () => {
expect(getLocaleName('en')).toBe('English');
});
it('should return correct name for Italian locale', () => {
expect(getLocaleName('it')).toBe('Italiano');
});
it('should return English for unsupported locale', () => {
expect(getLocaleName('fr')).toBe('English');
expect(getLocaleName('de')).toBe('English');
expect(getLocaleName('es')).toBe('English');
});
it('should handle empty string', () => {
expect(getLocaleName('')).toBe('English');
});
it('should handle undefined as string', () => {
expect(getLocaleName('undefined')).toBe('English');
});
it('should handle locale codes with region (fallback)', () => {
expect(getLocaleName('en-US')).toBe('English');
expect(getLocaleName('en-GB')).toBe('English');
expect(getLocaleName('it-IT')).toBe('English'); // Not exact match, falls back
});
});
describe('getLocaleNativeName', () => {
it('should return native name for English locale', () => {
expect(getLocaleNativeName('en')).toBe('English');
});
it('should return native name for Italian locale', () => {
expect(getLocaleNativeName('it')).toBe('Italiano');
});
it('should return English for unsupported locale', () => {
expect(getLocaleNativeName('fr')).toBe('English');
expect(getLocaleNativeName('de')).toBe('English');
});
it('should match getLocaleName output for supported locales', () => {
expect(getLocaleNativeName('en')).toBe(getLocaleName('en'));
expect(getLocaleNativeName('it')).toBe(getLocaleName('it'));
});
it('should handle case variations (fallback behavior)', () => {
expect(getLocaleNativeName('EN')).toBe('English');
expect(getLocaleNativeName('IT')).toBe('English');
});
});
describe('getLocaleFlag', () => {
it('should return US flag for English locale', () => {
expect(getLocaleFlag('en')).toBe('🇺🇸');
});
it('should return Italian flag for Italian locale', () => {
expect(getLocaleFlag('it')).toBe('🇮🇹');
});
it('should return US flag for unsupported locale', () => {
expect(getLocaleFlag('fr')).toBe('🇺🇸');
expect(getLocaleFlag('de')).toBe('🇺🇸');
expect(getLocaleFlag('es')).toBe('🇺🇸');
});
it('should return valid emoji flags', () => {
const enFlag = getLocaleFlag('en');
const itFlag = getLocaleFlag('it');
// Check that these are unicode emoji characters
expect(enFlag).toMatch(/[\u{1F1E6}-\u{1F1FF}]{2}/u);
expect(itFlag).toMatch(/[\u{1F1E6}-\u{1F1FF}]{2}/u);
});
it('should handle empty string gracefully', () => {
expect(getLocaleFlag('')).toBe('🇺🇸');
});
});
describe('formatRelativeTime', () => {
const now = new Date();
describe('English locale', () => {
it('should format "just now" for times less than 60 seconds', () => {
const date = new Date(now.getTime() - 30 * 1000); // 30 seconds ago
expect(formatRelativeTime(date, 'en')).toBe('just now');
});
it('should format minutes correctly', () => {
const date1 = new Date(now.getTime() - 1 * 60 * 1000); // 1 minute ago
expect(formatRelativeTime(date1, 'en')).toBe('1 minute ago');
const date2 = new Date(now.getTime() - 5 * 60 * 1000); // 5 minutes ago
expect(formatRelativeTime(date2, 'en')).toBe('5 minutes ago');
const date3 = new Date(now.getTime() - 30 * 60 * 1000); // 30 minutes ago
expect(formatRelativeTime(date3, 'en')).toBe('30 minutes ago');
});
it('should format hours correctly', () => {
const date1 = new Date(now.getTime() - 1 * 60 * 60 * 1000); // 1 hour ago
expect(formatRelativeTime(date1, 'en')).toBe('1 hour ago');
const date2 = new Date(now.getTime() - 5 * 60 * 60 * 1000); // 5 hours ago
expect(formatRelativeTime(date2, 'en')).toBe('5 hours ago');
const date3 = new Date(now.getTime() - 23 * 60 * 60 * 1000); // 23 hours ago
expect(formatRelativeTime(date3, 'en')).toBe('23 hours ago');
});
it('should format days correctly', () => {
const date1 = new Date(now.getTime() - 1 * 24 * 60 * 60 * 1000); // 1 day ago
expect(formatRelativeTime(date1, 'en')).toBe('1 day ago');
const date2 = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); // 7 days ago
expect(formatRelativeTime(date2, 'en')).toBe('7 days ago');
const date3 = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); // 30 days ago
expect(formatRelativeTime(date3, 'en')).toBe('30 days ago');
});
it('should default to English when locale not specified', () => {
const date = new Date(now.getTime() - 2 * 60 * 1000); // 2 minutes ago
expect(formatRelativeTime(date)).toBe('2 minutes ago');
});
});
describe('Italian locale', () => {
it('should format "proprio ora" for times less than 60 seconds', () => {
const date = new Date(now.getTime() - 45 * 1000); // 45 seconds ago
expect(formatRelativeTime(date, 'it')).toBe('proprio ora');
});
it('should format minutes correctly with Italian grammar', () => {
const date1 = new Date(now.getTime() - 1 * 60 * 1000); // 1 minuto ago
expect(formatRelativeTime(date1, 'it')).toBe('1 minuto fa');
const date2 = new Date(now.getTime() - 5 * 60 * 1000); // 5 minuti ago
expect(formatRelativeTime(date2, 'it')).toBe('5 minuti fa');
});
it('should format hours correctly with Italian grammar', () => {
const date1 = new Date(now.getTime() - 1 * 60 * 60 * 1000); // 1 ora ago
expect(formatRelativeTime(date1, 'it')).toBe('1 ora fa');
const date2 = new Date(now.getTime() - 5 * 60 * 60 * 1000); // 5 ore ago
expect(formatRelativeTime(date2, 'it')).toBe('5 ore fa');
});
it('should format days correctly with Italian grammar', () => {
const date1 = new Date(now.getTime() - 1 * 24 * 60 * 60 * 1000); // 1 giorno ago
expect(formatRelativeTime(date1, 'it')).toBe('1 giorno fa');
const date2 = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); // 7 giorni ago
expect(formatRelativeTime(date2, 'it')).toBe('7 giorni fa');
});
});
describe('Edge cases', () => {
it('should handle dates exactly at boundaries', () => {
// Exactly 60 seconds
const date1 = new Date(now.getTime() - 60 * 1000);
const result1 = formatRelativeTime(date1, 'en');
expect(result1).toBe('1 minute ago');
// Exactly 1 hour
const date2 = new Date(now.getTime() - 60 * 60 * 1000);
const result2 = formatRelativeTime(date2, 'en');
expect(result2).toBe('1 hour ago');
// Exactly 24 hours
const date3 = new Date(now.getTime() - 24 * 60 * 60 * 1000);
const result3 = formatRelativeTime(date3, 'en');
expect(result3).toBe('1 day ago');
});
it('should handle future dates (negative time)', () => {
// Date in the future - implementation treats it as "just now" or "0 units ago"
const futureDate = new Date(now.getTime() + 60 * 1000);
const result = formatRelativeTime(futureDate, 'en');
// Depending on implementation, might show negative or just now
expect(result).toBeDefined();
});
it('should handle very old dates', () => {
const oldDate = new Date(now.getTime() - 365 * 24 * 60 * 60 * 1000); // 1 year ago
const result = formatRelativeTime(oldDate, 'en');
expect(result).toBe('365 days ago');
});
});
describe('Unsupported locale fallback', () => {
it('should fallback to English for unsupported locales', () => {
const date = new Date(now.getTime() - 2 * 60 * 1000); // 2 minutes ago
expect(formatRelativeTime(date, 'fr')).toBe('2 minutes ago');
expect(formatRelativeTime(date, 'de')).toBe('2 minutes ago');
expect(formatRelativeTime(date, 'es')).toBe('2 minutes ago');
});
});
});
describe('Locale code consistency', () => {
it('should handle the same locale codes across all functions', () => {
const locales = ['en', 'it'];
locales.forEach((locale) => {
// All functions should return non-empty strings
expect(getLocaleName(locale)).toBeTruthy();
expect(getLocaleNativeName(locale)).toBeTruthy();
expect(getLocaleFlag(locale)).toBeTruthy();
});
});
it('should have consistent fallback behavior', () => {
const unsupportedLocales = ['fr', 'de', 'es', 'invalid', ''];
unsupportedLocales.forEach((locale) => {
// All should fall back to English
expect(getLocaleName(locale)).toBe('English');
expect(getLocaleNativeName(locale)).toBe('English');
expect(getLocaleFlag(locale)).toBe('🇺🇸');
});
});
});
});