diff --git a/app/package-lock.json b/app/package-lock.json index db1cc3f..743c813 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -17,12 +17,16 @@ "@angular/platform-browser-dynamic": "^16.0.0", "@angular/router": "^16.0.0", "@capacitor/app": "5.0.6", + "@capacitor/camera": "^5.0.7", "@capacitor/core": "5.5.0", + "@capacitor/device": "^5.0.6", "@capacitor/haptics": "5.0.6", "@capacitor/keyboard": "5.0.6", "@capacitor/preferences": "^5.0.6", "@capacitor/status-bar": "5.0.6", "@ionic/angular": "^7.0.0", + "@ngx-translate/core": "^15.0.0", + "@ngx-translate/http-loader": "^8.0.0", "@types/uuid": "^9.0.5", "ionicons": "^7.0.0", "rxjs": "~7.8.0", @@ -2571,6 +2575,14 @@ "@capacitor/core": "^5.0.0" } }, + "node_modules/@capacitor/camera": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/@capacitor/camera/-/camera-5.0.7.tgz", + "integrity": "sha512-1Wk3Dk0UhhNHdBB07UrPvUOSL7Wi5gFZRyLY1LZL2awt34iqy2cnajtfJplFmEZHk8lD0i7NAl3HbkWm4td4OQ==", + "peerDependencies": { + "@capacitor/core": "^5.0.0" + } + }, "node_modules/@capacitor/cli": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-5.5.0.tgz", @@ -2689,6 +2701,14 @@ "tslib": "^2.1.0" } }, + "node_modules/@capacitor/device": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@capacitor/device/-/device-5.0.6.tgz", + "integrity": "sha512-tmjK0H8IKbDLMcmzZzJPbV+9yLkKJ76QOdz4A7fZAOYx2GnFHsFngxldq/wKotGAJuDX/ih3ZzHNrzVguzlv2g==", + "peerDependencies": { + "@capacitor/core": "^5.0.0" + } + }, "node_modules/@capacitor/haptics": { "version": "5.0.6", "resolved": "https://registry.npmjs.org/@capacitor/haptics/-/haptics-5.0.6.tgz", @@ -3673,6 +3693,33 @@ "webpack": "^5.54.0" } }, + "node_modules/@ngx-translate/core": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@ngx-translate/core/-/core-15.0.0.tgz", + "integrity": "sha512-Am5uiuR0bOOxyoercDnAA3rJVizo4RRqJHo8N3RqJ+XfzVP/I845yEnMADykOHvM6HkVm4SZSnJBOiz0Anx5BA==", + "engines": { + "node": "^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/common": ">=16.0.0", + "@angular/core": ">=16.0.0", + "rxjs": "^6.5.5 || ^7.4.0" + } + }, + "node_modules/@ngx-translate/http-loader": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@ngx-translate/http-loader/-/http-loader-8.0.0.tgz", + "integrity": "sha512-SFMsdUcmHF5OdZkL1CHEoSAwbP5EbAOPTLLboOCRRoOg21P4GJx+51jxGdJeGve6LSKLf4Pay7BkTwmE6vxYlg==", + "engines": { + "node": "^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/common": ">=16.0.0", + "@angular/core": ">=16.0.0", + "@ngx-translate/core": ">=15.0.0", + "rxjs": "^6.5.5 || ^7.4.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", diff --git a/app/package.json b/app/package.json index f5ccb0f..4c57f10 100644 --- a/app/package.json +++ b/app/package.json @@ -38,7 +38,10 @@ "zone.js": "~0.13.0", "@capacitor/preferences": "^5.0.6", "@types/uuid": "^9.0.5", - "@capacitor/camera": "^5.0.7" + "@capacitor/camera": "^5.0.7", + "@capacitor/device": "^5.0.6", + "@ngx-translate/core": "^15.0.0", + "@ngx-translate/http-loader": "^8.0.0" }, "devDependencies": { "@angular-devkit/build-angular": "^16.0.0", diff --git a/app/src/app/app.component.ts b/app/src/app/app.component.ts index 913de3d..7bd1682 100644 --- a/app/src/app/app.component.ts +++ b/app/src/app/app.component.ts @@ -1,4 +1,6 @@ import { Component } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { DeviceLanguageService } from './services/language/device-language.service'; @Component({ selector: 'app-root', @@ -6,5 +8,12 @@ import { Component } from '@angular/core'; styleUrls: ['app.component.scss'], }) export class AppComponent { - constructor() {} + constructor( + private translateService: TranslateService, + private deviceLanguageService: DeviceLanguageService, + ) { + this.deviceLanguageService.getLanguageCode().subscribe((languageCode) => { + this.translateService.use(languageCode); + }); + } } diff --git a/app/src/app/app.module.ts b/app/src/app/app.module.ts index 7a974cc..d270a8a 100644 --- a/app/src/app/app.module.ts +++ b/app/src/app/app.module.ts @@ -6,8 +6,18 @@ import { IonicModule, IonicRouteStrategy } from '@ionic/angular'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; -import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http'; +import { + HTTP_INTERCEPTORS, + HttpClient, + HttpClientModule, +} from '@angular/common/http'; import { AuthInterceptor } from './interceptors/auth.interceptor'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { TranslateHttpLoader } from '@ngx-translate/http-loader'; + +export function createTranslateLoader(http: HttpClient) { + return new TranslateHttpLoader(http, './assets/i18n/', '.json'); +} @NgModule({ declarations: [AppComponent], @@ -16,6 +26,13 @@ import { AuthInterceptor } from './interceptors/auth.interceptor'; IonicModule.forRoot(), AppRoutingModule, HttpClientModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: createTranslateLoader, + deps: [HttpClient], + }, + }), ], providers: [ { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, diff --git a/app/src/app/pages/forbidden/forbidden.module.ts b/app/src/app/pages/forbidden/forbidden.module.ts index d12f65d..5831ffc 100644 --- a/app/src/app/pages/forbidden/forbidden.module.ts +++ b/app/src/app/pages/forbidden/forbidden.module.ts @@ -7,9 +7,16 @@ import { IonicModule } from '@ionic/angular'; import { ForbiddenPageRoutingModule } from './forbidden-routing.module'; import { ForbiddenPage } from './forbidden.page'; +import { TranslateModule } from '@ngx-translate/core'; @NgModule({ - imports: [CommonModule, FormsModule, IonicModule, ForbiddenPageRoutingModule], + imports: [ + CommonModule, + FormsModule, + IonicModule, + ForbiddenPageRoutingModule, + TranslateModule, + ], declarations: [ForbiddenPage], }) export class ForbiddenPageModule {} diff --git a/app/src/app/pages/forbidden/forbidden.page.html b/app/src/app/pages/forbidden/forbidden.page.html index 1d7776d..3e76a9e 100644 --- a/app/src/app/pages/forbidden/forbidden.page.html +++ b/app/src/app/pages/forbidden/forbidden.page.html @@ -10,14 +10,11 @@ alt="403 Forbidden" class="forbidden-img" > -

