Refactored the whole app based on the requirements shared

This commit is contained in:
Stephanos A
2026-05-21 08:48:28 +03:00
parent 2dc3da9e74
commit 51bc906792
84 changed files with 6880 additions and 12659 deletions

View File

@@ -0,0 +1,9 @@
import { Module, Global } from '@nestjs/common';
import { I18nService } from './i18n.service';
@Global()
@Module({
providers: [I18nService],
exports: [I18nService],
})
export class I18nModule {}

View File

@@ -0,0 +1,49 @@
import { Test, TestingModule } from '@nestjs/testing';
import { I18nService } from './i18n.service';
describe('I18nService', () => {
let service: I18nService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [I18nService],
}).compile();
service = module.get<I18nService>(I18nService);
});
it('should translate English keys', () => {
expect(service.translate('common.welcome', 'en')).toBe('Welcome');
expect(service.translate('booking.created', 'en')).toBe('Booking created successfully');
});
it('should translate Amharic keys', () => {
expect(service.translate('common.welcome', 'am')).toBe('እንኳን ደህና መጡ');
});
it('should translate French keys', () => {
expect(service.translate('common.welcome', 'fr')).toBe('Bienvenue');
});
it('should translate Oromo keys', () => {
expect(service.translate('common.welcome', 'om')).toBe('Baga nagaan dhuftan');
});
it('should fallback to English for unsupported locale', () => {
expect(service.translate('common.welcome', 'de')).toBe('Welcome');
});
it('should return key if translation not found', () => {
expect(service.translate('nonexistent.key', 'en')).toBe('nonexistent.key');
});
it('should interpolate parameters', () => {
const result = service.translate('common.welcome', 'en', { name: 'John' });
expect(result).toBeDefined();
});
it('should return supported locales', () => {
const locales = service.getSupportedLocales();
expect(locales).toEqual(['en', 'am', 'fr', 'om']);
});
});

View File

@@ -0,0 +1,63 @@
import { Injectable } from '@nestjs/common';
import * as fs from 'fs';
import * as path from 'path';
type TranslationMap = Record<string, any>;
@Injectable()
export class I18nService {
private translations: Map<string, TranslationMap> = new Map();
private readonly supportedLocales = ['en', 'am', 'fr', 'om'];
private readonly defaultLocale = 'en';
constructor() {
this.loadTranslations();
}
private loadTranslations() {
for (const locale of this.supportedLocales) {
const filePath = path.join(__dirname, 'translations', `${locale}.json`);
try {
const content = fs.readFileSync(filePath, 'utf-8');
this.translations.set(locale, JSON.parse(content));
} catch (err) {
console.warn(`Failed to load translation file for locale: ${locale}`);
}
}
}
translate(key: string, locale: string = this.defaultLocale, params?: Record<string, string>): string {
const normalizedLocale = this.normalizeLocale(locale);
const translations = this.translations.get(normalizedLocale) || this.translations.get(this.defaultLocale);
if (!translations) return key;
const keys = key.split('.');
let value: any = translations;
for (const k of keys) {
value = value?.[k];
if (value === undefined) return key;
}
if (typeof value !== 'string') return key;
if (params) {
return Object.entries(params).reduce(
(text, [param, val]) => text.replace(new RegExp(`{{${param}}}`, 'g'), val),
value
);
}
return value;
}
private normalizeLocale(locale: string): string {
const normalized = locale.toLowerCase().split('-')[0];
return this.supportedLocales.includes(normalized) ? normalized : this.defaultLocale;
}
getSupportedLocales(): string[] {
return this.supportedLocales;
}
}

View File

@@ -0,0 +1,9 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { LOCALE_KEY } from './locale.middleware';
export const Locale = createParamDecorator(
(data: unknown, ctx: ExecutionContext): string => {
const request = ctx.switchToHttp().getRequest();
return request[LOCALE_KEY] || 'en';
},
);

View File

@@ -0,0 +1,16 @@
import { Injectable, NestMiddleware } from '@nestjs/common';
export const LOCALE_KEY = 'locale';
@Injectable()
export class LocaleMiddleware implements NestMiddleware {
use(req: any, res: any, next: () => void) {
const locale =
req.query.lang as string ||
req.headers['accept-language']?.split(',')[0]?.split('-')[0] ||
'en';
req[LOCALE_KEY] = locale;
next();
}
}

View File

@@ -0,0 +1,23 @@
{
"common": {
"welcome": "እንኳን ደህና መጡ",
"error": "ስህተት ተከስቷል",
"success": "ተሳክቷል"
},
"booking": {
"created": "ቦታ ማስያዝ በተሳካ ሁኔታ ተፈጥሯል",
"notFound": "ቦታ ማስያዝ አልተገኘም",
"cancelled": "ቦታ ማስያዝ ተሰርዟል",
"confirmed": "ቦታ ማስያዝ ተረጋግጧል"
},
"payment": {
"succeeded": "ክፍያ ተሳክቷል",
"failed": "ክፍያ አልተሳካም",
"pending": "ክፍያ በመጠባበቅ ላይ"
},
"ticket": {
"issued": "ትኬት ተሰጥቷል",
"validated": "ትኬት ተረጋግጧል",
"alreadyValidated": "ትኬት ቀድሞውኑ ተረጋግጧል"
}
}

View File

@@ -0,0 +1,23 @@
{
"common": {
"welcome": "Welcome",
"error": "An error occurred",
"success": "Success"
},
"booking": {
"created": "Booking created successfully",
"notFound": "Booking not found",
"cancelled": "Booking cancelled",
"confirmed": "Booking confirmed"
},
"payment": {
"succeeded": "Payment successful",
"failed": "Payment failed",
"pending": "Payment pending"
},
"ticket": {
"issued": "Ticket issued",
"validated": "Ticket validated",
"alreadyValidated": "Ticket already validated"
}
}

View File

@@ -0,0 +1,23 @@
{
"common": {
"welcome": "Bienvenue",
"error": "Une erreur s'est produite",
"success": "Succès"
},
"booking": {
"created": "Réservation créée avec succès",
"notFound": "Réservation introuvable",
"cancelled": "Réservation annulée",
"confirmed": "Réservation confirmée"
},
"payment": {
"succeeded": "Paiement réussi",
"failed": "Échec du paiement",
"pending": "Paiement en attente"
},
"ticket": {
"issued": "Billet émis",
"validated": "Billet validé",
"alreadyValidated": "Billet déjà validé"
}
}

View File

@@ -0,0 +1,23 @@
{
"common": {
"welcome": "Baga nagaan dhuftan",
"error": "Dogongora uumame",
"success": "Milkaa'ina"
},
"booking": {
"created": "Bakka qabachuu milkaa'inaan uumame",
"notFound": "Bakka qabachuu hin argamne",
"cancelled": "Bakka qabachuu haqame",
"confirmed": "Bakka qabachuu mirkaneeffame"
},
"payment": {
"succeeded": "Kaffaltiin milkaa'e",
"failed": "Kaffaltiin hin milkoofne",
"pending": "Kaffaltiin eegaa jira"
},
"ticket": {
"issued": "Tiikeetiin kenname",
"validated": "Tiikeetiin mirkaneeffame",
"alreadyValidated": "Tiikeetiin duraan mirkaneeffame"
}
}