feat: implement internationalization with multi-language support and runtime language selection
This commit is contained in:
1 parent
2de09383b9
commit
9c9114879d
26 files changed
+826
-159
No files matched your search
@@ -2,17 +2,14 @@
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<mat-icon>rule</mat-icon>
|
||||
Règles Personnalisées
|
||||
{{ tr().rules.title }}
|
||||
</mat-panel-title>
|
||||
<mat-panel-description> Gérer les règles d'optimisation </mat-panel-description>
|
||||
<mat-panel-description> {{ tr().rules.subtitle }} </mat-panel-description>
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
<div class="priority-statement">
|
||||
<mat-icon color="primary">info</mat-icon>
|
||||
<span
|
||||
>L'ordre des règles définit leur priorité d'exécution (de haut en bas). Glissez-déposez
|
||||
pour réorganiser.</span
|
||||
>
|
||||
<span>{{ tr().rules.priorityInfo }}</span>
|
||||
</div>
|
||||
|
||||
<div class="rules-list" cdkDropList (cdkDropListDropped)="drop($event)">
|
||||
@@ -52,50 +49,21 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rule-summary">
|
||||
@if (rule.type === RuleType.IF_THEN) {
|
||||
<span>
|
||||
Si un jour férié tombe un
|
||||
<strong>{{
|
||||
daysOfWeek[rule.dayOfWeek > 0 ? rule.dayOfWeek - 1 : 6]
|
||||
.label
|
||||
}}</strong
|
||||
>, placer <strong>{{ rule.placeDays }}</strong> jour(s)
|
||||
<strong>{{ getDayTypeLabel(rule.placeType) }}</strong>
|
||||
<strong>
|
||||
{{
|
||||
placementOptions.find((p) => p.value === rule.placement)
|
||||
?.label | lowercase
|
||||
}}</strong
|
||||
>
|
||||
et obtenir <strong>{{ rule.rewardDays }}</strong> jour(s)
|
||||
<strong>{{ getDayTypeLabel(rule.rewardType) }}</strong> en
|
||||
bonus.
|
||||
</span>
|
||||
} @else if (rule.type === RuleType.TIME_WINDOW) {
|
||||
<span>
|
||||
Placer <strong>{{ rule.placeDays }}</strong> jour(s)
|
||||
<strong>{{ getDayTypeLabel(rule.placeType) }}</strong> entre le
|
||||
<strong>{{ rule.startDate | date: 'dd/MM/yyyy' }}</strong> et le
|
||||
<strong>{{ rule.endDate | date: 'dd/MM/yyyy' }}</strong
|
||||
>.
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<div class="rule-summary" [innerHTML]="getRuleSummary(rule)"></div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="rule-editor">
|
||||
<h4>Modifier la règle</h4>
|
||||
<h4>{{ tr().rules.editRule }}</h4>
|
||||
|
||||
<mat-form-field class="full-width">
|
||||
<mat-label>Nom de la règle</mat-label>
|
||||
<mat-label>{{ tr().rules.ruleName }}</mat-label>
|
||||
<input matInput [(ngModel)]="$any(editBuffer).name" required />
|
||||
</mat-form-field>
|
||||
|
||||
@if ($any(editBuffer).type === RuleType.IF_THEN) {
|
||||
<div class="editor-row">
|
||||
<mat-form-field>
|
||||
<mat-label>Jour férié tombe un</mat-label>
|
||||
<mat-label>{{ tr().rules.holidayFalls }}</mat-label>
|
||||
<mat-select [(ngModel)]="$any(editBuffer).dayOfWeek">
|
||||
@for (day of daysOfWeek; track day.value) {
|
||||
<mat-option [value]="day.value">{{
|
||||
@@ -107,9 +75,9 @@
|
||||
</div>
|
||||
|
||||
<div class="editor-row">
|
||||
<span class="action-text">Placer</span>
|
||||
<span class="action-text">{{ tr().rules.place }}</span>
|
||||
<mat-form-field class="small-input">
|
||||
<mat-label>Jours</mat-label>
|
||||
<mat-label>{{ tr().rules.days }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="number"
|
||||
@@ -119,7 +87,7 @@
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field>
|
||||
<mat-label>Type</mat-label>
|
||||
<mat-label>{{ tr().rules.type }}</mat-label>
|
||||
<mat-select [(ngModel)]="$any(editBuffer).placeType">
|
||||
@for (type of placeDayTypes; track type) {
|
||||
<mat-option [value]="type">{{
|
||||
@@ -130,7 +98,7 @@
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field>
|
||||
<mat-label>Position</mat-label>
|
||||
<mat-label>{{ tr().rules.position }}</mat-label>
|
||||
<mat-select [(ngModel)]="$any(editBuffer).placement">
|
||||
@for (opt of placementOptions; track opt.value) {
|
||||
<mat-option [value]="opt.value">{{
|
||||
@@ -142,9 +110,9 @@
|
||||
</div>
|
||||
|
||||
<div class="editor-row">
|
||||
<span class="action-text">Obtenir en bonus</span>
|
||||
<span class="action-text">{{ tr().rules.getBonus }}</span>
|
||||
<mat-form-field class="small-input">
|
||||
<mat-label>Jours</mat-label>
|
||||
<mat-label>{{ tr().rules.days }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="number"
|
||||
@@ -154,7 +122,7 @@
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field>
|
||||
<mat-label>Type</mat-label>
|
||||
<mat-label>{{ tr().rules.type }}</mat-label>
|
||||
<mat-select [(ngModel)]="$any(editBuffer).rewardType">
|
||||
@for (type of rewardDayTypes; track type) {
|
||||
<mat-option [value]="type">{{
|
||||
@@ -167,7 +135,7 @@
|
||||
} @else {
|
||||
<div class="editor-row">
|
||||
<mat-form-field>
|
||||
<mat-label>Date de début</mat-label>
|
||||
<mat-label>{{ tr().rules.startDate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[matDatepicker]="startPicker"
|
||||
@@ -181,7 +149,7 @@
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field>
|
||||
<mat-label>Date de fin</mat-label>
|
||||
<mat-label>{{ tr().rules.endDate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[matDatepicker]="endPicker"
|
||||
@@ -196,9 +164,9 @@
|
||||
</div>
|
||||
|
||||
<div class="editor-row">
|
||||
<span class="action-text">Placer</span>
|
||||
<span class="action-text">{{ tr().rules.place }}</span>
|
||||
<mat-form-field class="small-input">
|
||||
<mat-label>Jours</mat-label>
|
||||
<mat-label>{{ tr().rules.days }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="number"
|
||||
@@ -208,7 +176,7 @@
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field>
|
||||
<mat-label>Type</mat-label>
|
||||
<mat-label>{{ tr().rules.type }}</mat-label>
|
||||
<mat-select [(ngModel)]="$any(editBuffer).placeType">
|
||||
@for (type of placeDayTypes; track type) {
|
||||
<mat-option [value]="type">{{
|
||||
@@ -221,9 +189,11 @@
|
||||
}
|
||||
|
||||
<div class="editor-actions">
|
||||
<button mat-button (click)="cancelEdit()">Annuler</button>
|
||||
<button mat-button (click)="cancelEdit()">
|
||||
{{ tr().rules.cancel }}
|
||||
</button>
|
||||
<button mat-flat-button color="primary" (click)="saveEdit()">
|
||||
Enregistrer
|
||||
{{ tr().rules.save }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -234,17 +204,17 @@
|
||||
@if (isAddingNew()) {
|
||||
<div class="rule-item editing">
|
||||
<div class="rule-editor">
|
||||
<h4>Ajouter une règle</h4>
|
||||
<h4>{{ tr().rules.addRule }}</h4>
|
||||
|
||||
<mat-form-field class="full-width">
|
||||
<mat-label>Nom de la règle</mat-label>
|
||||
<mat-label>{{ tr().rules.ruleName }}</mat-label>
|
||||
<input matInput [(ngModel)]="$any(editBuffer).name" required />
|
||||
</mat-form-field>
|
||||
|
||||
@if (newRuleType() === RuleType.IF_THEN) {
|
||||
<div class="editor-row">
|
||||
<mat-form-field>
|
||||
<mat-label>Jour férié tombe un</mat-label>
|
||||
<mat-label>{{ tr().rules.holidayFalls }}</mat-label>
|
||||
<mat-select [(ngModel)]="$any(editBuffer).dayOfWeek">
|
||||
@for (day of daysOfWeek; track day.value) {
|
||||
<mat-option [value]="day.value">{{ day.label }}</mat-option>
|
||||
@@ -254,9 +224,9 @@
|
||||
</div>
|
||||
|
||||
<div class="editor-row">
|
||||
<span class="action-text">Placer</span>
|
||||
<span class="action-text">{{ tr().rules.place }}</span>
|
||||
<mat-form-field class="small-input">
|
||||
<mat-label>Jours</mat-label>
|
||||
<mat-label>{{ tr().rules.days }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="number"
|
||||
@@ -266,7 +236,7 @@
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field>
|
||||
<mat-label>Type</mat-label>
|
||||
<mat-label>{{ tr().rules.type }}</mat-label>
|
||||
<mat-select [(ngModel)]="$any(editBuffer).placeType">
|
||||
@for (type of placeDayTypes; track type) {
|
||||
<mat-option [value]="type">{{
|
||||
@@ -277,7 +247,7 @@
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field>
|
||||
<mat-label>Position</mat-label>
|
||||
<mat-label>{{ tr().rules.position }}</mat-label>
|
||||
<mat-select [(ngModel)]="$any(editBuffer).placement">
|
||||
@for (opt of placementOptions; track opt.value) {
|
||||
<mat-option [value]="opt.value">{{ opt.label }}</mat-option>
|
||||
@@ -287,9 +257,9 @@
|
||||
</div>
|
||||
|
||||
<div class="editor-row">
|
||||
<span class="action-text">Obtenir en bonus</span>
|
||||
<span class="action-text">{{ tr().rules.getBonus }}</span>
|
||||
<mat-form-field class="small-input">
|
||||
<mat-label>Jours</mat-label>
|
||||
<mat-label>{{ tr().rules.days }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="number"
|
||||
@@ -299,7 +269,7 @@
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field>
|
||||
<mat-label>Type</mat-label>
|
||||
<mat-label>{{ tr().rules.type }}</mat-label>
|
||||
<mat-select [(ngModel)]="$any(editBuffer).rewardType">
|
||||
@for (type of rewardDayTypes; track type) {
|
||||
<mat-option [value]="type">{{
|
||||
@@ -312,7 +282,7 @@
|
||||
} @else {
|
||||
<div class="editor-row">
|
||||
<mat-form-field>
|
||||
<mat-label>Date de début</mat-label>
|
||||
<mat-label>{{ tr().rules.startDate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[matDatepicker]="startPicker"
|
||||
@@ -326,7 +296,7 @@
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field>
|
||||
<mat-label>Date de fin</mat-label>
|
||||
<mat-label>{{ tr().rules.endDate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[matDatepicker]="endPicker"
|
||||
@@ -341,9 +311,9 @@
|
||||
</div>
|
||||
|
||||
<div class="editor-row">
|
||||
<span class="action-text">Placer</span>
|
||||
<span class="action-text">{{ tr().rules.place }}</span>
|
||||
<mat-form-field class="small-input">
|
||||
<mat-label>Jours</mat-label>
|
||||
<mat-label>{{ tr().rules.days }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="number"
|
||||
@@ -353,7 +323,7 @@
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field>
|
||||
<mat-label>Type</mat-label>
|
||||
<mat-label>{{ tr().rules.type }}</mat-label>
|
||||
<mat-select [(ngModel)]="$any(editBuffer).placeType">
|
||||
@for (type of placeDayTypes; track type) {
|
||||
<mat-option [value]="type">{{
|
||||
@@ -366,9 +336,9 @@
|
||||
}
|
||||
|
||||
<div class="editor-actions">
|
||||
<button mat-button (click)="cancelEdit()">Annuler</button>
|
||||
<button mat-button (click)="cancelEdit()">{{ tr().rules.cancel }}</button>
|
||||
<button mat-flat-button color="primary" (click)="saveEdit()">
|
||||
Ajouter
|
||||
{{ tr().rules.add }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -377,10 +347,10 @@
|
||||
|
||||
<div class="add-buttons" [class.hidden]="isAddingNew() || editingRuleId()">
|
||||
<button mat-stroked-button (click)="startAddNew(RuleType.IF_THEN)">
|
||||
<mat-icon>add</mat-icon> Nouvelle Règle "Si ... Alors"
|
||||
<mat-icon>add</mat-icon> {{ tr().rules.newIfThenRule }}
|
||||
</button>
|
||||
<button mat-stroked-button (click)="startAddNew(RuleType.TIME_WINDOW)">
|
||||
<mat-icon>add</mat-icon> Nouvelle Période Imposée
|
||||
<mat-icon>add</mat-icon> {{ tr().rules.newTimeWindowRule }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { provideZonelessChangeDetection } from '@angular/core';
|
||||
import { RulesPanel } from './rules-panel';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { RuleService, RuleType } from '../../services/rule-service';
|
||||
import { TranslationService } from '../../services/translation-service';
|
||||
|
||||
describe('RulesPanel', () => {
|
||||
let component: RulesPanel;
|
||||
@@ -19,6 +20,11 @@ describe('RulesPanel', () => {
|
||||
|
||||
fixture = TestBed.createComponent(RulesPanel);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
// Force language to 'fr' so the default rule names match French assertions
|
||||
const translationService = TestBed.inject(TranslationService);
|
||||
translationService.setLanguage('fr');
|
||||
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { Component, ChangeDetectionStrategy, inject, signal, Injectable } from '@angular/core';
|
||||
import {
|
||||
Component,
|
||||
ChangeDetectionStrategy,
|
||||
inject,
|
||||
signal,
|
||||
Injectable,
|
||||
effect,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
RuleService,
|
||||
RuleType,
|
||||
@@ -7,7 +14,8 @@ import {
|
||||
VacationRule,
|
||||
PlacementStrategy,
|
||||
} from '../../services/rule-service';
|
||||
import { SelectableDayType, tooltipTypeMapping, DayType } from '../../services/calendar-service';
|
||||
import { SelectableDayType } from '../../services/calendar-service';
|
||||
import { TranslationService } from '../../services/translation-service';
|
||||
import { MatExpansionModule } from '@angular/material/expansion';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
@@ -17,7 +25,7 @@ import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { DatePipe, LowerCasePipe } from '@angular/common';
|
||||
|
||||
import {
|
||||
DateAdapter,
|
||||
MAT_DATE_FORMATS,
|
||||
@@ -61,8 +69,6 @@ const MY_DATE_FORMATS = {
|
||||
MatSelectModule,
|
||||
MatDatepickerModule,
|
||||
FormsModule,
|
||||
DatePipe,
|
||||
LowerCasePipe,
|
||||
CdkDropList,
|
||||
CdkDrag,
|
||||
],
|
||||
@@ -77,30 +83,47 @@ const MY_DATE_FORMATS = {
|
||||
})
|
||||
export class RulesPanel {
|
||||
public ruleService = inject(RuleService);
|
||||
protected readonly translationService = inject(TranslationService);
|
||||
private readonly dateAdapter = inject(DateAdapter);
|
||||
|
||||
get tr() {
|
||||
return this.translationService.tr;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
this.dateAdapter.setLocale(this.translationService.locale());
|
||||
});
|
||||
}
|
||||
|
||||
protected readonly RuleType = RuleType;
|
||||
protected readonly placeDayTypes = Object.values(SelectableDayType);
|
||||
protected readonly rewardDayTypes = Object.values(SelectableDayType).filter(
|
||||
(t) => t !== SelectableDayType.ANY,
|
||||
);
|
||||
protected readonly tooltipTypeMapping = tooltipTypeMapping;
|
||||
|
||||
protected readonly daysOfWeek = [
|
||||
{ value: 1, label: 'Lundi' },
|
||||
{ value: 2, label: 'Mardi' },
|
||||
{ value: 3, label: 'Mercredi' },
|
||||
{ value: 4, label: 'Jeudi' },
|
||||
{ value: 5, label: 'Vendredi' },
|
||||
{ value: 6, label: 'Samedi' },
|
||||
{ value: 0, label: 'Dimanche' },
|
||||
];
|
||||
protected get daysOfWeek() {
|
||||
const tr = this.tr();
|
||||
return [
|
||||
{ value: 1, label: tr.rules.daysOfWeek[1] },
|
||||
{ value: 2, label: tr.rules.daysOfWeek[2] },
|
||||
{ value: 3, label: tr.rules.daysOfWeek[3] },
|
||||
{ value: 4, label: tr.rules.daysOfWeek[4] },
|
||||
{ value: 5, label: tr.rules.daysOfWeek[5] },
|
||||
{ value: 6, label: tr.rules.daysOfWeek[6] },
|
||||
{ value: 0, label: tr.rules.daysOfWeek[0] },
|
||||
];
|
||||
}
|
||||
|
||||
protected readonly placementOptions: { value: PlacementStrategy; label: string }[] = [
|
||||
{ value: 'before', label: ' Avant' },
|
||||
{ value: 'after', label: ' Après' },
|
||||
{ value: 'around', label: ' Autour' },
|
||||
{ value: 'auto', label: ' Auto (Meilleur choix)' },
|
||||
];
|
||||
protected get placementOptions(): { value: PlacementStrategy; label: string }[] {
|
||||
const tr = this.tr();
|
||||
return [
|
||||
{ value: 'before', label: tr.rules.placement.before },
|
||||
{ value: 'after', label: tr.rules.placement.after },
|
||||
{ value: 'around', label: tr.rules.placement.around },
|
||||
{ value: 'auto', label: tr.rules.placement.auto },
|
||||
];
|
||||
}
|
||||
|
||||
protected editingRuleId = signal<string | null>(null);
|
||||
protected isAddingNew = signal(false);
|
||||
@@ -161,7 +184,7 @@ export class RulesPanel {
|
||||
|
||||
if (type === RuleType.IF_THEN) {
|
||||
this.editBuffer = {
|
||||
name: 'Nouvelle Règle',
|
||||
name: this.tr().rules.defaultNewRuleName,
|
||||
type: RuleType.IF_THEN,
|
||||
dayOfWeek: 6,
|
||||
placeDays: 2,
|
||||
@@ -172,7 +195,7 @@ export class RulesPanel {
|
||||
} as Partial<IfThenRule>;
|
||||
} else {
|
||||
this.editBuffer = {
|
||||
name: 'Nouvelle Période',
|
||||
name: this.tr().rules.defaultNewPeriodName,
|
||||
type: RuleType.TIME_WINDOW,
|
||||
startDate: new Date(),
|
||||
endDate: new Date(new Date().getTime() + 7 * 24 * 60 * 60 * 1000),
|
||||
@@ -184,6 +207,44 @@ export class RulesPanel {
|
||||
|
||||
protected getDayTypeLabel(type: string | undefined | null): string {
|
||||
if (!type) return '';
|
||||
return tooltipTypeMapping[type as unknown as DayType] || String(type);
|
||||
return this.translationService.getDayTypeLabel(type);
|
||||
}
|
||||
|
||||
public getRuleSummary(rule: VacationRule): string {
|
||||
const tr = this.tr();
|
||||
if (rule.type === RuleType.IF_THEN) {
|
||||
const ifThen = rule as IfThenRule;
|
||||
const dayLabel = this.daysOfWeek.find((d) => d.value === ifThen.dayOfWeek)?.label ?? '';
|
||||
const placeTypeLabel = this.getDayTypeLabel(ifThen.placeType);
|
||||
const placementLabel = (
|
||||
this.placementOptions.find((p) => p.value === ifThen.placement)?.label ?? ''
|
||||
)
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
const rewardTypeLabel = this.getDayTypeLabel(ifThen.rewardType);
|
||||
|
||||
return tr.rules.ifThenTemplate
|
||||
.replace('{day}', dayLabel)
|
||||
.replace('{placeDays}', String(ifThen.placeDays))
|
||||
.replace('{placeType}', placeTypeLabel)
|
||||
.replace('{placement}', placementLabel)
|
||||
.replace('{rewardDays}', String(ifThen.rewardDays))
|
||||
.replace('{rewardType}', rewardTypeLabel);
|
||||
} else {
|
||||
const timeWindow = rule as TimeWindowRule;
|
||||
const placeTypeLabel = this.getDayTypeLabel(timeWindow.placeType);
|
||||
const startDateStr = new Date(timeWindow.startDate).toLocaleDateString(
|
||||
this.translationService.locale(),
|
||||
);
|
||||
const endDateStr = new Date(timeWindow.endDate).toLocaleDateString(
|
||||
this.translationService.locale(),
|
||||
);
|
||||
|
||||
return tr.rules.timeWindowTemplate
|
||||
.replace('{placeDays}', String(timeWindow.placeDays))
|
||||
.replace('{placeType}', placeTypeLabel)
|
||||
.replace('{startDate}', startDateStr)
|
||||
.replace('{endDate}', endDateStr);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user