feat; add multi language support

This commit is contained in:
2025-12-21 20:06:18 -03:00
parent 3926f4d254
commit 7b4c95678e
23 changed files with 163 additions and 77 deletions

View File

@@ -5,20 +5,20 @@
<app-form-field
[errorsDictionary]="companyAndNameErrorsDictionary"
formControlName="name"
[label]="(strings.name|upperfirst) + ':'"
[placeholder]="strings.contactsName|upperfirst"
[label]="(languageManager.strings.name|upperfirst) + ':'"
[placeholder]="languageManager.strings.contactsName|upperfirst"
/>
<app-form-field
[errorsDictionary]="companyAndNameErrorsDictionary"
formControlName="company"
[label]="(strings.company|upperfirst) + ':'"
[placeholder]="strings.contactsCompany|upperfirst"
[label]="(languageManager.strings.company|upperfirst) + ':'"
[placeholder]="languageManager.strings.contactsCompany|upperfirst"
/>
<app-form-field
[errorsDictionary]="phoneErrorsDictionary"
formControlName="phone"
[label]="(strings.phone|upperfirst) +':'"
[placeholder]="strings.contactsPhone|upperfirst"
[label]="(languageManager.strings.phone|upperfirst) +':'"
[placeholder]="languageManager.strings.contactsPhone|upperfirst"
type="tel"
/>
</div>

View File

@@ -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<ContactDTO>();
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();

View File

@@ -2,10 +2,10 @@
<table class="contact-list">
<thead>
<tr>
<th>{{strings.name}}</th>
<th>{{strings.company}}</th>
<th>{{strings.phone}}</th>
<th>{{strings.actions}}</th>
<th>{{languageManager.strings.name}}</th>
<th>{{languageManager.strings.company}}</th>
<th>{{languageManager.strings.phone}}</th>
<th>{{languageManager.strings.actions}}</th>
</tr>
</thead>
<tbody>

View File

@@ -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<ContactDTO[]>([]);
protected readonly strings = inject(STRINGS_INJECTOR);
protected readonly languageManager = inject(LanguageManager);
private readonly contactService = inject(ContactService);
private readonly router = inject(Router);

View File

@@ -1,10 +1,10 @@
<div class="contact-list">
<div class="contact-list__container">
<h2>{{strings.contacts|upperfirst}}</h2>
<h2>{{languageManager.strings.contacts|upperfirst}}</h2>
<app-contact-search-bar (contactSearch)="filter=$event"/>
<app-counter
[count]="(this.contacts$|async)?.length ?? 0"
item="{{strings.contacts}}"
item="{{languageManager.strings.contacts}}"
/>
<app-contact-list-table [contactList]="(this.contacts$|async)|contactsFilter:filter"/>
</div>

View File

@@ -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 = '';

View File

@@ -1,5 +1,5 @@
<input
(input)="contactSearch.emit($event.target.value)"
class="search-bar shadow"
placeholder="{{strings.searchContactPlaceholder}}"
placeholder="{{languageManager.strings.searchContactPlaceholder|upperfirst}}"
type="text">

View File

@@ -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<string>()
protected readonly strings = inject(STRINGS_INJECTOR);
contactSearch = output<string>();
protected readonly languageManager = inject(LanguageManager);
}

View File

@@ -1,6 +1,6 @@
@if(notification$(); as notification) {
<div class="notification notification--{{notification.type}} notification--fade-in-out">
<p>{{ notification.message }}</p>
<p>{{ notification.message|upperfirst }}</p>
</div>
}

View File

@@ -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',
})

View File

@@ -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 } {

View File

@@ -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;

View File

@@ -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);

View File

@@ -1,11 +1,11 @@
<app-main-header
[title]="strings.editContact"
[title]="languageManager.strings.editContact"
>
<app-rounded-btn
(keydown)="goBack()"
(click)="goBack()"
slot="action-button"
text="Go back"
text={{languageManager.strings.goBack|upperfirst}}
/>
</app-main-header>
<app-card bgColor="var(--secondary)">
@@ -14,9 +14,9 @@
class="form"
(contact)="edit(form.value)"
[form]="form"
[submitText]="strings.save"
[submitText]="languageManager.strings.save"
><app-form-header
[title]="strings.editTheContact|upperfirst"
[title]="languageManager.strings.editTheContact|upperfirst"
slot="header"
/>
</app-contact-form>

View File

@@ -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);

View File

@@ -1,13 +1,13 @@
<app-main-header [title]="strings.contactList"/>
<app-main-header [title]="languageManager.strings.contactAgenda"/>
<app-card bgColor="var(--secondary)">
<app-contact-form
(contact)="save($event)"
[form]="form"
[submitText]="strings.add"
[submitText]="languageManager.strings.add"
class="form"
><app-form-header
[subtitle]="strings.allFieldRequired|upperfirst"
[title]="strings.addContact|upperfirst"
[subtitle]="languageManager.strings.allFieldRequired|upperfirst"
[title]="languageManager.strings.addContact|upperfirst"
slot="header"
/>
</app-contact-form>

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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<ContactDTO[]>([]);
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
)
);
}

View File

@@ -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]);
});
});

View File

@@ -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];
}
}

View File

@@ -1,25 +1,54 @@
export const strings = Object.freeze({
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",
contactList: 'contact list',
createContactSuccessNotification: 'Contact created successfully',
deleteContactSuccessNotification: 'Contact deleted successfully',
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',
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...',
},
});

View File

@@ -0,0 +1,3 @@
import { strings } from '../strings';
export type Language = keyof typeof strings;