feat: implement internationalization with multi-language support and runtime language selection

This commit is contained in:
nessar committed 2026-05-30 21:03:12 +02:00
1 parent 2de09383b9
commit 9c9114879d
26 files changed
+826 -159

No files matched your search

@@ -5,13 +5,9 @@
@if (calendarSettingsService.get('showWeekNumbers')()) {
<p class="day empty week-number"></p>
}
<p class="day">L</p>
<p class="day">M</p>
<p class="day">M</p>
<p class="day">J</p>
<p class="day">V</p>
<p class="day">S</p>
<p class="day">D</p>
@for (letter of getWeekdayLetters(); track $index) {
<p class="day">{{ letter }}</p>
}
</div>
<div class="cols">
@for (week of weeksInAMonth; track $index) {
@@ -36,7 +32,7 @@
[class.selected]="selectedDates.isSelected(day)"
[class]="selectedDates.getClasses(day)"
class="day number"
[matTooltip]="selectedDates.getTooltip(day)"
[matTooltip]="getDayTooltip(day)"
[matTooltipShowDelay]="500"
>
{{ day.getDate() }}
@@ -1,8 +1,9 @@
import { Component, Input, OnInit, inject } from '@angular/core';
import { CalendarService, Month, monthNames, SelectedDates } from '../../services/calendar-service';
import { CalendarService, Month, SelectedDates } from '../../services/calendar-service';
import { MatTooltip } from '@angular/material/tooltip';
import { CalendarSettingsService } from '../../services/calendar-settings-service';
import { DateCacheService } from '../../services/date-cache.service';
import { TranslationService } from '../../services/translation-service';
@Component({
selector: 'app-calendar-month',
@@ -13,6 +14,7 @@ import { DateCacheService } from '../../services/date-cache.service';
export class CalendarMonth implements OnInit {
protected calendarSettingsService = inject(CalendarSettingsService);
private dateCache = inject(DateCacheService);
protected translationService = inject(TranslationService);
@Input({ required: true })
monthIndex!: number;
@@ -20,6 +22,10 @@ export class CalendarMonth implements OnInit {
@Input({ required: true })
year!: number;
get tr() {
return this.translationService.tr;
}
// Use DateCacheService instead of creating Date objects
getNow(): Date {
return this.dateCache.getNow();
@@ -33,7 +39,24 @@ export class CalendarMonth implements OnInit {
selectedDates!: SelectedDates;
getMonthName(): string {
return monthNames[this.monthIndex];
const locale = this.translationService.locale();
const tempDate = new Date(this.year, this.monthIndex, 1);
const monthName = tempDate.toLocaleString(locale, { month: 'long' });
return monthName.charAt(0).toUpperCase() + monthName.slice(1);
}
getWeekdayLetters(): string[] {
// January 5, 2026 is a Monday
const baseDate = new Date(2026, 0, 5);
const locale = this.translationService.locale();
const letters: string[] = [];
for (let i = 0; i < 7; i++) {
const date = new Date(baseDate);
date.setDate(baseDate.getDate() + i);
const letter = date.toLocaleString(locale, { weekday: 'narrow' });
letters.push(letter.toUpperCase());
}
return letters;
}
getWeekNumber(week: (Date | null)[]): number | null {
@@ -45,6 +68,13 @@ export class CalendarMonth implements OnInit {
return null;
}
getDayTooltip(day: Date): string | null {
const tooltips = this.selectedDates.datesSelected
.filter((dateSelected) => dateSelected.isSelected(day) && dateSelected.type)
.map((dateSelected) => this.translationService.getDayTypeLabel(dateSelected.type!));
return tooltips.length > 0 ? tooltips.join(' + ') : null;
}
ngOnInit(): void {
this.weeksInAMonth = CalendarService.weeksInMonth(this.monthIndex, this.year);
}
@@ -1,4 +1,4 @@
<button matFab extended (click)="openDialog()">
<mat-icon>settings</mat-icon>
Calendar Settings
{{ tr().settings.title }}
</button>
@@ -1,9 +1,10 @@
import { Component } from '@angular/core';
import { Component, inject } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { CalendarSettingsDialog } from '../dialog/calendar-settings-dialog/calendar-settings-dialog';
import { MatDividerModule } from '@angular/material/divider';
import { TranslationService } from '../../services/translation-service';
@Component({
selector: 'app-calendar-settings',
@@ -12,6 +13,12 @@ import { MatDividerModule } from '@angular/material/divider';
styleUrl: './calendar-settings.scss',
})
export class CalendarSettings {
private readonly translationService = inject(TranslationService);
get tr() {
return this.translationService.tr;
}
constructor(private dialog: MatDialog) {}
openDialog() {
@@ -6,7 +6,7 @@
<mat-spinner [mode]="'determinate'" [value]="loadingProgress()"></mat-spinner>
<div class="spinner-text">{{ loadingProgress() }}%</div>
</div>
<p>Calcul en cours...</p>
<p>{{ tr().loading }}</p>
</div>
</div>
}
@@ -3,10 +3,10 @@ import { UserInputService } from '../../services/user-input-service';
import { MatCard, MatCardContent } from '@angular/material/card';
import { CalendarMonth } from '../calendar-month/calendar-month';
import { CalendarService, SelectedDates } from '../../services/calendar-service';
import { MatProgressSpinner } from '@angular/material/progress-spinner';
import { DateCacheService } from '../../services/date-cache.service';
import { RuleService } from '../../services/rule-service';
import { TranslationService } from '../../services/translation-service';
@Component({
selector: 'app-calendar-year',
@@ -19,8 +19,14 @@ export class CalendarYear {
private userInput = inject(UserInputService);
private calendarService = inject(CalendarService);
private ruleService = inject(RuleService);
protected readonly translationService = inject(TranslationService);
get tr() {
return this.translationService.tr;
}
protected isLoading = signal(true);
protected loadingProgress = signal(0);
// Use DateCacheService instead of creating Date objects
@@ -1,21 +1,37 @@
<div class="container">
<div>
<h1>Calendar Settings</h1>
<p>Configure your calendar settings here.</p>
<div class="container" style="min-width: 280px; padding: 8px">
<div style="margin-bottom: 1.5rem; text-align: center">
<h1 style="margin: 0 0 0.5rem 0; font-size: 1.5rem">{{ tr().settings.title }}</h1>
<p style="margin: 0; opacity: 0.7; font-size: 0.9rem">{{ tr().settings.description }}</p>
</div>
<div style="display: flex; flex-direction: column; gap: 1rem">
<div style="display: flex; flex-direction: column; gap: 1rem; width: 100%">
@for (setting of calendarSettingsService.settingsArray; track setting.id) {
<div style="align-self: flex-start">
<div style="align-self: flex-start; margin-bottom: 0.5rem">
<mat-slide-toggle
[id]="setting.id"
[name]="setting.id"
[checked]="setting.signal()"
(change)="setting.signal.set($event.checked)"
>
{{ setting.description }}
{{ getSettingDescription(setting.id) }}
</mat-slide-toggle>
</div>
}
<mat-form-field appearance="outline" style="width: 100%; margin-top: 0.5rem">
<mat-label>{{ tr().settings.selectLanguage }}</mat-label>
<mat-select
[value]="translationService.language()"
(valueChange)="translationService.setLanguage($event)"
>
@for (lang of translationService.availableLanguages; track lang) {
<mat-option [value]="lang">{{
translationService.getLanguageName(lang)
}}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<button mat-button (click)="closeDialog()">Fermer</button>
<button mat-button (click)="closeDialog()" style="margin-top: 1.5rem; width: 100%">
{{ tr().settings.close }}
</button>
</div>
@@ -3,16 +3,32 @@ import { MatButton } from '@angular/material/button';
import { MatDialogRef } from '@angular/material/dialog';
import { CalendarSettingsService } from '../../../services/calendar-settings-service';
import { MatSlideToggle } from '@angular/material/slide-toggle';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatSelectModule } from '@angular/material/select';
import { TranslationService } from '../../../services/translation-service';
@Component({
selector: 'app-calendar-settings-dialog',
imports: [MatButton, MatSlideToggle],
imports: [MatButton, MatSlideToggle, MatFormFieldModule, MatSelectModule],
templateUrl: './calendar-settings-dialog.html',
styleUrl: './calendar-settings-dialog.scss',
})
export class CalendarSettingsDialog {
public readonly dialogRef = inject<MatDialogRef<CalendarSettingsDialog>>(MatDialogRef);
protected readonly calendarSettingsService = inject(CalendarSettingsService);
protected readonly translationService = inject(TranslationService);
get tr() {
return this.translationService.tr;
}
getSettingDescription(id: string): string {
const tr = this.tr();
if (id === 'showWeekNumbers') {
return tr.settings.showWeekNumbers;
}
return id;
}
closeDialog() {
this.dialogRef.close();
+42 -72
View File
@@ -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();
});
+85 -24
View File
@@ -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);
}
}
}
+14 -7
View File
@@ -1,5 +1,5 @@
<div class="user-input">
<p>Optimisez vos jours de vacances en fonction des jours fériés et des week-ends !</p>
<p>{{ tr().app.description }}</p>
<div class="listOfDays">
@for (day of daysOff; track day.id) {
<div class="day-input-wrapper" animate.enter="day-enter" animate.leave="day-leave">
@@ -11,16 +11,16 @@
</div>
}
<mat-card class="add-day-card mat-elevation-z2" (click)="addDay()">
<button matFab aria-label="Add entry">
<button matFab [aria-label]="tr().userInput.addVacationDay">
<mat-icon>add</mat-icon>
</button>
<h3>Ajouter un jour de vacances</h3>
<h3>{{ tr().userInput.addVacationDay }}</h3>
</mat-card>
</div>
<div class="action-buttons">
<button matFab extended (click)="triggerComputation()">
<mat-icon>calculate</mat-icon>
Calculer
{{ tr().userInput.calculate }}
</button>
<app-calendar-settings></app-calendar-settings>
</div>
@@ -31,13 +31,20 @@
<mat-icon color="warn">warning</mat-icon>
<div class="warning">
<span>
{{ remainingDays() }} jour{{ remainingDays() > 1 ? 's' : '' }} de vacances
non placé{{ remainingDays() > 1 ? 's' : '' }}:
{{
tr()
.userInput.remainingDaysWarning.replace(
'{count}',
remainingDays().toString()
)
.split('{plural}')
.join(remainingDays() > 1 ? 's' : '')
}}
</span>
@for (day of remainingDaysDetails(); track $index) {
<span class="remaining-day-detail">- {{ day }}</span>
}
<span> Veuillez les placer manuellement. </span>
<span> {{ tr().userInput.remainingDaysManualPlace }} </span>
</div>
</mat-card-content>
</mat-card>
+18 -8
View File
@@ -7,7 +7,8 @@ import { MatCard, MatCardContent } from '@angular/material/card';
import { MatIcon } from '@angular/material/icon';
import { DayInput } from '../userInput/day-input/day-input';
import { MatButtonModule } from '@angular/material/button';
import { tooltipTypeMapping, DayType, VacDay } from '../../services/calendar-service';
import { VacDay } from '../../services/calendar-service';
import { TranslationService } from '../../services/translation-service';
@Component({
selector: 'app-user-input',
@@ -27,6 +28,11 @@ import { tooltipTypeMapping, DayType, VacDay } from '../../services/calendar-ser
})
export class UserInput {
private userInputService: UserInputService = inject(UserInputService);
protected readonly translationService = inject(TranslationService);
get tr() {
return this.translationService.tr;
}
protected remainingDays = computed(() => {
const remaining = this.userInputService.remainingVacationDaysSignal();
@@ -38,13 +44,17 @@ export class UserInput {
return remaining
.filter((day) => day.numberOfDays > 0)
.map((day) => {
const typeName = tooltipTypeMapping[day.type as unknown as DayType];
const expiryDate = day.expiryDate.toLocaleDateString('fr-FR', {
day: 'numeric',
month: 'short',
year: 'numeric',
});
return `${day.numberOfDays} ${typeName} (exp. ${expiryDate})`;
const typeName = this.translationService.getDayTypeLabel(day.type);
const expiryDate = day.expiryDate.toLocaleDateString(
this.translationService.locale(),
{
day: 'numeric',
month: 'short',
year: 'numeric',
},
);
const expiresLabel = this.tr().userInput.expires;
return `${day.numberOfDays} ${typeName} (${expiresLabel} ${expiryDate})`;
});
});
@@ -13,12 +13,14 @@
<mat-select (valueChange)="onTypeChange($event)" [value]="day().type">
<mat-option></mat-option>
@for (dayType of selectableDayTypes; track $index) {
<mat-option [value]="dayType">{{ tooltipTypeMapping[dayType] }}</mat-option>
<mat-option [value]="dayType">{{
translationService.getDayTypeLabel(dayType)
}}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field class="dateInput">
<mat-label>Choose a date</mat-label>
<mat-label>{{ tr().userInput.chooseDate }}</mat-label>
<input
matInput
type="datetime"
@@ -1,5 +1,14 @@
import { Component, input, output, ChangeDetectionStrategy, Injectable } from '@angular/core';
import { SelectableDayType, tooltipTypeMapping, VacDay } from '../../../services/calendar-service';
import {
Component,
input,
output,
ChangeDetectionStrategy,
Injectable,
inject,
effect,
} from '@angular/core';
import { SelectableDayType, VacDay } from '../../../services/calendar-service';
import { TranslationService } from '../../../services/translation-service';
import { MatCard } from '@angular/material/card';
import { MatOption, MatSelect } from '@angular/material/select';
import { MatInput, MatInputModule, MatLabel } from '@angular/material/input';
@@ -86,10 +95,22 @@ export class DayInput {
public dayChange = output<VacDay>();
public deleteEntry = output<void>();
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 selectableDayTypes = Object.values(SelectableDayType).filter(
(t) => t !== SelectableDayType.ANY,
);
protected readonly tooltipTypeMapping = tooltipTypeMapping;
onInput(event: Event) {
const value = Number((event.target as HTMLInputElement).value);