diff --git a/src/app/fullname-pipe.ts b/src/app/fullname-pipe.ts index 740fc0f..f55a0d5 100644 --- a/src/app/fullname-pipe.ts +++ b/src/app/fullname-pipe.ts @@ -1,16 +1,24 @@ -import { Pipe, PipeTransform } from '@angular/core'; +import { inject, Pipe, PipeTransform } from '@angular/core'; import { Umpire } from 'db'; +import { TranslationService } from './i18n/translation.service'; @Pipe({ name: 'fullname', - standalone: true + standalone: true, + pure: false }) export class FullnamePipe implements PipeTransform { + readonly translationService = inject(TranslationService); + transform(value: Umpire | undefined, ...args: unknown[]): string { if (typeof value === 'undefined') { return ''; } - // TODO: in case of multilang, change here + + if (this.translationService.language() === 'en') { + return value.firstName + ' ' + value.lastName; + } + return value.lastName + ' ' + value.firstName; } } diff --git a/src/app/i18n/translation.pipe.ts b/src/app/i18n/translation.pipe.ts new file mode 100644 index 0000000..9d6fab0 --- /dev/null +++ b/src/app/i18n/translation.pipe.ts @@ -0,0 +1,16 @@ +import { Pipe, PipeTransform, inject } from '@angular/core'; +import { TranslationKey, TranslationParams } from './translations'; +import { TranslationService } from './translation.service'; + +@Pipe({ + name: 'translate', + standalone: true, + pure: false +}) +export class TranslatePipe implements PipeTransform { + private readonly translationService = inject(TranslationService); + + transform(key: TranslationKey, params: TranslationParams = {}): string { + return this.translationService.translate(key, params); + } +} diff --git a/src/app/i18n/translation.service.ts b/src/app/i18n/translation.service.ts new file mode 100644 index 0000000..7a0568f --- /dev/null +++ b/src/app/i18n/translation.service.ts @@ -0,0 +1,47 @@ +import { Injectable, signal } from '@angular/core'; +import { + translations, + Language, + TranslationKey, + TranslationParams +} from './translations'; + +@Injectable({ + providedIn: 'root' +}) +export class TranslationService { + private readonly storageKey = 'language'; + + readonly language = signal(this.getInitialLanguage()); + + setLanguage(language: Language): void { + this.language.set(language); + localStorage.setItem(this.storageKey, language); + } + + translate(key: TranslationKey, params: TranslationParams = {}): string { + let text: string = translations[this.language()][key]; + + for (const [param, value] of Object.entries(params)) { + text = text.replaceAll(`{${param}}`, String(value)); + } + + return text; + } + + private getInitialLanguage(): Language { + const stored = localStorage.getItem(this.storageKey); + + if (stored && stored in translations) { + return stored as Language; + } + + const browserLanguage = navigator.language.split('-')[0] as Language; + + if (browserLanguage in translations) { + return browserLanguage; + } + + return 'en'; + } +} diff --git a/src/app/i18n/translations.ts b/src/app/i18n/translations.ts new file mode 100644 index 0000000..770540b --- /dev/null +++ b/src/app/i18n/translations.ts @@ -0,0 +1,86 @@ +import { language, male, settings } from 'ionicons/icons'; +import { noop } from 'rxjs'; + +export const translations = { + en: { + courts: 'Courts', + numberOfCourts: 'Number of Courts', + showAlerts: 'Show alerts on quick buttons', + serviceJudge: 'Service Judge', + language: 'Language', + settings: 'Settings', + courtOfficials: 'Court Officials', + add: 'Add', + noCourtOfficials: 'No court officials', + newCourtOfficial: 'New Court Official', + cancel: 'Cancel', + save: 'Save', + firstName: 'First Name', + lastName: 'Last Name', + country: 'Country', + gender: 'Gender', + male: 'Male', + female: 'Female', + confirmation: 'Confirmation', + confirmDeleteUmpire: 'Do you really want to delete this court official?', + no: 'No', + yes: 'Yes', + umpires: 'Umpires', + serviceJudges: 'Service Judges', + notOnDuty: 'Not on Duty', + shortCourtName: 'C', + court: 'Court', + umpire: 'Umpire', + removeUmpireFromCourt: + 'Are you sure you want to remove them from the court?', + removeUmpiresFromCourt: + 'Are you sure you want to remove them from the court?', + courtConfirm: 'Court {courtNo}', + sendUmpireToCourt: 'Are you sure you want to send them to the court?', + sendUmpiresToCourt: 'Are you sure you want to send them to the court?', + sendUmpireMessage: '{umpire}', + sendUmpiresMessage: '{umpire} - {serviceJudge}' + }, + + hu: { + courts: 'Pályák', + numberOfCourts: 'Pályák száma', + showAlerts: 'Figyelmeztetések megjelenítése a gyors gomboknál', + serviceJudge: 'Adogatásbíró', + language: 'Nyelv', + settings: 'Beállítások', + courtOfficials: 'Tisztségviselők', + add: 'Hozzáadás', + noCourtOfficials: 'Nincsenek tisztségviselők', + newCourtOfficial: 'Új Tisztségviselő', + cancel: 'Mégse', + save: 'Mentés', + firstName: 'Keresztnév', + lastName: 'Vezetéknév', + country: 'Ország', + gender: 'Nem', + male: 'Férfi', + female: 'Nő', + confirmation: 'Megerősítés', + confirmDeleteUmpire: 'Biztosan törölni szeretnéd ezt a tisztségviselőt?', + no: 'Nem', + yes: 'Igen', + umpires: 'Játékeveztők', + serviceJudges: 'Adogatásbírók', + notOnDuty: 'Pihenő', + shortCourtName: 'P', + court: 'Pálya', + umpire: 'Játékeveztő', + removeUmpireFromCourt: 'Biztos leveszed őt a pályáról?', + removeUmpiresFromCourt: 'Biztos leveszed őket a pályáról?', + courtConfirm: 'Pálya {courtNo}', + sendUmpireToCourt: 'Biztos pályára küldöd őt?', + sendUmpiresToCourt: 'Biztos pályára küldöd őket?', + sendUmpireMessage: '{umpire}', + sendUmpiresMessage: '{umpire} - {serviceJudge}' + } +} as const; + +export type Language = keyof typeof translations; +export type TranslationKey = keyof typeof translations.en; +export type TranslationParams = Record; diff --git a/src/app/tab1/tab1.page.html b/src/app/tab1/tab1.page.html index 771df59..1805dd4 100644 --- a/src/app/tab1/tab1.page.html +++ b/src/app/tab1/tab1.page.html @@ -1,33 +1,33 @@ - Pályák + {{ 'courts' | translate }} - Pályák + {{ 'courts' | translate }} - Pálya + {{ 'court' | translate }} - P + {{ 'shortCourtName' | translate }} - Játékvezető + {{ 'umpire' | translate }} @if (serviceJudgeEnabled()) { - Adogatásbíró + {{ 'serviceJudge' | translate }} } @@ -119,19 +119,19 @@ - Játékvezetők + {{ 'umpires' | translate }} @if (serviceJudgeEnabled()) { - Adogatásbírók + {{ 'serviceJudges' | translate }} } - Pihenők + {{ 'notOnDuty' | translate }} diff --git a/src/app/tab1/tab1.page.ts b/src/app/tab1/tab1.page.ts index 2cdf781..f60b1a8 100644 --- a/src/app/tab1/tab1.page.ts +++ b/src/app/tab1/tab1.page.ts @@ -32,6 +32,8 @@ import { CourtUmpireService } from '../services/court.umpire.service'; import { CourtServiceJudgeService } from '../services/court.service.judge.service'; import { addIcons } from 'ionicons'; import { enterOutline, exitOutline } from 'ionicons/icons'; +import { TranslatePipe } from '../i18n/translation.pipe'; +import { TranslationService } from '../i18n/translation.service'; @Component({ selector: 'app-tab1', @@ -56,7 +58,8 @@ import { enterOutline, exitOutline } from 'ionicons/icons'; CdkDrag, CommonModule, CdkDragPlaceholder, - CdkDropListGroup + CdkDropListGroup, + TranslatePipe ] }) export class Tab1Page { @@ -70,6 +73,7 @@ export class Tab1Page { readonly waitingServiceJudgeService = inject(WaitingServiceJudgesService); readonly courtUmpireService = inject(CourtUmpireService); readonly courtServiceJudgeService = inject(CourtServiceJudgeService); + readonly translationService = inject(TranslationService); private alertController = inject(AlertController); private fullnamePipe = inject(FullnamePipe); @@ -398,17 +402,27 @@ export class Tab1Page { } const alert = await this.alertController.create({ - header: `Pálya ${courtNo}`, - subHeader: `Biztos leveszed ${serviceJudge ? 'őket' : 'őt'} a pályáról?`, + header: this.translationService.translate('courtConfirm', { + courtNo + }), + + subHeader: this.translationService.translate( + serviceJudge ? 'removeUmpiresFromCourt' : 'removeUmpireFromCourt' + ), + cssClass: 'wide', - message: `${this.fullnamePipe.transform(umpire)} ${serviceJudge ? '- ' : ''} ${this.fullnamePipe.transform(serviceJudge)}`, + + message: `${this.fullnamePipe.transform(umpire)}${ + serviceJudge ? ' - ' : '' + }${this.fullnamePipe.transform(serviceJudge)}`, + buttons: [ { - text: 'Nem', + text: this.translationService.translate('no'), role: 'cancel' }, { - text: 'Igen', + text: this.translationService.translate('yes'), role: 'confirm', handler: () => { if (this.serviceJudgeEnabled()) { @@ -478,17 +492,31 @@ export class Tab1Page { } const alert = await this.alertController.create({ - header: `Pálya ${courtNo}`, + header: this.translationService.translate('court', { + courtNo + }), + cssClass: 'wide', - subHeader: `Biztos pályára küldöd ${serviceJudge ? 'őket' : 'őt'}?`, - message: `${this.fullnamePipe.transform(umpire)} ${serviceJudge ? ' - ' : ''} ${this.fullnamePipe.transform(serviceJudge)}`, + + subHeader: this.translationService.translate( + serviceJudge ? 'sendUmpiresToCourt' : 'sendUmpireToCourt' + ), + + message: this.translationService.translate( + serviceJudge ? 'sendUmpiresMessage' : 'sendUmpireMessage', + { + umpire: this.fullnamePipe.transform(umpire), + serviceJudge: this.fullnamePipe.transform(serviceJudge) + } + ), + buttons: [ { - text: 'Nem', + text: this.translationService.translate('no'), role: 'cancel' }, { - text: 'Igen', + text: this.translationService.translate('yes'), role: 'confirm', handler: () => { if (this.serviceJudgeEnabled() && serviceJudge) { diff --git a/src/app/tab2/tab2.page.html b/src/app/tab2/tab2.page.html index d9dc70c..b08f7b1 100644 --- a/src/app/tab2/tab2.page.html +++ b/src/app/tab2/tab2.page.html @@ -1,10 +1,10 @@ - Játékvezetők + {{ 'courtOfficials' | translate }} - Hozzáadás + {{ 'add' | translate }} @@ -32,7 +32,7 @@ } @empty { - Nincsenek játékvezetők + {{ 'noCourtOfficials' | translate }} } @@ -44,15 +44,17 @@ - Mégse + {{ 'cancel' | translate }} - Új játékvezető + {{ 'newCourtOfficial' | translate }} - Mentés + {{ 'save' | translate }} @@ -61,33 +63,36 @@ - + @for (item of genders; track item.code) { - {{item.label}} + {{item.label | translate }} } diff --git a/src/app/tab2/tab2.page.ts b/src/app/tab2/tab2.page.ts index ae4bf39..21c69b7 100644 --- a/src/app/tab2/tab2.page.ts +++ b/src/app/tab2/tab2.page.ts @@ -1,4 +1,4 @@ -import { Component, ViewChild } from '@angular/core'; +import { Component, inject, ViewChild } from '@angular/core'; import { IonHeader, IonToolbar, @@ -26,6 +26,9 @@ import { add, trash } from 'ionicons/icons'; import { OverlayEventDetail } from '@ionic/core/components'; import { FormsModule } from '@angular/forms'; import { FullnamePipe } from '../fullname-pipe'; +import { TranslatePipe } from '../i18n/translation.pipe'; +import { TranslationKey } from '../i18n/translations'; +import { TranslationService } from '../i18n/translation.service'; @Component({ selector: 'app-tab2', @@ -51,12 +54,19 @@ import { FullnamePipe } from '../fullname-pipe'; IonSelect, IonSelectOption, IonList, - FullnamePipe + FullnamePipe, + TranslatePipe ] }) export class Tab2Page { + readonly translationService = inject(TranslationService); + @ViewChild(IonModal) modal!: IonModal; + public readonly umpireService = inject(UmpireService); + public readonly fullnamePipe = inject(FullnamePipe); + public readonly alertController = inject(AlertController); + public readonly umpires = this.umpireService.umpires; public lastName: string = ''; @@ -64,16 +74,12 @@ export class Tab2Page { public country: string = ''; public gender: string = ''; - public genders = [ - { label: 'gender.male', code: 'M' }, - { label: 'gender.female', code: 'W' } + public genders: { label: TranslationKey; code: string }[] = [ + { label: 'male', code: 'M' }, + { label: 'female', code: 'W' } ]; - constructor( - private umpireService: UmpireService, - private alertController: AlertController, - private fullnamePipe: FullnamePipe - ) { + constructor() { addIcons({ add, trash }); } @@ -112,18 +118,18 @@ export class Tab2Page { return this.lastName !== '' && this.firstName !== ''; } - public async showDeleteConfirmation(umpire: Umpire) { + public async showDeleteConfirmation(umpire: Umpire): Promise { const alert = await this.alertController.create({ - header: 'Megerősítés', - subHeader: 'Biztos törlöd a következő játékvezetőt?', + header: this.translationService.translate('confirmation'), + subHeader: this.translationService.translate('confirmDeleteUmpire'), message: this.fullnamePipe.transform(umpire), buttons: [ { - text: 'Nem', + text: this.translationService.translate('no'), role: 'cancel' }, { - text: 'Igen', + text: this.translationService.translate('yes'), role: 'confirm', handler: () => { // TODO: also remove from first tab diff --git a/src/app/tab3/tab3.page.html b/src/app/tab3/tab3.page.html index d0971ce..8090edc 100644 --- a/src/app/tab3/tab3.page.html +++ b/src/app/tab3/tab3.page.html @@ -1,6 +1,6 @@ - Beállítások + {{ 'settings' | translate }} @@ -11,11 +11,23 @@ @if (settings(); as currentSettings) { + + + English + + Magyar + + + - Adogatásbíró + {{ 'serviceJudge' | translate }} @@ -23,13 +35,13 @@ - Figyelmeztetések megjelenítése a gyors gomboknál + {{ 'showAlerts' | translate }} { this.waitingServiceJudgeService.remove(wsj.id); }); @@ -90,4 +98,8 @@ export class Tab3Page { showAlert: event.detail.checked }); } + + updateLanguage(event: CustomEvent): void { + this.translationService.setLanguage(event.detail.value as Language); + } } diff --git a/src/app/tabs/tabs.page.html b/src/app/tabs/tabs.page.html index 71fe43b..5d98888 100644 --- a/src/app/tabs/tabs.page.html +++ b/src/app/tabs/tabs.page.html @@ -2,17 +2,17 @@ - Pályák + {{ 'courts' | translate }} - Játékvezetők + {{ 'courtOfficials' | translate }} - Beállítások + {{ 'settings' | translate }} diff --git a/src/app/tabs/tabs.page.ts b/src/app/tabs/tabs.page.ts index c34fce0..091b064 100644 --- a/src/app/tabs/tabs.page.ts +++ b/src/app/tabs/tabs.page.ts @@ -8,12 +8,13 @@ import { } from '@ionic/angular/standalone'; import { addIcons } from 'ionicons'; import { gridOutline, peopleOutline, settingsOutline } from 'ionicons/icons'; +import { TranslatePipe } from '../i18n/translation.pipe'; @Component({ selector: 'app-tabs', templateUrl: 'tabs.page.html', styleUrls: ['tabs.page.scss'], - imports: [IonTabs, IonTabBar, IonTabButton, IonIcon, IonLabel] + imports: [IonTabs, IonTabBar, IonTabButton, IonIcon, IonLabel, TranslatePipe] }) export class TabsPage { public environmentInjector = inject(EnvironmentInjector); diff --git a/tsconfig.json b/tsconfig.json index 3fc44e5..f793b0a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,7 +18,7 @@ "importHelpers": true, "target": "es2022", "module": "es2020", - "lib": ["es2018", "dom"], + "lib": ["es2021", "dom"], "skipLibCheck": true, "useDefineForClassFields": false },