Recurs no trobat

-

- La pàgina que busques està prohibida per al teu rol. O bé ets un - trapella o és culpa nostra. -

- Ves a l'inici +

{{ 'FORBIDDEN_ERROR.HEADER' | translate }}

+

{{ 'FORBIDDEN_ERROR.MESSAGE' | translate }}

+ + {{ 'FORBIDDEN_ERROR.EXIT' | translate }} + diff --git a/app/src/app/pages/internal-error-page/internal-error-page.module.ts b/app/src/app/pages/internal-error-page/internal-error-page.module.ts index f295842..faab798 100644 --- a/app/src/app/pages/internal-error-page/internal-error-page.module.ts +++ b/app/src/app/pages/internal-error-page/internal-error-page.module.ts @@ -7,6 +7,7 @@ import { IonicModule } from '@ionic/angular'; import { InternalErrorPagePageRoutingModule } from './internal-error-page-routing.module'; import { InternalErrorPagePage } from './internal-error-page.page'; +import { TranslateModule } from '@ngx-translate/core'; @NgModule({ imports: [ @@ -14,6 +15,7 @@ import { InternalErrorPagePage } from './internal-error-page.page'; FormsModule, IonicModule, InternalErrorPagePageRoutingModule, + TranslateModule, ], declarations: [InternalErrorPagePage], }) diff --git a/app/src/app/pages/internal-error-page/internal-error-page.page.html b/app/src/app/pages/internal-error-page/internal-error-page.page.html index a348922..ac97ed2 100644 --- a/app/src/app/pages/internal-error-page/internal-error-page.page.html +++ b/app/src/app/pages/internal-error-page/internal-error-page.page.html @@ -10,17 +10,16 @@ alt="500 - Internal Server Error" class="not-found-image" > -

Error Intern

-

- Disculpa, hi ha hagut un error intern. Torna a intentar-ho més tard. -

+

{{ 'INTERNAL_ERROR.HEADER' | translate }}

+

{{ 'INTERNAL_ERROR.MESSAGE' | translate }}

