From 7b4c95678e202176e0bc5085db7b6d31a25f662d Mon Sep 17 00:00:00 2001 From: Gabriel De Los Rios Date: Sun, 21 Dec 2025 20:06:18 -0300 Subject: [PATCH] feat; add multi language support --- .../components/contact-form/contact-form.html | 12 +-- .../components/contact-form/contact-form.ts | 4 +- .../contact-list-table.html | 8 +- .../contact-list-table/contact-list-table.ts | 4 +- .../components/contact-list/contact-list.html | 4 +- .../components/contact-list/contact-list.ts | 4 +- .../contact-search-bar.html | 2 +- .../contact-search-bar/contact-search-bar.ts | 9 ++- .../components/notification/notification.html | 2 +- .../components/notification/notification.ts | 3 +- .../name-and-company-field.ts | 8 +- src/app/errors-dictionaries/phone-field.ts | 8 +- src/app/pages/edit/contact-resolver.spec.ts | 3 + src/app/pages/edit/edit.html | 8 +- src/app/pages/edit/edit.ts | 4 +- src/app/pages/main/main.html | 8 +- src/app/pages/main/main.ts | 4 +- src/app/services/contact.service.spec.ts | 8 +- src/app/services/contact.service.ts | 16 ++-- src/app/services/language-manager.spec.ts | 27 +++++++ src/app/services/language-manager.ts | 16 ++++ src/app/strings.ts | 75 +++++++++++++------ src/app/types/Language.type.ts | 3 + 23 files changed, 163 insertions(+), 77 deletions(-) create mode 100644 src/app/services/language-manager.spec.ts create mode 100644 src/app/services/language-manager.ts create mode 100644 src/app/types/Language.type.ts diff --git a/src/app/components/contact-form/contact-form.html b/src/app/components/contact-form/contact-form.html index 341d887..42c2ada 100644 --- a/src/app/components/contact-form/contact-form.html +++ b/src/app/components/contact-form/contact-form.html @@ -5,20 +5,20 @@ diff --git a/src/app/components/contact-form/contact-form.ts b/src/app/components/contact-form/contact-form.ts index c66fbbf..2db682a 100644 --- a/src/app/components/contact-form/contact-form.ts +++ b/src/app/components/contact-form/contact-form.ts @@ -1,7 +1,6 @@ import { Component, inject, input, output } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; import { FormField } from '../form-field/form-field'; -import { STRINGS_INJECTOR } from '../../app.config'; import { SquaredBtn } from '../squared-btn/squared-btn'; import { NameAndCompanyFieldsErrorsDictionary } from '../../errors-dictionaries/name-and-company-field'; import { PhoneFieldErroresDictionary } from '../../errors-dictionaries/phone-field'; @@ -9,6 +8,7 @@ import { UpperfirstPipe } from '../../pipes/upperfirst-pipe'; import { ContactDTO } from '../../models/ContactDTO'; import { ContactFormValue } from '../../types/ContactFormValue.type'; import { FormGroupContact } from '../../utils/form-group-contact'; +import { LanguageManager } from '../../services/language-manager'; @Component({ selector: 'app-contact-form', @@ -20,7 +20,7 @@ export class ContactForm { contact = output(); form = input(new FormGroupContact()); submitText = input(''); - protected strings = inject(STRINGS_INJECTOR); + protected languageManager = inject(LanguageManager); protected companyAndNameErrorsDictionary = new NameAndCompanyFieldsErrorsDictionary().getDictionary(); protected phoneErrorsDictionary = new PhoneFieldErroresDictionary().getDictionary(); diff --git a/src/app/components/contact-list-table/contact-list-table.html b/src/app/components/contact-list-table/contact-list-table.html index e3c452c..1e36b9f 100644 --- a/src/app/components/contact-list-table/contact-list-table.html +++ b/src/app/components/contact-list-table/contact-list-table.html @@ -2,10 +2,10 @@ - - - - + + + + diff --git a/src/app/components/contact-list-table/contact-list-table.ts b/src/app/components/contact-list-table/contact-list-table.ts index faee43c..d912db0 100644 --- a/src/app/components/contact-list-table/contact-list-table.ts +++ b/src/app/components/contact-list-table/contact-list-table.ts @@ -1,9 +1,9 @@ import { Component, inject, input } from '@angular/core'; -import { STRINGS_INJECTOR } from '../../app.config'; import { ContactDTO } from '../../models/ContactDTO'; import { ContactActionsBar } from '../contact-actions-bar/contact-actions-bar'; import { ContactService } from '../../services/contact.service'; import { Router } from '@angular/router'; +import { LanguageManager } from '../../services/language-manager'; @Component({ selector: 'app-contact-list-table', @@ -13,7 +13,7 @@ import { Router } from '@angular/router'; }) export class ContactListTable { contactList = input([]); - protected readonly strings = inject(STRINGS_INJECTOR); + protected readonly languageManager = inject(LanguageManager); private readonly contactService = inject(ContactService); private readonly router = inject(Router); diff --git a/src/app/components/contact-list/contact-list.html b/src/app/components/contact-list/contact-list.html index f342901..21196b2 100644 --- a/src/app/components/contact-list/contact-list.html +++ b/src/app/components/contact-list/contact-list.html @@ -1,10 +1,10 @@
-

{{strings.contacts|upperfirst}}

+

{{languageManager.strings.contacts|upperfirst}}

diff --git a/src/app/components/contact-list/contact-list.ts b/src/app/components/contact-list/contact-list.ts index b71cf50..092dfec 100644 --- a/src/app/components/contact-list/contact-list.ts +++ b/src/app/components/contact-list/contact-list.ts @@ -1,5 +1,4 @@ import { Component, inject, OnInit } from '@angular/core'; -import { STRINGS_INJECTOR } from '../../app.config'; import { UpperfirstPipe } from '../../pipes/upperfirst-pipe'; import { ContactSearchBar } from '../contact-search-bar/contact-search-bar'; import { ContactListTable } from '../contact-list-table/contact-list-table'; @@ -7,6 +6,7 @@ import { ContactService } from '../../services/contact.service'; import { AsyncPipe } from '@angular/common'; import { Counter } from '../counter/counter'; import { ContactsFilterPipe } from '../../pipes/contacts-filter-pipe'; +import { LanguageManager } from '../../services/language-manager'; @Component({ selector: 'app-contact-list', @@ -23,7 +23,7 @@ import { ContactsFilterPipe } from '../../pipes/contacts-filter-pipe'; }) export class ContactList implements OnInit { private readonly contactService = inject(ContactService); - protected readonly strings = inject(STRINGS_INJECTOR); + protected readonly languageManager = inject(LanguageManager); protected contacts$ = this.contactService.contacts$; protected filter = ''; diff --git a/src/app/components/contact-search-bar/contact-search-bar.html b/src/app/components/contact-search-bar/contact-search-bar.html index 83b3e49..adf7c94 100644 --- a/src/app/components/contact-search-bar/contact-search-bar.html +++ b/src/app/components/contact-search-bar/contact-search-bar.html @@ -1,5 +1,5 @@ \ No newline at end of file diff --git a/src/app/components/contact-search-bar/contact-search-bar.ts b/src/app/components/contact-search-bar/contact-search-bar.ts index 5645058..83b4c5d 100644 --- a/src/app/components/contact-search-bar/contact-search-bar.ts +++ b/src/app/components/contact-search-bar/contact-search-bar.ts @@ -1,13 +1,14 @@ import { Component, inject, output } from '@angular/core'; -import { STRINGS_INJECTOR } from '../../app.config'; +import { LanguageManager } from '../../services/language-manager'; +import { UpperfirstPipe } from '../../pipes/upperfirst-pipe'; @Component({ selector: 'app-contact-search-bar', - imports: [], + imports: [UpperfirstPipe], templateUrl: './contact-search-bar.html', styleUrl: './contact-search-bar.scss', }) export class ContactSearchBar { - contactSearch = output() - protected readonly strings = inject(STRINGS_INJECTOR); + contactSearch = output(); + protected readonly languageManager = inject(LanguageManager); } diff --git a/src/app/components/notification/notification.html b/src/app/components/notification/notification.html index 93256e0..4eb5710 100644 --- a/src/app/components/notification/notification.html +++ b/src/app/components/notification/notification.html @@ -1,6 +1,6 @@ @if(notification$(); as notification) {
-

{{ notification.message }}

+

{{ notification.message|upperfirst }}

} diff --git a/src/app/components/notification/notification.ts b/src/app/components/notification/notification.ts index 488c161..f0814ad 100644 --- a/src/app/components/notification/notification.ts +++ b/src/app/components/notification/notification.ts @@ -1,9 +1,10 @@ import { Component, inject } from '@angular/core'; import { Notifier } from '../../services/notifier'; +import { UpperfirstPipe } from '../../pipes/upperfirst-pipe'; @Component({ selector: 'app-notification', - imports: [], + imports: [UpperfirstPipe], templateUrl: './notification.html', styleUrl: './notification.scss', }) diff --git a/src/app/errors-dictionaries/name-and-company-field.ts b/src/app/errors-dictionaries/name-and-company-field.ts index 3087467..ce4da5c 100644 --- a/src/app/errors-dictionaries/name-and-company-field.ts +++ b/src/app/errors-dictionaries/name-and-company-field.ts @@ -1,14 +1,14 @@ import { inject } from '@angular/core'; import { Dictionary } from '../interfaces/dictionary.interface'; -import { STRINGS_INJECTOR } from '../app.config'; +import { LanguageManager } from '../services/language-manager'; export class NameAndCompanyFieldsErrorsDictionary implements Dictionary { - private readonly strings = inject(STRINGS_INJECTOR); + private readonly languageManager = inject(LanguageManager); private readonly maxlen: string; - private readonly required = this.strings.errorMessageRequired; + private readonly required = this.languageManager.strings.errorMessageRequired; constructor() { - this.maxlen = this.strings.errorMessageMaxLength(120); + this.maxlen = this.languageManager.strings.errorMessageMaxLength(120); } getDictionary(): { [key: string]: string } { diff --git a/src/app/errors-dictionaries/phone-field.ts b/src/app/errors-dictionaries/phone-field.ts index 0bddeea..b283f22 100644 --- a/src/app/errors-dictionaries/phone-field.ts +++ b/src/app/errors-dictionaries/phone-field.ts @@ -1,11 +1,11 @@ import { inject } from '@angular/core'; import { Dictionary } from '../interfaces/dictionary.interface'; -import { STRINGS_INJECTOR } from '../app.config'; +import { LanguageManager } from '../services/language-manager'; export class PhoneFieldErroresDictionary implements Dictionary { - strings = inject(STRINGS_INJECTOR); - pattern = this.strings.errorMessagePhonePattern; - required = this.strings.errorMessageRequired; + languageManager = inject(LanguageManager); + pattern = this.languageManager.strings.errorMessagePhonePattern; + required = this.languageManager.strings.errorMessageRequired; getDictionary(): { [key: string]: string } { const { required, pattern } = this; diff --git a/src/app/pages/edit/contact-resolver.spec.ts b/src/app/pages/edit/contact-resolver.spec.ts index 8dc84a8..f3ab2cf 100644 --- a/src/app/pages/edit/contact-resolver.spec.ts +++ b/src/app/pages/edit/contact-resolver.spec.ts @@ -6,6 +6,8 @@ import { ContactDTO } from '../../models/ContactDTO'; import { provideHttpClient } from '@angular/common/http'; import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { environment } from '../../../environments/environment'; +import { STRINGS_INJECTOR } from '../../app.config'; +import { strings } from '../../strings'; describe('contactResolver', () => { let httpTestingController: HttpTestingController; @@ -31,6 +33,7 @@ describe('contactResolver', () => { provideHttpClient(), provideHttpClientTesting(), { provide: Router, useValue: router }, + { provide: STRINGS_INJECTOR, useValue: strings }, ], }); httpTestingController = TestBed.inject(HttpTestingController); diff --git a/src/app/pages/edit/edit.html b/src/app/pages/edit/edit.html index e65a556..2dccab3 100644 --- a/src/app/pages/edit/edit.html +++ b/src/app/pages/edit/edit.html @@ -1,11 +1,11 @@ @@ -14,9 +14,9 @@ class="form" (contact)="edit(form.value)" [form]="form" - [submitText]="strings.save" + [submitText]="languageManager.strings.save" > diff --git a/src/app/pages/edit/edit.ts b/src/app/pages/edit/edit.ts index 2cdaebb..223e7d7 100644 --- a/src/app/pages/edit/edit.ts +++ b/src/app/pages/edit/edit.ts @@ -1,6 +1,5 @@ import { Component, inject } from '@angular/core'; import { MainHeader } from '../../components/main-header/main-header'; -import { STRINGS_INJECTOR } from '../../app.config'; import { RoundedBtn } from '../../components/rounded-btn/rounded-btn'; import { ActivatedRoute, Router } from '@angular/router'; import { Card } from '../../components/card/card'; @@ -13,6 +12,7 @@ import { FormGroupContact } from '../../utils/form-group-contact'; import { AsyncPipe } from '@angular/common'; import { ContactService } from '../../services/contact.service'; import { ContactFormValue } from '../../types/ContactFormValue.type'; +import { LanguageManager } from '../../services/language-manager'; @Component({ selector: 'app-edit', @@ -21,7 +21,7 @@ import { ContactFormValue } from '../../types/ContactFormValue.type'; styleUrl: './edit.scss', }) export class Edit { - protected readonly strings = inject(STRINGS_INJECTOR); + protected readonly languageManager = inject(LanguageManager); private readonly activatedRoute = inject(ActivatedRoute); private readonly contactService = inject(ContactService); private readonly router = inject(Router); diff --git a/src/app/pages/main/main.html b/src/app/pages/main/main.html index 2cde2ad..9f6dc41 100644 --- a/src/app/pages/main/main.html +++ b/src/app/pages/main/main.html @@ -1,13 +1,13 @@ - + diff --git a/src/app/pages/main/main.ts b/src/app/pages/main/main.ts index 4d088f0..e1623e5 100644 --- a/src/app/pages/main/main.ts +++ b/src/app/pages/main/main.ts @@ -5,10 +5,10 @@ import { ContactList } from '../../components/contact-list/contact-list'; import { ContactService } from '../../services/contact.service'; import { ContactDTO } from '../../models/ContactDTO'; import { FormHeader } from '../../components/form-header/form-header'; -import { STRINGS_INJECTOR } from '../../app.config'; import { UpperfirstPipe } from '../../pipes/upperfirst-pipe'; import { MainHeader } from '../../components/main-header/main-header'; import { FormGroupContact } from '../../utils/form-group-contact'; +import { LanguageManager } from '../../services/language-manager'; @Component({ selector: 'app-main', @@ -18,7 +18,7 @@ import { FormGroupContact } from '../../utils/form-group-contact'; }) export class Main { protected readonly form = new FormGroupContact(); - protected readonly strings = inject(STRINGS_INJECTOR); + protected readonly languageManager = inject(LanguageManager); private readonly contactService = inject(ContactService); save(contactDTO: ContactDTO) { diff --git a/src/app/services/contact.service.spec.ts b/src/app/services/contact.service.spec.ts index 40437aa..8b9ba7a 100644 --- a/src/app/services/contact.service.spec.ts +++ b/src/app/services/contact.service.spec.ts @@ -5,6 +5,8 @@ import { provideHttpClient } from '@angular/common/http'; import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { environment } from '../../environments/environment'; import { ContactDTO } from '../models/ContactDTO'; +import { STRINGS_INJECTOR } from '../app.config'; +import { strings } from '../strings'; describe('ContactService', () => { let service: ContactService; @@ -16,7 +18,11 @@ describe('ContactService', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [], - providers: [provideHttpClient(), provideHttpClientTesting()], + providers: [ + provideHttpClient(), + provideHttpClientTesting(), + { provide: STRINGS_INJECTOR, useValue: strings }, + ], }); service = TestBed.inject(ContactService); httpTestingController = TestBed.inject(HttpTestingController); diff --git a/src/app/services/contact.service.ts b/src/app/services/contact.service.ts index 6b2143d..363747f 100644 --- a/src/app/services/contact.service.ts +++ b/src/app/services/contact.service.ts @@ -5,7 +5,7 @@ import { ContactDTO } from '../models/ContactDTO'; import { Response } from '../models/Response'; import { BehaviorSubject, map, switchMap, tap } from 'rxjs'; import { ResponseStateNotification } from './ResponseStateNotificatio'; -import { strings } from '../strings'; +import { LanguageManager } from './language-manager'; @Injectable({ providedIn: 'root', @@ -14,7 +14,7 @@ export class ContactService { private readonly httpClient = inject(HttpClient); private readonly responseStateNotification = inject(ResponseStateNotification); private readonly contacts = new BehaviorSubject([]); - + private readonly languageManager = inject(LanguageManager); readonly contacts$ = this.contacts.asObservable(); delete(id: number) { @@ -22,8 +22,8 @@ export class ContactService { (r) => this.responseStateNotification.handleResponse( r, - strings.deleteContactSuccessNotification, - strings.errorNotification + this.languageManager.strings.deleteContactSuccessNotification, + this.languageManager.strings.errorNotification ), switchMap(() => this.getAll()) ); @@ -47,8 +47,8 @@ export class ContactService { (r) => this.responseStateNotification.handleResponse( r, - strings.createContactSuccessNotification, - strings.errorNotification + this.languageManager.strings.createContactSuccessNotification, + this.languageManager.strings.errorNotification ), switchMap(() => this.getAll()) @@ -61,8 +61,8 @@ export class ContactService { .pipe((r) => this.responseStateNotification.handleResponse( r, - strings.editContactSuccessNotification, - strings.errorNotification + this.languageManager.strings.editContactSuccessNotification, + this.languageManager.strings.errorNotification ) ); } diff --git a/src/app/services/language-manager.spec.ts b/src/app/services/language-manager.spec.ts new file mode 100644 index 0000000..13e6d8a --- /dev/null +++ b/src/app/services/language-manager.spec.ts @@ -0,0 +1,27 @@ +import { TestBed } from '@angular/core/testing'; + +import { LanguageManager } from './language-manager'; +import { strings } from '../strings'; +import { STRINGS_INJECTOR } from '../app.config'; +import { Language } from '../types/Language.type'; + +describe('LanguageManager', () => { + let service: LanguageManager; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [{ provide: STRINGS_INJECTOR, useValue: strings }], + }); + service = TestBed.inject(LanguageManager); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should switch language', () => { + const SELECTED_LANGUAGE_MOCK: Language = 'es'; + service.setLanguage(SELECTED_LANGUAGE_MOCK); + expect(service.strings).toEqual(strings[SELECTED_LANGUAGE_MOCK]); + }); +}); diff --git a/src/app/services/language-manager.ts b/src/app/services/language-manager.ts new file mode 100644 index 0000000..42ad8c5 --- /dev/null +++ b/src/app/services/language-manager.ts @@ -0,0 +1,16 @@ +import { inject, Injectable } from '@angular/core'; +import { STRINGS_INJECTOR } from '../app.config'; +import { Language } from '../types/Language.type'; + +@Injectable({ + providedIn: 'root', +}) +export class LanguageManager { + private readonly stringsDictionary = inject(STRINGS_INJECTOR); + + strings = this.stringsDictionary.en; + + setLanguage(language: Language) { + this.strings = this.stringsDictionary[language]; + } +} diff --git a/src/app/strings.ts b/src/app/strings.ts index 83470ca..a6ca68a 100644 --- a/src/app/strings.ts +++ b/src/app/strings.ts @@ -1,25 +1,54 @@ export const strings = Object.freeze({ - actions: 'actions', - add: 'add', - addContact: 'add a contact', - allFieldRequired: 'all fields are required', - company: 'company', - contacts: 'contacts', - contactsName: "Contact's name", - contactsCompany: "Contact's company", - contactsPhone: "Contact's phone", - contactList: 'contact list', - createContactSuccessNotification: 'Contact created successfully', - deleteContactSuccessNotification: 'Contact deleted successfully', - editContact: 'edit contact', - editContactSuccessNotification: 'Contact edited successfully', - editTheContact: 'edit the contact', - errorMessageMaxLength: (maxLen: number) => `Must be ${maxLen} characters or fewer.`, - errorMessagePhonePattern: `Valid format: + (optional) plus 12 to 15 digits`, - errorMessageRequired: 'This field is required.', - errorNotification: 'Oops, there was an error', - name: 'name', - phone: 'phone', - save: 'save', - searchContactPlaceholder: 'Search contact...', + en: { + actions: 'actions', + add: 'add', + addContact: 'add a contact', + allFieldRequired: 'all fields are required', + company: 'company', + contactAgenda: 'contact agenda', + contacts: 'contacts', + contactsName: "contact's name", + contactsCompany: "contact's company", + contactsPhone: "contact's phone", + createContactSuccessNotification: 'contact created successfully', + deleteContactSuccessNotification: 'contact deleted successfully', + editContact: 'edit contact', + editContactSuccessNotification: 'contact edited successfully', + editTheContact: 'edit the contact', + errorMessageMaxLength: (maxLen: number) => `Must be ${maxLen} characters or fewer.`, + errorMessagePhonePattern: `Valid format: + (optional) plus 12 to 15 digits`, + errorMessageRequired: 'This field is required.', + errorNotification: 'Oops, there was an error', + goBack: 'go back', + name: 'name', + phone: 'phone', + save: 'save', + searchContactPlaceholder: 'Search contact...', + }, + es: { + actions: 'acciones', + add: 'añadir', + addContact: 'añada un contacto', + allFieldRequired: 'todos los campos son obligatorios', + company: 'empresa', + contactAgenda: 'agenda de contactos', + contacts: 'contactos', + contactsName: 'nombre contacto', + contactsCompany: 'empresa contacto', + contactsPhone: 'teléfono contacto', + createContactSuccessNotification: 'contacto creado correctamente', + deleteContactSuccessNotification: 'contacto eliminado', + editContact: 'editar contacto', + editContactSuccessNotification: 'contacto editado correctamente', + editTheContact: 'edite el contacto. ', + errorMessageMaxLength: (maxLen: number) => `Debe contener ${maxLen} caracteres or menos.`, + errorMessagePhonePattern: `Formato: + (opcional) y 12 a 15 digitos`, + errorMessageRequired: 'este campo es requerido.', + errorNotification: 'hubo un error', + goBack: 'volver', + name: 'nombre', + phone: 'teléfono', + save: 'guardar', + searchContactPlaceholder: 'buscar contactos...', + }, }); diff --git a/src/app/types/Language.type.ts b/src/app/types/Language.type.ts new file mode 100644 index 0000000..595dff2 --- /dev/null +++ b/src/app/types/Language.type.ts @@ -0,0 +1,3 @@ +import { strings } from '../strings'; + +export type Language = keyof typeof strings;
{{strings.name}}{{strings.company}}{{strings.phone}}{{strings.actions}}{{languageManager.strings.name}}{{languageManager.strings.company}}{{languageManager.strings.phone}}{{languageManager.strings.actions}}