From f5e650d58a09d38780035b3a3a412351e8b476c1 Mon Sep 17 00:00:00 2001
From: Alex ORLUC <alex.orluc@maarch.org>
Date: Thu, 11 Apr 2019 19:02:09 +0200
Subject: [PATCH] FEAT #8805 add internationalization functionality

---
 package.json                                  |  6 ++
 src/frontend/app/app.component.ts             |  5 +-
 src/frontend/app/app.module.ts                | 40 +++++++---
 .../app/profile/profile.component.html        | 73 ++++++++-----------
 src/frontend/app/profile/profile.component.ts | 43 +++++++----
 .../app/service/notification.service.ts       |  2 +-
 src/frontend/assets/i18n/en.json              | 40 ++++++++++
 src/frontend/assets/i18n/fr.json              | 40 ++++++++++
 8 files changed, 178 insertions(+), 71 deletions(-)
 create mode 100644 src/frontend/assets/i18n/en.json
 create mode 100644 src/frontend/assets/i18n/fr.json

diff --git a/package.json b/package.json
index cc4524b83f..ce432c1424 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,8 @@
     "build": "ng build",
     "build-prod": "ng build --prod",
     "build-watch": "ng build --watch",
+    "build-lang": "ng xi18n --i18n-locale fr --output-path locale",
+    "extract-translations": "ngx-translate-extract i src/frontend/app -p /**/*.html -o src/frontend/assets/i18n/en.json src/frontend/assets/i18n/fr.json --format namespaced-json",
     "build-css": "node-sass --include-path scss src/frontend/css/maarch-material.scss src/frontend/css/maarch-material.css --output-style compressed",
     "test": "ng test",
     "lint": "ng lint",
@@ -22,6 +24,8 @@
     "@fortawesome/fontawesome-free": "^5.8.1",
     "@ngrx/store": "^7.4.0",
     "@ngrx/store-devtools": "^7.4.0",
+    "@ngx-translate/core": "^11.0.1",
+    "@ngx-translate/http-loader": "^4.0.0",
     "angular2-draggable": "^2.2.2",
     "angular2-signaturepad": "^2.8.0",
     "core-js": "^2.6.5",
@@ -33,6 +37,7 @@
     "ngx-cookie-service": "^2.1.0",
     "ngx-scroll-event": "^1.0.8",
     "pdfjs-dist": "^2.0.943",
+    "rxjs": "^6.4.0",
     "simple-pdf-viewer": "^2.0.3",
     "zone.js": "~0.8.29"
   },
@@ -52,6 +57,7 @@
     "@angular/platform-browser": "^7.2.12",
     "@angular/platform-browser-dynamic": "^7.2.12",
     "@angular/router": "^7.2.12",
+    "@biesbjerg/ngx-translate-extract": "^2.3.4",
     "@types/hammerjs": "^2.0.36",
     "@types/jasmine": "^3.3.12",
     "@types/jasminewd2": "^2.0.6",
diff --git a/src/frontend/app/app.component.ts b/src/frontend/app/app.component.ts
index 630a144679..c27a4e171a 100755
--- a/src/frontend/app/app.component.ts
+++ b/src/frontend/app/app.component.ts
@@ -4,6 +4,7 @@ import { SignaturesContentService } from './service/signatures.service';
 import { HttpClient } from '@angular/common/http';
 import { NotificationService } from './service/notification.service';
 import { DomSanitizer } from '@angular/platform-browser';