Ves a la llista de refugis + {{ 'INTERNAL_ERROR.EXIT' | translate }} + diff --git a/app/src/app/pages/login/login.module.ts b/app/src/app/pages/login/login.module.ts index 2986355..e5cc400 100644 --- a/app/src/app/pages/login/login.module.ts +++ b/app/src/app/pages/login/login.module.ts @@ -7,9 +7,16 @@ import { IonicModule } from '@ionic/angular'; import { LoginPageRoutingModule } from './login-routing.module'; import { LoginPage } from './login.page'; +import { TranslateModule } from '@ngx-translate/core'; @NgModule({ - imports: [CommonModule, FormsModule, IonicModule, LoginPageRoutingModule], + imports: [ + CommonModule, + FormsModule, + IonicModule, + LoginPageRoutingModule, + TranslateModule, + ], declarations: [LoginPage], }) export class LoginPageModule {} diff --git a/app/src/app/pages/login/login.page.html b/app/src/app/pages/login/login.page.html index f553977..1eaba8e 100644 --- a/app/src/app/pages/login/login.page.html +++ b/app/src/app/pages/login/login.page.html @@ -4,7 +4,7 @@ - Login + {{'LOGIN.HEADER' | translate }} @@ -19,27 +19,27 @@
{{errorMessage}} @@ -47,7 +47,7 @@ Login + >{{'LOGIN.SUBMIT' | translate }} diff --git a/app/src/app/pages/login/login.page.ts b/app/src/app/pages/login/login.page.ts index 5d78448..5396b24 100644 --- a/app/src/app/pages/login/login.page.ts +++ b/app/src/app/pages/login/login.page.ts @@ -12,6 +12,7 @@ import { AuthenticationErrors, ServerErrors, } from '../../schemas/auth/errors'; +import { TranslateService } from '@ngx-translate/core'; @Component({ selector: 'app-login', @@ -31,6 +32,7 @@ export class LoginPage implements OnInit { private authService: AuthService, private alertController: AlertController, private loadingController: LoadingController, + private translateService: TranslateService, ) {} ngOnInit() {} @@ -70,13 +72,12 @@ export class LoginPage implements OnInit { private async handleClientError() { const alert = await this.alertController.create({ - header: 'Alerta', - subHeader: 'El teu dispositiu està fallant', - message: - 'Funciona la connexió a Internet? Potser és culpa nostra i el nostre servidor està caigut.', + header: this.translateService.instant('HOME.CLIENT_ERROR.HEADER'), + subHeader: this.translateService.instant('HOME.CLIENT_ERROR.SUBHEADER'), + message: this.translateService.instant('HOME.CLIENT_ERROR.MESSAGE'), buttons: [ { - text: 'Tornar-ho a provar', + text: this.translateService.instant('HOME.CLIENT_ERROR.TRY_AGAIN'), handler: () => { this.router.navigate(['/login']).then(); }, @@ -98,11 +99,15 @@ export class LoginPage implements OnInit { }) .with(AdminErrors.INCORRECT_PASSWORD, () => { this.showErrorAndFinishLoadingAnimation( - 'Contrasenya incorrecta', + this.translateService.instant( + 'LOGIN.PASSWORD.ERROR_INCORRECT_PASSWORD', + ), ).then(); }) .with(AdminErrors.USER_NOT_FOUND, () => { - this.showErrorAndFinishLoadingAnimation("L'usuari no existeix").then(); + this.showErrorAndFinishLoadingAnimation( + this.translateService.instant('LOGIN.USERNAME.ERROR_NOT_FOUND'), + ).then(); }) .exhaustive(); } @@ -125,7 +130,7 @@ export class LoginPage implements OnInit { private async startLoadingAnimation() { const loading = await this.loadingController.create({ - message: 'Iniciant sessió...', + message: this.translateService.instant('LOGIN.LOADING'), translucent: true, }); return await loading.present(); diff --git a/app/src/app/pages/not-found/not-found.module.ts b/app/src/app/pages/not-found/not-found.module.ts index 76920e1..8ec7ab2 100644 --- a/app/src/app/pages/not-found/not-found.module.ts +++ b/app/src/app/pages/not-found/not-found.module.ts @@ -7,9 +7,16 @@ import { IonicModule } from '@ionic/angular'; import { NotFoundPageRoutingModule } from './not-found-routing.module'; import { NotFoundPage } from './not-found.page'; +import { TranslateModule } from '@ngx-translate/core'; @NgModule({ - imports: [CommonModule, FormsModule, IonicModule, NotFoundPageRoutingModule], + imports: [ + CommonModule, + FormsModule, + IonicModule, + NotFoundPageRoutingModule, + TranslateModule, + ], declarations: [NotFoundPage], }) export class NotFoundPageModule {} diff --git a/app/src/app/pages/not-found/not-found.page.html b/app/src/app/pages/not-found/not-found.page.html index 35f1592..0927796 100644 --- a/app/src/app/pages/not-found/not-found.page.html +++ b/app/src/app/pages/not-found/not-found.page.html @@ -10,14 +10,11 @@ alt="404 Not Found" class="not-found-image" > -

Recurs no trobat

-

- La pàgina que busques no existeix. O bé estàs fent malifetes o és - culpa nostra. -

- Ves a l'inici +

{{'NOT_FOUND_ERROR.HEADER' | translate}}

+

{{'NOT_FOUND_ERROR.MESSAGE' | translate}}

+ + {{'NOT_FOUND_ERROR.EXIT' | translate}} + diff --git a/app/src/app/pages/programming-error/programming-error.module.ts b/app/src/app/pages/programming-error/programming-error.module.ts index 1bfb38b..dc05433 100644 --- a/app/src/app/pages/programming-error/programming-error.module.ts +++ b/app/src/app/pages/programming-error/programming-error.module.ts @@ -7,6 +7,7 @@ import { IonicModule } from '@ionic/angular'; import { ProgrammingErrorPageRoutingModule } from './programming-error-routing.module'; import { ProgrammingErrorPage } from './programming-error.page'; +import { TranslateModule } from '@ngx-translate/core'; @NgModule({ imports: [ @@ -14,6 +15,7 @@ import { ProgrammingErrorPage } from './programming-error.page'; FormsModule, IonicModule, ProgrammingErrorPageRoutingModule, + TranslateModule, ], declarations: [ProgrammingErrorPage], }) diff --git a/app/src/app/pages/programming-error/programming-error.page.html b/app/src/app/pages/programming-error/programming-error.page.html index ea5df09..22204b0 100644 --- a/app/src/app/pages/programming-error/programming-error.page.html +++ b/app/src/app/pages/programming-error/programming-error.page.html @@ -10,18 +10,16 @@ alt="Programming Error" class="not-found-image" > -

Error de programació

-

- Ho sentim, però hi ha hagut un error de programació i no s'ha pogut - mostrar la pàgina que busques. -

+

{{ 'PROGRAMMING_ERROR.HEADER' | translate}}

+

{{ 'PROGRAMMING_ERROR.MESSAGE' | translate}}

Ves a la llista de refugis + {{ 'PROGRAMMING_ERROR.EXIT' | translate}} + diff --git a/app/src/app/pages/refuges/refuge-create/refuge-create.page.html b/app/src/app/pages/refuges/refuge-create/refuge-create.page.html index 1f746f4..73ed4ca 100644 --- a/app/src/app/pages/refuges/refuge-create/refuge-create.page.html +++ b/app/src/app/pages/refuges/refuge-create/refuge-create.page.html @@ -1,6 +1,6 @@ - Create refuge + {{ 'REFUGES.CREATE.HEADER' | translate}} @@ -14,7 +14,7 @@
- @@ -114,7 +113,7 @@ (click)="onCreate(createRefugeForm)" type="submit" expand="block" - >Create refuge + >{{ 'REFUGES.CREATE.SUBMIT' | translate}} diff --git a/app/src/app/pages/refuges/refuge-create/refuge-create.page.ts b/app/src/app/pages/refuges/refuge-create/refuge-create.page.ts index eccca67..ae191ed 100644 --- a/app/src/app/pages/refuges/refuge-create/refuge-create.page.ts +++ b/app/src/app/pages/refuges/refuge-create/refuge-create.page.ts @@ -21,6 +21,7 @@ import { PostImageErrors, PostImageResponse, } from '../../../schemas/image/post-image-schema'; +import { TranslateService } from '@ngx-translate/core'; @Component({ selector: 'app-refuge-create', @@ -51,13 +52,14 @@ export class RefugeCreatePage implements OnInit { private loadingController: LoadingController, private alertController: AlertController, private imageService: ImageService, + private translateService: TranslateService, ) {} ngOnInit() {} async createRefugeLoading(): Promise { const loading = await this.loadingController.create({ - message: 'Creant refugi...', + message: this.translateService.instant('REFUGES.CREATE.LOADING'), translucent: true, }); return await loading.present(); @@ -132,7 +134,7 @@ export class RefugeCreatePage implements OnInit { private async handleUnauthorizedError() { await this.showError(async () => { await this.showErrorMessage( - 'Trapella! La teva sessió no està iniciada!', + this.translateService.instant('UNAUTHORIZED_ERROR.MESSAGE'), ).then(); }); } @@ -160,7 +162,7 @@ export class RefugeCreatePage implements OnInit { private async handleConflictError() { await this.showError(async () => { await this.showErrorMessage( - 'Ja existeix un refugi amb aquest nom', + this.translateService.instant('REFUGES.FORM.ERRORS.CONFLICT'), ).then(); }); } @@ -187,16 +189,15 @@ export class RefugeCreatePage implements OnInit { private async handleClientError() { const alert = await this.alertController.create({ - header: 'Alerta', - subHeader: 'El teu dispositiu està fallant', - message: - 'Funciona la connexió a Internet? Potser és culpa nostra i el nostre servidor està caigut.', + header: this.translateService.instant('HOME.CLIENT_ERROR.HEADER'), + subHeader: this.translateService.instant('HOME.CLIENT_ERROR.SUBHEADER'), + message: this.translateService.instant('HOME.CLIENT_ERROR.MESSAGE'), buttons: [ { - text: "Ves a l'inici", + text: this.translateService.instant('HOME.CLIENT_ERROR.EXIT'), handler: () => { this.alertController.dismiss().then(); - this.router.navigate(['/home']).then(); + this.router.navigate(['/refuges']).then(); }, }, ], @@ -216,7 +217,7 @@ export class RefugeCreatePage implements OnInit { async postImageLoading(): Promise { const loading = await this.loadingController.create({ - message: 'Pujant imatge...', + message: this.translateService.instant('REFUGES.FORM.IMAGE.LOADING'), translucent: true, }); return await loading.present(); @@ -279,7 +280,7 @@ export class RefugeCreatePage implements OnInit { private async handleInvalidRequestError() { await this.showError(async () => { await this.showErrorMessage( - 'El format del fitxer ha de ser .png o .jpeg', + this.translateService.instant('REFUGES.FORM.IMAGE.ERROR'), ).then(); }); } diff --git a/app/src/app/pages/refuges/refuge-detail/refuges-detail.page.html b/app/src/app/pages/refuges/refuge-detail/refuges-detail.page.html index 9ea4e13..3da826b 100644 --- a/app/src/app/pages/refuges/refuge-detail/refuges-detail.page.html +++ b/app/src/app/pages/refuges/refuge-detail/refuges-detail.page.html @@ -11,33 +11,47 @@

{{refuge.name}}

- Regió + + {{ 'REFUGES.FORM.REGION.LABEL' | translate }} + {{refuge.region}} - Altitud - {{refuge.altitude}} metres + + {{ 'REFUGES.FORM.ALTITUDE.LABEL' | translate }} + + + {{refuge.altitude}} {{'REFUGES.LIST.METRES' | translate }} - Capacitat + + {{ 'REFUGES.LIST.CAPACITY' | translate }} + - Hivern + {{ 'REFUGES.LIST.WINTER' | translate}} - {{refuge.capacity.winter}} persones + {{refuge.capacity.winter}} {{'REFUGES.LIST.PEOPLE' | translate}} - Estiu + {{ 'REFUGES.LIST.SUMMER' | translate}} - {{refuge.capacity.summer}} persones + {{refuge.capacity.summer}} {{'REFUGES.LIST.PEOPLE' | translate}} diff --git a/app/src/app/pages/refuges/refuge-detail/refuges-detail.page.ts b/app/src/app/pages/refuges/refuge-detail/refuges-detail.page.ts index 8a3a92e..1f28e4b 100644 --- a/app/src/app/pages/refuges/refuge-detail/refuges-detail.page.ts +++ b/app/src/app/pages/refuges/refuge-detail/refuges-detail.page.ts @@ -12,6 +12,7 @@ import { DeleteRefugeFromIdErrors, DeleteRefugeResponse, } from '../../../schemas/refuge/delete-refuge-schema'; +import { TranslateService } from '@ngx-translate/core'; @Component({ selector: 'app-refuges-detail', @@ -28,6 +29,7 @@ export class RefugesDetailPage implements OnInit { private refugeService: RefugeService, private alertController: AlertController, private loadingController: LoadingController, + private translateService: TranslateService, ) { const refugeId = this.getRefugeIdFromUrl(); this.fetchRefuge(refugeId).then(); @@ -89,13 +91,12 @@ export class RefugesDetailPage implements OnInit { private async handleClientError() { const alert = await this.alertController.create({ - header: 'Alert', - subHeader: 'The client is failing', - message: - 'Is your internet connection working? Maybe is our fault and our server is down.', + header: this.translateService.instant('ERRORS.CLIENT_ERROR.HEADER'), + subHeader: this.translateService.instant('ERRORS.CLIENT_ERROR.SUBHEADER'), + message: this.translateService.instant('ERRORS.CLIENT_ERROR.MESSAGE'), buttons: [ { - text: 'OK', + text: this.translateService.instant('ERRORS.CLIENT_ERROR.EXIT'), handler: () => { this.alertController.dismiss().then(); this.fetchRefuge(this.getRefugeIdFromUrl()); @@ -157,17 +158,17 @@ export class RefugesDetailPage implements OnInit { deleteRefuge() { const alert = this.alertController.create({ - header: 'Esborrar refugi', - message: 'Estàs segur que vols esborrar el refugi?', + header: this.translateService.instant('REFUGES.DELETE.HEADER'), + message: this.translateService.instant('REFUGES.DELETE.CONFIRMATION'), buttons: [ { - text: 'Cancel·lar', + text: this.translateService.instant('REFUGES.DELETE.CANCEL'), role: 'cancel', cssClass: 'secondary', handler: () => this.alertController.dismiss().then(), }, { - text: 'Esborrar', + text: this.translateService.instant('REFUGES.DELETE.SUBMIT'), handler: () => { this.alertController .dismiss() @@ -248,7 +249,7 @@ export class RefugesDetailPage implements OnInit { private async startDeleteRefugeAnimation() { const loading = await this.loadingController.create({ - message: 'Esborrant refugi...', + message: this.translateService.instant('REFUGES.DELETE.LOADING'), translucent: true, }); return await loading.present(); diff --git a/app/src/app/pages/refuges/refuge-list/refuges-list.page.html b/app/src/app/pages/refuges/refuge-list/refuges-list.page.html index f515d78..c91f457 100644 --- a/app/src/app/pages/refuges/refuge-list/refuges-list.page.html +++ b/app/src/app/pages/refuges/refuge-list/refuges-list.page.html @@ -1,11 +1,11 @@ - Refuges + {{'REFUGES.LIST.HEADER' | translate}}
{{refuge.name}}

- Regió:
+ {{'REFUGES.FORM.REGION.LABEL' | translate}}
{{refuge.region}}

- Altitud:
- {{refuge.altitude}}m + {{'REFUGES.LIST.CAPACITY' | translate}}
+ + {{refuge.altitude}} {{'REFUGES.LIST.METRES' | translate}} +

- Capacitat: + {{'REFUGES.FORM.ALTITUDE.LABEL' | translate}} +
+ - {{'REFUGES.LIST.WINTER' | translate}}: + {{refuge.capacity.winter}} + +
+ - {{'REFUGES.LIST.SUMMER' | translate}}: + {{refuge.capacity.summer}} +
- - Hivern: {{refuge.capacity.winter}}
- - Estiu: {{refuge.capacity.summer}}

diff --git a/app/src/app/pages/refuges/refuge-list/refuges-list.page.ts b/app/src/app/pages/refuges/refuge-list/refuges-list.page.ts index 28bcbba..1f1d0da 100644 --- a/app/src/app/pages/refuges/refuge-list/refuges-list.page.ts +++ b/app/src/app/pages/refuges/refuge-list/refuges-list.page.ts @@ -19,6 +19,7 @@ import { Observable, Subject, } from 'rxjs'; +import { TranslateService } from '@ngx-translate/core'; @Component({ selector: 'app-refuges', @@ -35,6 +36,7 @@ export class RefugesListPage implements OnInit { private router: Router, private refugeService: RefugeService, private alertController: AlertController, + private translateService: TranslateService, ) { this.errors = this.refugeService.getRefuges().pipe( filter( @@ -87,13 +89,12 @@ export class RefugesListPage implements OnInit { private async handleClientError() { const alert = await this.alertController.create({ - header: 'Alert', - subHeader: 'The client is failing', - message: - 'Is your internet connection working? Maybe is our fault and our server is down.', + header: this.translateService.instant('ERRORS.CLIENT_ERROR.HEADER'), + subHeader: this.translateService.instant('ERRORS.CLIENT_ERROR.SUBHEADER'), + message: this.translateService.instant('ERRORS.CLIENT_ERROR.MESSAGE'), buttons: [ { - text: 'OK', + text: this.translateService.instant('ERRORS.CLIENT_ERROR.EXIT'), handler: () => { this.alertController.dismiss().then(); }, diff --git a/app/src/app/pages/refuges/refuge-update/refuge-udpate.page.ts b/app/src/app/pages/refuges/refuge-update/refuge-udpate.page.ts index 0ec77b1..7fa7835 100644 --- a/app/src/app/pages/refuges/refuge-update/refuge-udpate.page.ts +++ b/app/src/app/pages/refuges/refuge-update/refuge-udpate.page.ts @@ -23,6 +23,7 @@ import { NgForm } from '@angular/forms'; import { UpdateRefugeResponse } from '../../../schemas/refuge/update/update-refuge-response'; import { UpdateRefugeError } from '../../../schemas/refuge/update/update-refuge-error'; import { ServerError } from '../../../schemas/refuge/create/create-refuge-error'; +import { TranslateService } from '@ngx-translate/core'; @Component({ selector: 'app-refuge-update', @@ -57,6 +58,7 @@ export class RefugeUdpatePage implements OnInit { private alertController: AlertController, private loadingController: LoadingController, private imageService: ImageService, + private translateService: TranslateService, ) { const refugeId = this.getRefugeIdFromUrl(); this.fetchRefuge(refugeId).then(); @@ -166,13 +168,12 @@ export class RefugeUdpatePage implements OnInit { private async handleClientError() { const alert = await this.alertController.create({ - header: 'Alert', - subHeader: 'The client is failing', - message: - 'Is your internet connection working? Maybe is our fault and our server is down.', + header: this.translateService.instant('HOME.CLIENT_ERROR.HEADER'), + subHeader: this.translateService.instant('HOME.CLIENT_ERROR.SUBHEADER'), + message: this.translateService.instant('HOME.CLIENT_ERROR.MESSAGE'), buttons: [ { - text: 'OK', + text: this.translateService.instant('HOME.CLIENT_ERROR.EXIT'), handler: () => { this.alertController.dismiss().then(); this.fetchRefuge(this.getRefugeIdFromUrl()); @@ -192,7 +193,7 @@ export class RefugeUdpatePage implements OnInit { async postImageLoading(): Promise { const loading = await this.loadingController.create({ - message: 'Pujant imatge...', + message: this.translateService.instant('REFUGES.FORM.IMAGE.LOADING'), translucent: true, }); return await loading.present(); @@ -255,7 +256,7 @@ export class RefugeUdpatePage implements OnInit { private async handleInvalidRequestError() { await this.showError(async () => { await this.showErrorMessage( - 'El format del fitxer ha de ser .png o .jpeg', + this.translateService.instant('REFUGES.FORM.IMAGE.ERROR'), ).then(); }); } @@ -272,7 +273,7 @@ export class RefugeUdpatePage implements OnInit { async updateRefugeLoading(): Promise { const loading = await this.loadingController.create({ - message: 'Actualitzant refugi...', + message: this.translateService.instant('REFUGES.UPDATE.LOADING'), translucent: true, }); return await loading.present(); @@ -340,7 +341,7 @@ export class RefugeUdpatePage implements OnInit { private async handleUnauthorizedError() { await this.showError(async () => { await this.showErrorMessage( - 'Trapella! La teva sessió no està iniciada!', + this.translateService.instant('UNAUTHORIZED_ERROR.MESSAGE'), ).then(); }); } @@ -357,7 +358,7 @@ export class RefugeUdpatePage implements OnInit { private async handleConflictError() { await this.showError(async () => { await this.showErrorMessage( - 'Ja existeix un refugi amb aquest nom', + this.translateService.instant('REFUGES.FORM.ERRORS.CONFLICT'), ).then(); }); } diff --git a/app/src/app/pages/refuges/refuge-update/refuge-update.page.html b/app/src/app/pages/refuges/refuge-update/refuge-update.page.html index 9b9ceaa..638c41c 100644 --- a/app/src/app/pages/refuges/refuge-update/refuge-update.page.html +++ b/app/src/app/pages/refuges/refuge-update/refuge-update.page.html @@ -1,6 +1,6 @@ - Update refuge + {{ 'REFUGES.UPDATE.HEADER' | translate}} @@ -14,7 +14,7 @@
@@ -113,7 +113,7 @@ (click)="onUpdate(updateRefugeForm)" type="submit" expand="block" - >Update refuge + >{{ 'REFUGES.UPDATE.SUBMIT' | translate}} diff --git a/app/src/app/pages/refuges/refuges.module.ts b/app/src/app/pages/refuges/refuges.module.ts index f4d6df0..8f76299 100644 --- a/app/src/app/pages/refuges/refuges.module.ts +++ b/app/src/app/pages/refuges/refuges.module.ts @@ -10,6 +10,7 @@ import { RefugesListPage } from './refuge-list/refuges-list.page'; import { RefugesDetailPage } from './refuge-detail/refuges-detail.page'; import { RefugeCreatePage } from './refuge-create/refuge-create.page'; import { RefugeUdpatePage } from './refuge-update/refuge-udpate.page'; +import { TranslateModule } from '@ngx-translate/core'; @NgModule({ imports: [ @@ -18,6 +19,7 @@ import { RefugeUdpatePage } from './refuge-update/refuge-udpate.page'; IonicModule, RefugesPageRoutingModule, NgOptimizedImage, + TranslateModule, ], declarations: [ RefugesListPage, diff --git a/app/src/app/services/language/device-language.service.spec.ts b/app/src/app/services/language/device-language.service.spec.ts new file mode 100644 index 0000000..68e6fc6 --- /dev/null +++ b/app/src/app/services/language/device-language.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { DeviceLanguageService } from './device-language.service'; + +describe('DeviceLanguageService', () => { + let service: DeviceLanguageService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(DeviceLanguageService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/app/src/app/services/language/device-language.service.ts b/app/src/app/services/language/device-language.service.ts new file mode 100644 index 0000000..7870a4a --- /dev/null +++ b/app/src/app/services/language/device-language.service.ts @@ -0,0 +1,48 @@ +import { Injectable } from '@angular/core'; +import { Device } from '@capacitor/device'; +import { distinctUntilChanged, map, mergeMap, Observable, timer } from 'rxjs'; +import { fromPromise } from 'rxjs/internal/observable/innerFrom'; +import { StorageService } from '../storage/storage.service'; +import { TranslateService } from '@ngx-translate/core'; + +const LANGUAGE_KEY = 'language'; + +@Injectable({ + providedIn: 'root', +}) +export class DeviceLanguageService { + constructor( + private storageService: StorageService, + private translateService: TranslateService, + ) {} + + async getCurrentLanguageCode(): Promise { + const languageCode = await this.storageService.get(LANGUAGE_KEY); + if (languageCode) return languageCode; + return Device.getLanguageCode().then((languageTag) => languageTag.value); + } + + /** + * Gets the language code of the device, fetching it every 3 seconds. + * TODO: This is a workaround for the fact that Capacitor's Device plugin + * doesn't have an observable for getting the language code, so we have to + * poll it every 3 seconds. + */ + getLanguageCode(): Observable { + return timer(0, 3_000).pipe( + mergeMap(() => fromPromise(this.getCurrentLanguageCode())), + distinctUntilChanged(), + ); + } + + async setLanguageCode(languageCode: string): Promise { + const languageCodes = await this.getLanguagesCodes(); + if (!languageCodes.includes(languageCode)) + throw new Error(`Language code ${languageCode} not supported`); + await this.storageService.set(LANGUAGE_KEY, languageCode); + } + + async getLanguagesCodes(): Promise { + return this.translateService.getLangs(); + } +} diff --git a/app/src/assets/i18n/ca.json b/app/src/assets/i18n/ca.json new file mode 100644 index 0000000..f03a310 --- /dev/null +++ b/app/src/assets/i18n/ca.json @@ -0,0 +1,114 @@ +{ + "HOME": { + "CLIENT_ERROR": { + "HEADER": "Alerta!", + "SUBHEADER": "El teu dispositiu està fallant!", + "MESSAGE": "Té accés a Internet el teu dispositiu? Potser és culpa nostra i els nostres servidors estan caiguts", + "TRY_AGAIN": "Torna-ho a provar", + "EXIT": "Anar a casa" + } + }, + "FORBIDDEN_ERROR": { + "HEADER": "Recurs prohibit", + "MESSAGE": "La pàgina que estàs buscant està prohibit per al teu rol. O estàs essent un ximplet o és culpa nostra.", + "EXIT": "Anar a casa" + }, + "INTERNAL_ERROR": { + "HEADER": "Error intern", + "MESSAGE": "Ho sento, estem experimentant problemes interns. Torna-ho a provar més tard", + "EXIT": "Anar a casa" + }, + "NOT_FOUND_ERROR": { + "HEADER": "Recurs no trobat", + "MESSAGE": "La pàgina que estàs buscant no existeix. O estàs fent alguna cosa dolenta o és culpa nostra.", + "EXIT": "Anar a casa" + }, + "PROGRAMMING_ERROR": { + "HEADER": "Error de programació", + "MESSAGE": "Som uns inútils, el nostre codi té un error de programació i no ens n'hem adonat. Envia'ns un correu electrònic!", + "EXIT": "Anar a casa" + }, + "UNAUTHORIZED_ERROR": { + "MESSAGE": "No estàs autoritzat a accedir a aquest recurs. Estàs connectat?" + }, + "LOGIN": { + "HEADER": "Iniciar sessió", + "USERNAME": { + "LABEL": "Nom d'usuari", + "ERROR": "El nom d'usuari és obligatori", + "ERROR_NOT_FOUND": "Aquest administrador no existeix" + }, + "PASSWORD": { + "LABEL": "Contrasenya", + "ERROR": "La contrasenya és obligatòria", + "ERROR_INCORRECT_PASSWORD": "La contrasenya és incorrecta" + }, + "SUBMIT": "Iniciar sessió", + "LOADING": "Iniciant sessió..." + }, + "REFUGES": { + "CREATE": { + "HEADER": "Crear refugi", + "SUBMIT": "Crear refugi", + "LOADING": "Creant refugi..." + }, + "UPDATE": { + "HEADER": "Actualitzar refugi", + "SUBMIT": "Actualitzar refugi", + "LOADING": "Actualitzant refugi..." + }, + "DELETE": { + "HEADER": "Eliminar refugi", + "SUBMIT": "Eliminar", + "LOADING": "Eliminant refugi...", + "CONFIRMATION": "Estàs segur que vols eliminar aquest refugi?", + "CANCEL": "Cancel·lar" + }, + "FORM": { + "NAME": { + "LABEL": "Nom", + "ERROR": "El nom del refugi és obligatori" + }, + "REGION": { + "LABEL": "Regió", + "ERROR": "La regió del refugi és obligatòria" + }, + "ALTITUDE": { + "LABEL": "Altitud", + "ERROR": "L'altitud del refugi és obligatòria" + }, + "LONGITUDE": { + "LABEL": "Longitud", + "ERROR": "La longitud del refugi és obligatòria" + }, + "LATITUDE": { + "LABEL": "Latitud", + "ERROR": "La latitud del refugi és obligatòria" + }, + "WINTER_CAPACITY": { + "LABEL": "Capacitat a l'hivern", + "ERROR": "La capacitat a l'hivern del refugi és obligatòria" + }, + "SUMMER_CAPACITY": { + "LABEL": "Capacitat a l'estiu", + "ERROR": "La capacitat a l'estiu del refugi és obligatòria" + }, + "IMAGE": { + "LOADING": "Pujant imatge...", + "ERROR": "El format del fitxer ha de ser .png o .jpeg" + }, + "ERRORS": { + "CONFLICT": "Ja hi ha un refugi amb aquest nom" + } + }, + "LIST": { + "HEADER": "Refugis", + "CAPACITY": "Capacitat", + "METRES": "metres", + "WINTER": "Hivern", + "SUMMER": "Estiu", + "PEOPLE": "persones", + "SEARCH": "Cercar per nom" + } + } +} diff --git a/app/src/assets/i18n/en.json b/app/src/assets/i18n/en.json new file mode 100644 index 0000000..ae59c20 --- /dev/null +++ b/app/src/assets/i18n/en.json @@ -0,0 +1,114 @@ +{ + "HOME": { + "CLIENT_ERROR": { + "HEADER": "Alert!", + "SUBHEADER": "Your device is failing!", + "MESSAGE": "Does your device have internet access? Maybe is our fault and our servers are down", + "TRY_AGAIN": "Try again", + "EXIT": "Go home" + } + }, + "FORBIDDEN_ERROR": { + "HEADER": "Forbidden resource", + "MESSAGE": "The page you're looking for is forbidden for your role. You're either being naughty or it's our fault.", + "EXIT": "Go home" + }, + "INTERNAL_ERROR": { + "HEADER": "Internal error", + "MESSAGE": "Sorry, we are having internal issues. Try again later", + "EXIT": "Go home" + }, + "NOT_FOUND_ERROR": { + "HEADER": "Resource not found", + "MESSAGE": "The page you are searching doesn't exist. You are being a bad guy or it's our fault.", + "EXIT": "Go home" + }, + "PROGRAMMING_ERROR": { + "HEADER": "Programming error", + "MESSAGE": "We suck, our code had a programming error and we didn't notice it. Send us an email!", + "EXIT": "Go home" + }, + "UNAUTHORIZED_ERROR": { + "MESSAGE": "You are not authorized to access this resource. Are you logged in?" + }, + "LOGIN": { + "HEADER": "Login", + "USERNAME": { + "LABEL": "Username", + "ERROR": "Username is required", + "ERROR_NOT_FOUND": "This admin doesn't exist" + }, + "PASSWORD": { + "LABEL": "Password", + "ERROR": "Password is required", + "ERROR_INCORRECT_PASSWORD": "Password is incorrect" + }, + "SUBMIT": "Login", + "LOADING": "Signing in..." + }, + "REFUGES": { + "CREATE": { + "HEADER": "Create refuge", + "SUBMIT": "Create refuge", + "LOADING": "Creating refuge..." + }, + "UPDATE": { + "HEADER": "Update refuge", + "SUBMIT": "Update refuge", + "LOADING": "Updating refuge..." + }, + "DELETE": { + "HEADER": "Delete refuge", + "SUBMIT": "Delete", + "LOADING": "Deleting refuge...", + "CONFIRMATION": "Are you sure you want to delete this refuge?", + "CANCEL": "Cancel" + }, + "FORM": { + "NAME": { + "LABEL": "Name", + "ERROR": "Refuge's name is required" + }, + "REGION": { + "LABEL": "Region", + "ERROR": "Refuge's region is required" + }, + "ALTITUDE": { + "LABEL": "Altitude", + "ERROR": "Refuge's altitude is required" + }, + "LONGITUDE": { + "LABEL": "Longitude", + "ERROR": "Refuge's longitude is required" + }, + "LATITUDE": { + "LABEL": "Latitude", + "ERROR": "Refuge's latitude is required" + }, + "WINTER_CAPACITY": { + "LABEL": "Winter capacity", + "ERROR": "Refuge's winter capacity is required" + }, + "SUMMER_CAPACITY": { + "LABEL": "Summer capacity", + "ERROR": "Refuge's summer capacity is required" + }, + "IMAGE": { + "LOADING": "Uploading image...", + "ERROR": "File format must be .png or .jpeg" + }, + "ERRORS": { + "CONFLICT": "There is already a refuge with this name" + } + }, + "LIST": { + "HEADER": "Refuges", + "CAPACITY": "Capacity", + "METRES": "metres", + "WINTER": "Winter", + "SUMMER": "Summer", + "PEOPLE": "people", + "SEARCH": "Search by name" + } + } +} diff --git a/app/src/assets/i18n/es.json b/app/src/assets/i18n/es.json new file mode 100644 index 0000000..374bf48 --- /dev/null +++ b/app/src/assets/i18n/es.json @@ -0,0 +1,114 @@ +{ + "HOME": { + "CLIENT_ERROR": { + "HEADER": "¡Alerta!", + "SUBHEADER": "¡Tu dispositivo está fallando!", + "MESSAGE": "¿Tiene tu dispositivo acceso a Internet? Tal vez sea nuestra culpa y nuestros servidores estén caídos", + "TRY_AGAIN": "Intenta de nuevo", + "EXIT": "Ir a inicio" + } + }, + "FORBIDDEN_ERROR": { + "HEADER": "Recurso prohibido", + "MESSAGE": "La página que estás buscando está prohibida para tu rol. O estás siendo travieso o es nuestra culpa.", + "EXIT": "Ir a inicio" + }, + "INTERNAL_ERROR": { + "HEADER": "Error interno", + "MESSAGE": "Lo siento, estamos experimentando problemas internos. Inténtalo de nuevo más tarde", + "EXIT": "Ir a inicio" + }, + "NOT_FOUND_ERROR": { + "HEADER": "Recurso no encontrado", + "MESSAGE": "La página que estás buscando no existe. O estás haciendo algo mal o es nuestra culpa.", + "EXIT": "Ir a inicio" + }, + "PROGRAMMING_ERROR": { + "HEADER": "Error de programación", + "MESSAGE": "Somos malos, nuestro código tiene un error de programación y no lo notamos. ¡Envíanos un correo electrónico!", + "EXIT": "Ir a inicio" + }, + "UNAUTHORIZED_ERROR": { + "MESSAGE": "No estás autorizado para acceder a este recurso. ¿Estás conectado?" + }, + "LOGIN": { + "HEADER": "Iniciar sesión", + "USERNAME": { + "LABEL": "Nombre de usuario", + "ERROR": "El nombre de usuario es obligatorio", + "ERROR_NOT_FOUND": "Este administrador no existe" + }, + "PASSWORD": { + "LABEL": "Contraseña", + "ERROR": "La contraseña es obligatoria", + "ERROR_INCORRECT_PASSWORD": "La contraseña es incorrecta" + }, + "SUBMIT": "Iniciar sesión", + "LOADING": "Iniciando sesión..." + }, + "REFUGES": { + "CREATE": { + "HEADER": "Crear refugio", + "SUBMIT": "Crear refugio", + "LOADING": "Creando refugio..." + }, + "UPDATE": { + "HEADER": "Actualizar refugio", + "SUBMIT": "Actualizar refugio", + "LOADING": "Actualizando refugio..." + }, + "DELETE": { + "HEADER": "Eliminar refugio", + "SUBMIT": "Eliminar", + "LOADING": "Eliminando refugio...", + "CONFIRMATION": "¿Estás seguro de que quieres eliminar este refugio?", + "CANCEL": "Cancelar" + }, + "FORM": { + "NAME": { + "LABEL": "Nombre", + "ERROR": "El nombre del refugio es obligatorio" + }, + "REGION": { + "LABEL": "Región", + "ERROR": "La región del refugio es obligatoria" + }, + "ALTITUDE": { + "LABEL": "Altitud", + "ERROR": "La altitud del refugio es obligatoria" + }, + "LONGITUDE": { + "LABEL": "Longitud", + "ERROR": "La longitud del refugio es obligatoria" + }, + "LATITUDE": { + "LABEL": "Latitud", + "ERROR": "La latitud del refugio es obligatoria" + }, + "WINTER_CAPACITY": { + "LABEL": "Capacidad en invierno", + "ERROR": "La capacidad en invierno del refugio es obligatoria" + }, + "SUMMER_CAPACITY": { + "LABEL": "Capacidad en verano", + "ERROR": "La capacidad en verano del refugio es obligatoria" + }, + "IMAGE": { + "LOADING": "Subiendo imagen...", + "ERROR": "El formato del archivo debe ser .png o .jpeg" + }, + "ERRORS": { + "CONFLICT": "Ya existe un refugio con este nombre" + } + }, + "LIST": { + "HEADER": "Refugios", + "CAPACITY": "Capacidad", + "METRES": "metros", + "WINTER": "Invierno", + "SUMMER": "Verano", + "PEOPLE": "personas", + "SEARCH": "Buscar por nombre" + } + } +}