+import { TranslateService } from '@ngx-translate/core';
 
 @Component({
   selector: 'app-root',
@@ -14,7 +15,8 @@ import { DomSanitizer } from '@angular/platform-browser';
 
 export class AppComponent {
 
-  constructor(public http: HttpClient, public signaturesService: SignaturesContentService, public sanitizer: DomSanitizer, private cookieService: CookieService, public notificationService: NotificationService) {
+  constructor(private translate: TranslateService, public http: HttpClient, public signaturesService: SignaturesContentService, public sanitizer: DomSanitizer, private cookieService: CookieService, public notificationService: NotificationService) {
+    translate.setDefaultLang('en');
 
     if (this.cookieService.check('maarchParapheurAuth')) {
       const cookieInfo = JSON.parse(atob(this.cookieService.get('maarchParapheurAuth')));
@@ -22,6 +24,7 @@ export class AppComponent {
       this.http.get('../rest/users/' + cookieInfo.id)
         .subscribe((data: any) => {
           this.signaturesService.userLogged = data.user;
+          this.translate.use(this.signaturesService.userLogged.preferences.lang);
         },
           (err: any) => {
             this.notificationService.handleErrors(err);
diff --git a/src/frontend/app/app.module.ts b/src/frontend/app/app.module.ts
index e33f452e08..55fcd1f8f2 100755
--- a/src/frontend/app/app.module.ts
+++ b/src/frontend/app/app.module.ts
@@ -1,16 +1,19 @@
 import { BrowserModule } from '@angular/platform-browser';
 import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
 import { FormsModule, ReactiveFormsModule } from '@angular/forms';
-import { HttpClientModule } from '@angular/common/http';
+import { HttpClientModule, HttpClient } from '@angular/common/http';
 import { RouterModule } from '@angular/router';
 import { NgModule } from '@angular/core';
 import { HammerGestureConfig, HAMMER_GESTURE_CONFIG } from '@angular/platform-browser';
 
+// import ngx-translate and the http loader
+import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
+import { TranslateHttpLoader } from '@ngx-translate/http-loader';
 
-export class CustomHammerConfig extends HammerGestureConfig  {
+export class CustomHammerConfig extends HammerGestureConfig {
   overrides = <any>{
-      'pinch': { enable: false },
-      'rotate': { enable: false }
+    'pinch': { enable: false },
+    'rotate': { enable: false }
   };
 }
 
@@ -72,16 +75,24 @@ import { SignaturesContentService } from './service/signatures.service';
     BrowserAnimationsModule,
     HttpClientModule,
     RouterModule,
+    HttpClientModule,
+    TranslateModule.forRoot({
+      loader: {
+        provide: TranslateLoader,
+        useFactory: HttpLoaderFactory,
+        deps: [HttpClient]
+      }
+    }),
     SignaturePadModule,
     ScrollEventModule,
     AngularDraggableModule,
     SimplePdfViewerModule,
     AppMaterialModule,
     RouterModule.forRoot([
-      { path: 'documents/:id', component: DocumentComponent},
-      { path: 'documents', component: DocumentComponent},
-      { path: 'login', component: LoginComponent},
-      { path: '**',   redirectTo: 'login', pathMatch: 'full' },
+      { path: 'documents/:id', component: DocumentComponent },
+      { path: 'documents', component: DocumentComponent },
+      { path: 'login', component: LoginComponent },
+      { path: '**', redirectTo: 'login', pathMatch: 'full' },
     ], { useHash: true }),
   ],
   entryComponents: [
@@ -97,11 +108,16 @@ import { SignaturesContentService } from './service/signatures.service';
     {
       provide: HAMMER_GESTURE_CONFIG,
       useClass: CustomHammerConfig
-      },
+    },
     CookieService],
-    exports: [
-        RouterModule
-    ],
+  exports: [
+    RouterModule
+  ],
   bootstrap: [AppComponent]
 })
 export class AppModule { }
+
+// For traductions
+export function HttpLoaderFactory(http: HttpClient) {
+  return new TranslateHttpLoader(http, './frontend/assets/i18n/', '.json');
+}
diff --git a/src/frontend/app/profile/profile.component.html b/src/frontend/app/profile/profile.component.html
index 2bbc738793..ec3cc01e51 100644
--- a/src/frontend/app/profile/profile.component.html
+++ b/src/frontend/app/profile/profile.component.html
@@ -2,7 +2,7 @@
     <div class="main-header">
         <header class="profile-header">
             <div class="user" style="color: #F99830">
-                Mon Profil
+                {{'lang.myProfil' | translate}}
             </div>
             <div class="avatarProfile"
                 [ngStyle]="{'background': 'url(' + this.profileInfo.picture + ') no-repeat scroll center center / cover'}"
@@ -15,26 +15,26 @@
         <form (ngSubmit)="submitProfile()" #profileForm="ngForm">
             <mat-tab-group #tabProfile (selectedTabChange)="initProfileTab($event);"
                 (swipeleft)="siwtchToleft(tabProfile)" (swiperight)="siwtchToRight(tabProfile)">
-                <mat-tab label="Informations">
+                <mat-tab label="{{'lang.informations' | translate}}">
                     <div class="profile-content">
                         <mat-form-field class="input-row">
-                            <input name="login" matInput placeholder="Courriel" type="mail"
+                            <input name="login" matInput placeholder="{{'lang.email' | translate}}" type="mail"
                                 [(ngModel)]="profileInfo.email" (keyup)="newLogin.mail=newLogin.mail.toLowerCase()"
                                 disabled required>
                         </mat-form-field>
                         <mat-form-field class="input-row">
-                            <input name="firstname" matInput placeholder="Prénom" [(ngModel)]="profileInfo.firstname"
+                            <input name="firstname" matInput placeholder="{{'lang.firstname' | translate}}" [(ngModel)]="profileInfo.firstname"
                                 required>
                         </mat-form-field>
                         <mat-form-field class="input-row">
-                            <input name="nom" matInput placeholder="Nom" [(ngModel)]="profileInfo.lastname" required>
+                            <input name="nom" matInput placeholder="{{'lang.lastname' | translate}}" [(ngModel)]="profileInfo.lastname" required>
                         </mat-form-field>
                         <mat-accordion>
                             <mat-expansion-panel (closed)="showPassword=false" (opened)="changePasswd()"
                                 #passwordContent>
                                 <mat-expansion-panel-header>
                                     <mat-panel-title>
-                                        Modifier mon mot de passe
+                                        {{'lang.updatePassword' | translate}}
                                     </mat-panel-title>
                                     <mat-panel-description>
                                     </mat-panel-description>
@@ -42,7 +42,7 @@
                                 <ng-container *ngIf="showPassword">
                                     <mat-form-field class="input-row">
                                         <input name="currentPassword" matInput [(ngModel)]="password.currentPassword"
-                                            placeholder="Mot de passe actuel"
+                                            placeholder="{{'lang.currentPassword' | translate}}"
                                             [type]="hideCurrentPassword ? 'password' : 'text'">
                                         <mat-icon matSuffix (click)="hideCurrentPassword = !hideCurrentPassword"
                                             class="fa fa-2x"
@@ -50,7 +50,7 @@
                                     </mat-form-field>
                                     <mat-form-field class="input-row">
                                         <input name="newPassword" matInput [(ngModel)]="password.newPassword"
-                                            placeholder="Nouveau mot de passe"
+                                            placeholder="{{'lang.newPassword' | translate}}"
                                             [type]="hideNewPassword ? 'password' : 'text'"
                                             (keyup)="checkPasswordValidity(password.newPassword)">
                                         <mat-icon matSuffix (click)="hideNewPassword = !hideNewPassword"
@@ -61,34 +61,32 @@
                                     <mat-form-field class="input-row">
                                         <input name="passwordConfirmation" matInput
                                             [(ngModel)]="password.passwordConfirmation"
-                                            placeholder="Confirmer le nouveau mot de passe"
+                                            placeholder="{{'lang.passwordConfirmation' | translate}}"
                                             [type]="hideNewPasswordConfirm ? 'password' : 'text'">
                                         <mat-icon matSuffix (click)="hideNewPasswordConfirm = !hideNewPasswordConfirm"
                                             class="fa fa-2x"
                                             [ngClass]="[hideNewPasswordConfirm ? 'fa-eye-slash' : 'fa-eye']"></mat-icon>
                                         <mat-hint style="color:red;"
-                                            *ngIf="password.passwordConfirmation !== password.newPassword">Les
-                                            mots de passe ne correspondent pas !</mat-hint>
+                                            *ngIf="password.passwordConfirmation !== password.newPassword">{{'lang.passwordNotMatch' | translate}}</mat-hint>
                                         <mat-hint style="color:green;"
                                             *ngIf="password.passwordConfirmation === password.newPassword && password.newPassword.length > 0 && password.passwordConfirmation.length> 0">
-                                            Les
-                                            mots de passe sont identiques</mat-hint>
+                                            {{'lang.samePassword' | translate}}</mat-hint>
                                     </mat-form-field>
                                 </ng-container>
                             </mat-expansion-panel>
                         </mat-accordion>
                     </div>
                 </mat-tab>
-                <mat-tab label="Préférences">
+                <mat-tab label="{{'lang.preferences' | translate}}">
                     <div class="profile-content">
                         <div class="input-row">
                             <fieldset>
-                                <legend align="left">Notifications</legend>
+                                <legend align="left">{{'lang.notifications' | translate}}</legend>
                                 <div class="form-container">
                                     <div class="form-2-col">
                                         <mat-slide-toggle [checked]="this.profileInfo.preferences.notifications"
                                             (change)="this.profileInfo.preferences.notifications=!this.profileInfo.preferences.notifications"
-                                            color="primary">Recevoir les notifications</mat-slide-toggle>
+                                            color="primary">{{'lang.receiveNotif' | translate}}</mat-slide-toggle>
                                     </div>
                                     <div class="form-2-col" style="text-align: justify;font-size: 12px;">
 
@@ -98,13 +96,13 @@
                         </div>
                         <div class="input-row">
                             <fieldset>
-                                <legend align="left">Langue</legend>
+                                <legend align="left">{{'lang.language' | translate}}</legend>
                                 <div class="form-container">
                                     <div class="form-2-col">
                                         <mat-form-field>
-                                            <mat-select name="langUser" [(ngModel)]="this.profileInfo.preferences.lang">
+                                            <mat-select #langSelect name="langUser" [(ngModel)]="this.profileInfo.preferences.lang">
                                                 <mat-option value="fr">Français</mat-option>
-                                                <mat-option value="en">Anglais</mat-option>
+                                                <mat-option value="en">English</mat-option>
                                             </mat-select>
                                         </mat-form-field>
                                     </div>
@@ -112,34 +110,27 @@
 
                                     </div>
                                 </div>
-
                             </fieldset>
                         </div>
                         <div class="input-row">
                             <fieldset>
-                                <legend align="left">Mode de l'annotation</legend>
+                                <legend align="left">{{'lang.annotationMode' | translate}}</legend>
                                 <div class="form-container">
                                     <div class="form-2-col">
                                         <mat-form-field>
                                             <mat-select name="writingMode"
                                                 [(ngModel)]="this.profileInfo.preferences.writingMode">
-                                                <mat-option value="direct">Libre</mat-option>
-                                                <mat-option value="stylus">Stylet Apple <i class="fab fa-apple"></i>
+                                                <mat-option value="direct">{{'lang.free' | translate}}</mat-option>
+                                                <mat-option value="stylus">{{'lang.appleStylus' | translate}} <i class="fab fa-apple"></i>
                                                 </mat-option>
                                             </mat-select>
                                         </mat-form-field>
                                     </div>
                                     <div *ngIf="this.profileInfo.preferences.writingMode == 'stylus'" class="form-2-col"
-                                        style="text-align: justify;font-size: 12px;">
-                                        Vous ne pourrez annoter les documents qu'avec le <b>stylet Apple</b>.<br />
-                                        Cela a pour avantage de pouvoir poser la main sur la tablette sans perturber
-                                        l'écriture.
+                                        style="text-align: justify;font-size: 12px;" [innerHTML]="'lang.freeModeInfo' | translate">
                                     </div>
                                     <div *ngIf="this.profileInfo.preferences.writingMode == 'direct'" class="form-2-col"
-                                        style="text-align: justify;font-size: 12px;">
-                                        Mode standard, permettant l'annotation avec tout format (souris, main, stylets
-                                        de
-                                        toutes marques).
+                                        style="text-align: justify;font-size: 12px;" [innerHTML]="'lang.standardModeInfo' | translate">
                                     </div>
                                 </div>
 
@@ -147,7 +138,7 @@
                         </div>
                         <div class="input-row">
                             <fieldset>
-                                <legend align="left">Épaisseur du trait</legend>
+                                <legend align="left">{{'lang.stylusWidh' | translate}}</legend>
                                 <div class="form-container">
                                     <div class="form-2-col">
                                         <mat-form-field>
@@ -167,15 +158,15 @@
                         </div>
                         <div class="input-row">
                             <fieldset style="display:table;">
-                                <legend align="left">Couleur par défaut</legend>
+                                <legend align="left">{{'lang.defaultColor' | translate}}</legend>
                                 <div class="form-container">
                                     <div class="form-2-col">
                                         <mat-form-field>
                                             <mat-select name="writingColor"
                                                 [(ngModel)]="this.profileInfo.preferences.writingColor">
-                                                <mat-option style="color:#000000" value="#000000">Noir</mat-option>
-                                                <mat-option style="color:#1a75ff" value="#1a75ff">Bleu</mat-option>
-                                                <mat-option style="color:#FF0000" value="#FF0000">Rouge</mat-option>
+                                                <mat-option style="color:#000000" value="#000000">{{'lang.black' | translate}}</mat-option>
+                                                <mat-option style="color:#1a75ff" value="#1a75ff">{{'lang.blue' | translate}}</mat-option>
+                                                <mat-option style="color:#FF0000" value="#FF0000">{{'lang.red' | translate}}</mat-option>
                                             </mat-select>
                                         </mat-form-field>
                                     </div>
@@ -189,11 +180,11 @@
                         </div>
                     </div>
                 </mat-tab>
-                <mat-tab label="Administrations" *ngIf="signaturesService.userLogged.canManageRestUsers">
+                <mat-tab label="{{'lang.administrations' | translate}}" *ngIf="signaturesService.userLogged.canManageRestUsers">
                     <div class="profile-content">
                         <div class="input-row">
                             <fieldset>
-                                <legend align="left">Utilisateurs Webservice</legend>
+                                <legend align="left">{{'lang.wsUser' | translate}}</legend>
                                 <div class="form-container">
                                     <div class="form-col" style="width:35%;">
                                         <mat-form-field>
@@ -207,7 +198,7 @@
                                     <div class="form-col" style="width:35%;">
                                         <mat-form-field class="input-row">
                                             <input name="newPasswordRest" matInput [(ngModel)]="currentUserRestPassword"
-                                                placeholder="Nouveau mot de passe"
+                                                placeholder="{{'lang.newPassword' | translate}}"
                                                 [type]="hideNewPassword ? 'password' : 'text'"
                                                 (keyup)="checkPasswordValidity(currentUserRestPassword)">
                                             <mat-icon matSuffix (click)="hideNewPassword = !hideNewPassword"
@@ -219,7 +210,7 @@
                                     <div class="form-col" style="width:29%;">
                                         <button mat-raised-button type="button" color="primary"
                                             [disabled]="handlePassword.error || currentUserRestPassword.length === 0"
-                                            (click)="updateRestUser()">Modifier</button>
+                                            (click)="updateRestUser()">{{'lang.update' | translate}}</button>
                                     </div>
                                 </div>
                             </fieldset>
@@ -230,7 +221,7 @@
             <span class="actions">
                 <button class="validate" mat-button color="primary" type="submit"
                     [disabled]="allowValidate() || !profileForm.form.valid">{{
-                    msgButton }}</button>
+                    msgButton | translate}}</button>
                 <button class="cancel" mat-icon-button type="button" (tap)="closeProfile();">
                     <mat-icon fontSet="fas" fontIcon="fa-arrow-left fa-2x"></mat-icon>
                 </button>
diff --git a/src/frontend/app/profile/profile.component.ts b/src/frontend/app/profile/profile.component.ts
index bfab89ba71..cc68daab6e 100644
--- a/src/frontend/app/profile/profile.component.ts
+++ b/src/frontend/app/profile/profile.component.ts
@@ -6,6 +6,8 @@ import { SignaturesContentService } from '../service/signatures.service';
 import { NotificationService } from '../service/notification.service';
 import { CookieService } from 'ngx-cookie-service';
 import * as EXIF from 'exif-js';
+import { TranslateService } from '@ngx-translate/core';
+import {_} from '@biesbjerg/ngx-translate-extract/dist/utils/utils';
 
 @Component({
     selector: 'app-my-profile',
@@ -54,9 +56,9 @@ export class ProfileComponent implements OnInit {
     showPassword = false;
 
     disableState = false;
-    msgButton = 'Valider';
+    msgButton = 'lang.validate';
 
-    constructor(public http: HttpClient, iconReg: MatIconRegistry, public sanitizer: DomSanitizer, public notificationService: NotificationService, public signaturesService: SignaturesContentService, private cookieService: CookieService) {
+    constructor(private translate: TranslateService, public http: HttpClient, iconReg: MatIconRegistry, public sanitizer: DomSanitizer, public notificationService: NotificationService, public signaturesService: SignaturesContentService, private cookieService: CookieService) {
         iconReg.addSvgIcon('maarchLogo', sanitizer.bypassSecurityTrustResourceUrl('../src/frontend/assets/logo_white.svg'));
     }
 
@@ -176,7 +178,7 @@ export class ProfileComponent implements OnInit {
 
     submitProfile() {
         this.disableState = true;
-        this.msgButton = 'Envoi...';
+        this.msgButton = 'lang.sending';
         let profileToSend = {
             'firstname': this.profileInfo.firstname,
             'lastname': this.profileInfo.lastname,
@@ -198,6 +200,8 @@ export class ProfileComponent implements OnInit {
                 this.signaturesService.userLogged.picture = data.user.picture;
                 this.signaturesService.userLogged.preferences = data.user.preferences;
                 this.profileInfo.picture = data.user.picture;
+                this.setLang(this.signaturesService.userLogged.preferences.lang);
+
                 $('.avatarProfile').css({ 'transform': 'rotate(0deg)' });
 
                 if (this.showPassword) {
@@ -206,15 +210,15 @@ export class ProfileComponent implements OnInit {
                             this.password.newPassword = '';
                             this.password.passwordConfirmation = '';
                             this.password.currentPassword = '';
-                            this.notificationService.success('Profil modifié');
+                            this.notificationService.success('lang.profileUpdated');
                             this.disableState = false;
-                            this.msgButton = 'Valider';
+                            this.msgButton = 'lang.validate';
                             this.closeProfile();
                         }, (err) => {
                             this.disableState = false;
-                            this.msgButton = 'Valider';
+                            this.msgButton = 'lang.validate';
                             if (err.status === 401) {
-                                this.notificationService.error('Mauvais mot de passe');
+                                this.notificationService.error('lang.badPassword');
                             } else {
                                 this.notificationService.handleErrors(err);
                             }
@@ -222,14 +226,14 @@ export class ProfileComponent implements OnInit {
                 }
 
                 if (!this.showPassword) {
-                    this.notificationService.success('Profil modifié');
+                    this.notificationService.success('lang.profileUpdated');
                     this.disableState = false;
-                    this.msgButton = 'Valider';
+                    this.msgButton = 'lang.validate';
                     this.closeProfile();
                 }
             }, (err) => {
                 this.disableState = false;
-                this.msgButton = 'Valider';
+                this.msgButton = 'lang.validate';
                 this.notificationService.handleErrors(err);
                 this.disableState = false;
             });
@@ -272,10 +276,10 @@ export class ProfileComponent implements OnInit {
                 };
                 myReader.readAsDataURL(fileToUpload);
             } else {
-                this.notificationService.error('Ceci n\'est pas une image');
+                this.notificationService.error('lang.notAnImage');
             }
         } else {
-            this.notificationService.error('Image trop volumineuse (5mo max.)');
+            this.notificationService.error('lang.imageTooBig');
         }
     }
 
@@ -332,18 +336,25 @@ export class ProfileComponent implements OnInit {
                     this.currentUserRest = data.users[0];
 
                 }, (err: any) => {
-                        this.notificationService.handleErrors(err);
-                    });
+                    this.notificationService.handleErrors(err);
+                });
         }
     }
 
     updateRestUser() {
-        this.http.put('../rest/users/' + this.currentUserRest.id + '/password', {'newPassword' : this.currentUserRestPassword})
+        this.http.put('../rest/users/' + this.currentUserRest.id + '/password', { 'newPassword': this.currentUserRestPassword })
             .subscribe(() => {
                 this.currentUserRestPassword = '';
-                this.notificationService.success('Mot de passe de ' + this.currentUserRest.firstname + ' ' + this.currentUserRest.lastname + ' modifié');
+                this.translate.get('lang.passwordOfUserUpdated', {user: this.currentUserRest.firstname + ' ' + this.currentUserRest.lastname}).subscribe((res: string) => {
+                    this.notificationService.success(res);
+                });
             }, (err) => {
                 this.notificationService.handleErrors(err);
             });
     }
+
+
+    setLang(lang: any) {
+        this.translate.use(lang);
+    }
 }
diff --git a/src/frontend/app/service/notification.service.ts b/src/frontend/app/service/notification.service.ts
index 523ffafddd..f5940ed59d 100644
--- a/src/frontend/app/service/notification.service.ts
+++ b/src/frontend/app/service/notification.service.ts
@@ -5,7 +5,7 @@ import { Router } from '@angular/router';
 
 @Component({
     selector: 'app-custom-snackbar',
-    template: '{{data.message}}' // You may also use a HTML file
+    template: '{{data.message | translate}}' // You may also use a HTML file
 })
 export class CustomSnackbarComponent {
     constructor(@Inject(MAT_SNACK_BAR_DATA) public data: any) { }
diff --git a/src/frontend/assets/i18n/en.json b/src/frontend/assets/i18n/en.json
new file mode 100644
index 0000000000..024c33449e
--- /dev/null
+++ b/src/frontend/assets/i18n/en.json
@@ -0,0 +1,40 @@
+{
+	"lang": {
+		"administrations": "Administrations",
+		"annotationMode": "Annotation mode",
+		"appleStylus": "Apple stylus",
+		"badPassword": "Bad password",
+		"black": "Black",
+		"blue": "Blue",
+		"currentPassword": "Actual password",
+		"defaultColor": "Default color",
+		"email": "Email",
+		"firstname": "Firstname",
+		"free": "Free",
+		"freeModeInfo": "",
+		"imageTooBig": "Image is too big (5mo max.)",
+		"informations": "Informations",
+		"language": "Language",
+		"lastname": "Lastname",
+		"myProfil": "My profile",
+		"newPassword": "New password",
+		"notAnImage": "This is not an image",
+		"notifications": "Notifications",
+		"password": "Password",
+		"passwordConfirmation": "New password confirmation",
+		"passwordNotMatch": "Passwords does not match",
+		"passwordOfUserUpdated": "Password of {{user}} updated",
+		"preferences": "Preferences",
+		"profileUpdated": "Profile updated",
+		"receiveNotif": "Receive notifications",
+		"red": "Red",
+		"samePassword": "Passwords match",
+		"sending": "Sending...",
+		"standardModeInfo": "",
+		"stylusWidh": "Stylus width",
+		"update": "Update",
+		"updatePassword": "Update password",
+		"validate": "Validate",
+		"wsUser": "Web service user"
+	}
+}
diff --git a/src/frontend/assets/i18n/fr.json b/src/frontend/assets/i18n/fr.json
new file mode 100644
index 0000000000..c50f961522
--- /dev/null
+++ b/src/frontend/assets/i18n/fr.json
@@ -0,0 +1,40 @@
+{
+	"lang": {
+		"administrations": "Administrations",
+		"annotationMode": "Mode de l'annotation",
+		"appleStylus": "Stylet Apple",
+		"badPassword": "Mauvais mot de passe",
+		"black": "Noir",
+		"blue": "Bleu",
+		"currentPassword": "Mot de passe actuel",
+		"defaultColor": "Couleur par défaut",
+		"email": "Courriel",
+		"firstname": "Prénom",
+		"free": "Libre",
+		"freeModeInfo": "Vous ne pourrez annoter les documents qu'avec le <b>stylet Apple</b>.<br />Cela a pour avantage de pouvoir poser la main sur la tablette sans perturber l'écriture.",
+		"imageTooBig": "Image trop volumineuse (5mo max.)",
+		"informations": "Informations",
+		"language": "Langue",
+		"lastname": "Nom",
+		"myProfil": "Mon profil",
+		"newPassword": "Nouveau mot de passe",
+		"notAnImage": "Ceci n'est pas une image",
+		"notifications": "Notifications",
+		"password": "Mot de passe",
+		"passwordConfirmation": "Confirmer le nouveau mot de passe",
+		"passwordNotMatch": "Les mots de passe ne correspondent pas",
+		"passwordOfUserUpdated": "Mot de passe de {{user}} modifié",
+		"preferences": "Préférences",
+		"profileUpdated": "Profil modifié",
+		"receiveNotif": "Recevoir les notifications",
+		"red": "Rouge",
+		"samePassword": "Les mots de passe sont identiques",
+		"sending": "Envoi...",
+		"standardModeInfo": "Mode standard, permettant l'annotation avec tout format (souris, main, stylets de toutes marques).",
+		"stylusWidh": "Épaisseur du trait",
+		"update": "Modifier",
+		"updatePassword": "Modifier le mot de passe",
+		"validate": "Valider",
+		"wsUser": "Utilisateur web service"
+	}
+}
-- 
GitLab