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
@@ -41,3 +41,6 @@ __screenshots__/
|
||||
# System files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Auto-generated i18n files
|
||||
/src/app/i18n-registry.ts
|
||||
+1
-1
@@ -14,7 +14,7 @@ variables:
|
||||
cache:
|
||||
key:
|
||||
files:
|
||||
- bun.lockb
|
||||
- bun.lock
|
||||
paths:
|
||||
- .bun/
|
||||
|
||||
|
||||
+2
-2
@@ -8,7 +8,7 @@ RUN apk add --no-cache nodejs
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package.json bun.lockb* ./
|
||||
COPY package.json bun.lock* ./
|
||||
|
||||
# Install dependencies
|
||||
RUN bun install
|
||||
@@ -26,7 +26,7 @@ FROM oven/bun:alpine AS production
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package.json bun.lockb* ./
|
||||
COPY package.json bun.lock* ./
|
||||
|
||||
# Install only production dependencies
|
||||
RUN bun install --production
|
||||
|
||||
@@ -7,7 +7,7 @@ This project was generated using [Angular CLI](https://github.com/angular/angula
|
||||
To start a local development server, run:
|
||||
|
||||
```bash
|
||||
ng serve
|
||||
bun start
|
||||
```
|
||||
|
||||
Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files.
|
||||
@@ -21,13 +21,13 @@ This project uses ESLint and Prettier to maintain code quality and consistency.
|
||||
Check for linting errors:
|
||||
|
||||
```bash
|
||||
npm run lint
|
||||
bun run lint
|
||||
```
|
||||
|
||||
Auto-fix linting issues:
|
||||
|
||||
```bash
|
||||
npm run lint:fix
|
||||
bun run lint:fix
|
||||
```
|
||||
|
||||
### Formatting
|
||||
@@ -35,13 +35,13 @@ npm run lint:fix
|
||||
Check code formatting:
|
||||
|
||||
```bash
|
||||
npm run format:check
|
||||
bun run format:check
|
||||
```
|
||||
|
||||
Format code:
|
||||
|
||||
```bash
|
||||
npm run format
|
||||
bun run format
|
||||
```
|
||||
|
||||
## Code scaffolding
|
||||
@@ -63,17 +63,17 @@ ng generate --help
|
||||
To build the project run:
|
||||
|
||||
```bash
|
||||
ng build
|
||||
bun run build
|
||||
```
|
||||
|
||||
This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
|
||||
To execute unit tests, use the following command:
|
||||
|
||||
```bash
|
||||
ng test
|
||||
bun run test
|
||||
```
|
||||
|
||||
## Running end-to-end tests
|
||||
|
||||
+9
-9
@@ -3,15 +3,15 @@
|
||||
"version": "1.5.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test",
|
||||
"serve:ssr:vac-optimizer": "node dist/vac-optimizer/server/server.mjs",
|
||||
"lint": "eslint \"src/**/*.ts\"",
|
||||
"lint:fix": "eslint \"src/**/*.ts\" --fix",
|
||||
"format": "prettier --write \"src/**/*.{ts,html,scss,css,json}\"",
|
||||
"format:check": "prettier --check \"src/**/*.{ts,html,scss,css,json}\""
|
||||
"start": "bun scripts/generate-langs.js && ng serve",
|
||||
"build": "bun scripts/generate-langs.js && ng build",
|
||||
"watch": "bun scripts/generate-langs.js && ng build --watch --configuration development",
|
||||
"test": "bun scripts/generate-langs.js && ng test",
|
||||
"serve:ssr:vac-optimizer": "bun dist/vac-optimizer/server/server.mjs",
|
||||
"lint": "bun scripts/generate-langs.js && eslint \"src/**/*.ts\"",
|
||||
"lint:fix": "bun scripts/generate-langs.js && eslint \"src/**/*.ts\" --fix",
|
||||
"format": "bun scripts/generate-langs.js && prettier --write \"src/**/*.{ts,html,scss,css,json}\"",
|
||||
"format:check": "bun scripts/generate-langs.js && prettier --check \"src/**/*.{ts,html,scss,css,json}\""
|
||||
},
|
||||
"prettier": {
|
||||
"printWidth": 100,
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const i18nDir = path.join(__dirname, '../src/i18n');
|
||||
const registryFile = path.join(__dirname, '../src/app/i18n-registry.ts');
|
||||
|
||||
if (!fs.existsSync(i18nDir)) {
|
||||
console.error(`i18n directory not found at ${i18nDir}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(i18nDir).filter((file) => file.endsWith('.json'));
|
||||
|
||||
let importStatements = '';
|
||||
let registryEntries = '';
|
||||
|
||||
files.forEach((file) => {
|
||||
const langCode = path.basename(file, '.json');
|
||||
importStatements += `import ${langCode} from '../i18n/${file}';\n`;
|
||||
registryEntries += `\t${langCode}: ${langCode} as AppTranslations,\n`;
|
||||
});
|
||||
|
||||
const content = `// This file is auto-generated by scripts/generate-langs.js. Do not edit directly.
|
||||
import { AppTranslations } from './services/translation-service';
|
||||
${importStatements}
|
||||
export const translationsMap: Record<string, AppTranslations> = {
|
||||
${registryEntries}};
|
||||
|
||||
export const availableLanguagesList: string[] = Object.keys(translationsMap);
|
||||
`;
|
||||
|
||||
fs.writeFileSync(registryFile, content, 'utf8');
|
||||
console.log(`Successfully generated i18n registry at ${registryFile} with ${files.length} languages.`);
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
/// <reference types="vite/client" />
|
||||
import { Injectable, inject, Injector, computed } from '@angular/core';
|
||||
import { storedSignal } from '../shared/utils/stored-signal';
|
||||
|
||||
export interface AppTranslations {
|
||||
locale: string;
|
||||
app: {
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
userInput: {
|
||||
addVacationDay: string;
|
||||
calculate: string;
|
||||
remainingDaysWarning: string;
|
||||
remainingDaysManualPlace: string;
|
||||
chooseDate: string;
|
||||
daySuffix: string;
|
||||
daysSuffix: string;
|
||||
expires: string;
|
||||
};
|
||||
dayTypes: {
|
||||
weekend: string;
|
||||
closedDay: string;
|
||||
rtt: string;
|
||||
cp: string;
|
||||
other: string;
|
||||
today: string;
|
||||
any: string;
|
||||
};
|
||||
settings: {
|
||||
title: string;
|
||||
description: string;
|
||||
showWeekNumbers: string;
|
||||
close: string;
|
||||
selectLanguage: string;
|
||||
};
|
||||
rules: {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
priorityInfo: string;
|
||||
ifThenTemplate: string;
|
||||
timeWindowTemplate: string;
|
||||
editRule: string;
|
||||
addRule: string;
|
||||
place: string;
|
||||
ruleName: string;
|
||||
holidayFalls: string;
|
||||
days: string;
|
||||
type: string;
|
||||
position: string;
|
||||
getBonus: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
cancel: string;
|
||||
save: string;
|
||||
add: string;
|
||||
addRuleButton: string;
|
||||
newIfThenRule: string;
|
||||
newTimeWindowRule: string;
|
||||
defaultNewRuleName: string;
|
||||
defaultNewPeriodName: string;
|
||||
daysOfWeek: {
|
||||
1: string;
|
||||
2: string;
|
||||
3: string;
|
||||
4: string;
|
||||
5: string;
|
||||
6: string;
|
||||
0: string;
|
||||
};
|
||||
placement: {
|
||||
before: string;
|
||||
after: string;
|
||||
around: string;
|
||||
auto: string;
|
||||
};
|
||||
};
|
||||
loading: string;
|
||||
weekdaysShort: string[];
|
||||
}
|
||||
|
||||
import { translationsMap, availableLanguagesList } from '../i18n-registry';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class TranslationService {
|
||||
private readonly injector = inject(Injector);
|
||||
|
||||
public readonly availableLanguages = availableLanguagesList;
|
||||
|
||||
// Detect browser language, fallback to the first available language
|
||||
private getBrowserLanguage(): string {
|
||||
if (typeof navigator !== 'undefined' && navigator.language) {
|
||||
const primary = navigator.language.split('-')[0].toLowerCase();
|
||||
if (this.availableLanguages.includes(primary)) {
|
||||
return primary;
|
||||
}
|
||||
}
|
||||
return this.availableLanguages[0] || '';
|
||||
}
|
||||
|
||||
public readonly language = storedSignal<string>(
|
||||
'app.language',
|
||||
this.getBrowserLanguage(),
|
||||
this.injector,
|
||||
);
|
||||
|
||||
public readonly tr = computed<AppTranslations>(() => {
|
||||
const currentLang = this.language();
|
||||
const fallbackLang = this.availableLanguages[0] || '';
|
||||
return (
|
||||
(translationsMap[currentLang] as AppTranslations) ||
|
||||
(translationsMap[fallbackLang] as AppTranslations) ||
|
||||
({} as AppTranslations)
|
||||
);
|
||||
});
|
||||
|
||||
public readonly locale = computed<string>(() => {
|
||||
const tr = this.tr();
|
||||
if (tr && tr.locale) {
|
||||
return tr.locale;
|
||||
}
|
||||
const lang = this.language();
|
||||
if (lang) {
|
||||
return `${lang}-${lang.toUpperCase()}`;
|
||||
}
|
||||
const fallbackLang = this.availableLanguages[0];
|
||||
if (fallbackLang) {
|
||||
return `${fallbackLang}-${fallbackLang.toUpperCase()}`;
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
public setLanguage(lang: string): void {
|
||||
if (this.availableLanguages.includes(lang)) {
|
||||
this.language.set(lang);
|
||||
}
|
||||
}
|
||||
|
||||
public getLanguageName(langCode: string): string {
|
||||
try {
|
||||
// Using native Intl.DisplayNames to get name of language in its own language
|
||||
const displayNames = new Intl.DisplayNames([langCode], { type: 'language' });
|
||||
const name = displayNames.of(langCode);
|
||||
if (name) {
|
||||
return name.charAt(0).toUpperCase() + name.slice(1);
|
||||
}
|
||||
} catch {
|
||||
// Fallback to uppercase language code
|
||||
}
|
||||
return langCode.toUpperCase();
|
||||
}
|
||||
|
||||
public getDayTypeLabel(type: string): string {
|
||||
const tr = this.tr();
|
||||
if (!tr || !tr.dayTypes) {
|
||||
return type;
|
||||
}
|
||||
switch (type) {
|
||||
case 'weekend':
|
||||
return tr.dayTypes.weekend || type;
|
||||
case 'closed-day':
|
||||
return tr.dayTypes.closedDay || type;
|
||||
case 'rtt':
|
||||
return tr.dayTypes.rtt || type;
|
||||
case 'cp':
|
||||
return tr.dayTypes.cp || type;
|
||||
case 'other':
|
||||
return tr.dayTypes.other || type;
|
||||
case 'today':
|
||||
return tr.dayTypes.today || type;
|
||||
case 'any':
|
||||
return tr.dayTypes.any || type;
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"locale": "de-DE",
|
||||
"app": {
|
||||
"title": "vac-optimizer",
|
||||
"description": "Optimieren Sie Ihre Urlaubstage basierend auf Feiertagen und Wochenenden!"
|
||||
},
|
||||
"userInput": {
|
||||
"addVacationDay": "Urlaubstag hinzufügen",
|
||||
"calculate": "Berechnen",
|
||||
"remainingDaysWarning": "{count} nicht platziert:",
|
||||
"remainingDaysManualPlace": "Bitte manuell platzieren.",
|
||||
"chooseDate": "Datum auswählen",
|
||||
"daySuffix": "Tag",
|
||||
"daysSuffix": "Tage",
|
||||
"expires": "abl."
|
||||
},
|
||||
"dayTypes": {
|
||||
"weekend": "Wochenende",
|
||||
"closedDay": "Feiertag",
|
||||
"rtt": "RTT",
|
||||
"cp": "CP",
|
||||
"other": "Andere",
|
||||
"today": "Heute",
|
||||
"any": "Beliebig"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Einstellungen",
|
||||
"description": "Einstellungen anpassen",
|
||||
"showWeekNumbers": "Wochennummern anzeigen",
|
||||
"close": "Schließen",
|
||||
"selectLanguage": "Sprache auswählen"
|
||||
},
|
||||
"rules": {
|
||||
"title": "Regeln",
|
||||
"subtitle": "Urlaubsregeln konfigurieren",
|
||||
"priorityInfo": "Regeln werden von oben nach unten angewendet",
|
||||
"ifThenTemplate": "Wenn {type} dann {placement}",
|
||||
"timeWindowTemplate": "Zeitfenster {startDate} bis {endDate}",
|
||||
"editRule": "Regel bearbeiten",
|
||||
"addRule": "Regel hinzufügen",
|
||||
"place": "platzieren",
|
||||
"ruleName": "Regelname",
|
||||
"holidayFalls": "Feiertag fällt auf",
|
||||
"days": "Tage",
|
||||
"type": "Typ",
|
||||
"position": "Position",
|
||||
"getBonus": "Bonus erhalten",
|
||||
"startDate": "Startdatum",
|
||||
"endDate": "Enddatum",
|
||||
"cancel": "Abbrechen",
|
||||
"save": "Speichern",
|
||||
"add": "Hinzufügen",
|
||||
"addRuleButton": "Regel hinzufügen",
|
||||
"newIfThenRule": "Neue Wenn-Dann-Regel",
|
||||
"newTimeWindowRule": "Neue Zeitfenster-Regel",
|
||||
"defaultNewRuleName": "Neue Regel",
|
||||
"defaultNewPeriodName": "Neuer Zeitraum",
|
||||
"daysOfWeek": {
|
||||
"1": "Montag",
|
||||
"2": "Dienstag",
|
||||
"3": "Mittwoch",
|
||||
"4": "Donnerstag",
|
||||
"5": "Freitag",
|
||||
"6": "Samstag",
|
||||
"0": "Sonntag"
|
||||
},
|
||||
"placement": {
|
||||
"before": "davor",
|
||||
"after": "danach",
|
||||
"around": "darum herum",
|
||||
"auto": "automatisch"
|
||||
}
|
||||
},
|
||||
"loading": "Wird geladen...",
|
||||
"weekdaysShort": ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"]
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"locale": "en-GB",
|
||||
"app": {
|
||||
"title": "vac-optimizer",
|
||||
"description": "Optimize your vacation days based on holidays and weekends!"
|
||||
},
|
||||
"userInput": {
|
||||
"addVacationDay": "Add a vacation day",
|
||||
"calculate": "Calculate",
|
||||
"remainingDaysWarning": "{count} vacation day{plural} not placed:",
|
||||
"remainingDaysManualPlace": "Please place them manually.",
|
||||
"chooseDate": "Choose a date",
|
||||
"daySuffix": "day",
|
||||
"daysSuffix": "days",
|
||||
"expires": "exp."
|
||||
},
|
||||
"dayTypes": {
|
||||
"weekend": "Weekend",
|
||||
"closedDay": "Public Holiday",
|
||||
"rtt": "RTT",
|
||||
"cp": "Paid Leave",
|
||||
"other": "Other",
|
||||
"today": "Today",
|
||||
"any": "Any"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Calendar Settings",
|
||||
"description": "Configure your calendar settings here.",
|
||||
"showWeekNumbers": "Show week numbers",
|
||||
"close": "Close",
|
||||
"selectLanguage": "Language"
|
||||
},
|
||||
"rules": {
|
||||
"title": "Custom Rules",
|
||||
"subtitle": "Manage optimization rules",
|
||||
"priorityInfo": "The order of rules defines their execution priority (from top to bottom). Drag and drop to reorder.",
|
||||
"ifThenTemplate": "If a holiday falls on a <strong>{day}</strong>, place <strong>{placeDays}</strong> day(s) <strong>{placeType}</strong> <strong>{placement}</strong> and get <strong>{rewardDays}</strong> day(s) <strong>{rewardType}</strong> as a bonus.",
|
||||
"timeWindowTemplate": "Place <strong>{placeDays}</strong> day(s) <strong>{placeType}</strong> between <strong>{startDate}</strong> and <strong>{endDate}</strong>.",
|
||||
"editRule": "Edit rule",
|
||||
"addRule": "Add a rule",
|
||||
"place": "Place",
|
||||
"ruleName": "Rule name",
|
||||
"holidayFalls": "Holiday falls on a",
|
||||
"days": "Days",
|
||||
"type": "Type",
|
||||
"position": "Position",
|
||||
"getBonus": "Get as bonus",
|
||||
"startDate": "Start date",
|
||||
"endDate": "End date",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"add": "Add",
|
||||
"addRuleButton": "Add Rule",
|
||||
"newIfThenRule": "New \"If ... Then\" Rule",
|
||||
"newTimeWindowRule": "New Imposed Period",
|
||||
"defaultNewRuleName": "New Rule",
|
||||
"defaultNewPeriodName": "New Period",
|
||||
"daysOfWeek": {
|
||||
"1": "Monday",
|
||||
"2": "Tuesday",
|
||||
"3": "Wednesday",
|
||||
"4": "Thursday",
|
||||
"5": "Friday",
|
||||
"6": "Saturday",
|
||||
"0": "Sunday"
|
||||
},
|
||||
"placement": {
|
||||
"before": "Before",
|
||||
"after": "After",
|
||||
"around": "Around",
|
||||
"auto": "Auto (Best choice)"
|
||||
}
|
||||
},
|
||||
"loading": "Calculating...",
|
||||
"weekdaysShort": ["M", "T", "W", "T", "F", "S", "S"]
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"locale": "es-ES",
|
||||
"app": {
|
||||
"title": "vac-optimizer",
|
||||
"description": "¡Optimice sus días de vacaciones según los días festivos y los fines de semana!"
|
||||
},
|
||||
"userInput": {
|
||||
"addVacationDay": "Añadir un día de vacaciones",
|
||||
"calculate": "Calcular",
|
||||
"remainingDaysWarning": "{count} día{plural} de vacaciones no colocado{plural}:",
|
||||
"remainingDaysManualPlace": "Por favor, colóquelos manualmente.",
|
||||
"chooseDate": "Elija una fecha",
|
||||
"daySuffix": "día",
|
||||
"daysSuffix": "días",
|
||||
"expires": "exp."
|
||||
},
|
||||
"dayTypes": {
|
||||
"weekend": "Fin de semana",
|
||||
"closedDay": "Día festivo",
|
||||
"rtt": "RTT",
|
||||
"cp": "Vacaciones pagadas",
|
||||
"other": "Otro",
|
||||
"today": "Hoy",
|
||||
"any": "Cualquier"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Ajustes del calendario",
|
||||
"description": "Configure sus ajustes del calendario aquí.",
|
||||
"showWeekNumbers": "Mostrar números de semana",
|
||||
"close": "Cerrar",
|
||||
"selectLanguage": "Idioma"
|
||||
},
|
||||
"rules": {
|
||||
"title": "Reglas personalizadas",
|
||||
"subtitle": "Gestionar reglas de optimización",
|
||||
"priorityInfo": "El orden de las reglas define su prioridad de ejecución (de arriba a abajo). Arrastre y suelte para reorganizar.",
|
||||
"ifThenTemplate": "Si un día festivo cae en <strong>{day}</strong>, colocar <strong>{placeDays}</strong> día(s) <strong>{placeType}</strong> <strong>{placement}</strong> y obtener <strong>{rewardDays}</strong> día(s) <strong>{rewardType}</strong> de bonificación.",
|
||||
"timeWindowTemplate": "Colocar <strong>{placeDays}</strong> día(s) <strong>{placeType}</strong> entre el <strong>{startDate}</strong> y el <strong>{endDate}</strong>.",
|
||||
"editRule": "Editar regla",
|
||||
"addRule": "Añadir una regla",
|
||||
"place": "Colocar",
|
||||
"ruleName": "Nombre de la regla",
|
||||
"holidayFalls": "Día festivo cae en",
|
||||
"days": "Días",
|
||||
"type": "Tipo",
|
||||
"position": "Posición",
|
||||
"getBonus": "Obtener en bonificación",
|
||||
"startDate": "Fecha de inicio",
|
||||
"endDate": "Fecha de fin",
|
||||
"cancel": "Cancelar",
|
||||
"save": "Guardar",
|
||||
"add": "Añadir",
|
||||
"addRuleButton": "Añadir",
|
||||
"newIfThenRule": "Nueva regla \"Si ... Entonces\"",
|
||||
"newTimeWindowRule": "Nuevo período impuesto",
|
||||
"defaultNewRuleName": "Nueva regla",
|
||||
"defaultNewPeriodName": "Nuevo período",
|
||||
"daysOfWeek": {
|
||||
"1": "Lunes",
|
||||
"2": "Martes",
|
||||
"3": "Miércoles",
|
||||
"4": "Jueves",
|
||||
"5": "Viernes",
|
||||
"6": "Sábado",
|
||||
"0": "Domingo"
|
||||
},
|
||||
"placement": {
|
||||
"before": "Antes",
|
||||
"after": "Después",
|
||||
"around": "Alrededor",
|
||||
"auto": "Auto (Mejor opción)"
|
||||
}
|
||||
},
|
||||
"loading": "Calculando...",
|
||||
"weekdaysShort": ["L", "M", "M", "J", "V", "S", "D"]
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"locale": "fr-FR",
|
||||
"app": {
|
||||
"title": "vac-optimizer",
|
||||
"description": "Optimisez vos jours de vacances en fonction des jours fériés et des week-ends !"
|
||||
},
|
||||
"userInput": {
|
||||
"addVacationDay": "Ajouter un jour de vacances",
|
||||
"calculate": "Calculer",
|
||||
"remainingDaysWarning": "{count} jour{plural} de vacances non placé{plural}:",
|
||||
"remainingDaysManualPlace": "Veuillez les placer manuellement.",
|
||||
"chooseDate": "Choisissez une date",
|
||||
"daySuffix": "jour",
|
||||
"daysSuffix": "jours",
|
||||
"expires": "exp."
|
||||
},
|
||||
"dayTypes": {
|
||||
"weekend": "Weekend",
|
||||
"closedDay": "Jour Férié",
|
||||
"rtt": "RTT",
|
||||
"cp": "Congé Payé",
|
||||
"other": "Autre",
|
||||
"today": "Aujourd'hui",
|
||||
"any": "N'importe"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Paramètres du calendrier",
|
||||
"description": "Configurez vos paramètres de calendrier ici.",
|
||||
"showWeekNumbers": "Afficher les numéros de semaine",
|
||||
"close": "Fermer",
|
||||
"selectLanguage": "Langue"
|
||||
},
|
||||
"rules": {
|
||||
"title": "Règles Personnalisées",
|
||||
"subtitle": "Gérer les règles d'optimisation",
|
||||
"priorityInfo": "L'ordre des règles définit leur priorité d'exécution (de haut en bas). Glissez-déposez pour réorganiser.",
|
||||
"ifThenTemplate": "Si un jour férié tombe un <strong>{day}</strong>, placer <strong>{placeDays}</strong> jour(s) <strong>{placeType}</strong> <strong>{placement}</strong> et obtenir <strong>{rewardDays}</strong> jour(s) <strong>{rewardType}</strong> en bonus.",
|
||||
"timeWindowTemplate": "Placer <strong>{placeDays}</strong> jour(s) <strong>{placeType}</strong> entre le <strong>{startDate}</strong> et le <strong>{endDate}</strong>.",
|
||||
"editRule": "Modifier la règle",
|
||||
"addRule": "Ajouter une règle",
|
||||
"place": "Placer",
|
||||
"ruleName": "Nom de la règle",
|
||||
"holidayFalls": "Jour férié tombe un",
|
||||
"days": "Jours",
|
||||
"type": "Type",
|
||||
"position": "Position",
|
||||
"getBonus": "Obtenir en bonus",
|
||||
"startDate": "Date de début",
|
||||
"endDate": "Date de fin",
|
||||
"cancel": "Annuler",
|
||||
"save": "Enregistrer",
|
||||
"add": "Ajouter",
|
||||
"addRuleButton": "Ajouter",
|
||||
"newIfThenRule": "Nouvelle Règle \"Si ... Alors\"",
|
||||
"newTimeWindowRule": "Nouvelle Période Imposée",
|
||||
"defaultNewRuleName": "Nouvelle Règle",
|
||||
"defaultNewPeriodName": "Nouvelle Période",
|
||||
"daysOfWeek": {
|
||||
"1": "Lundi",
|
||||
"2": "Mardi",
|
||||
"3": "Mercredi",
|
||||
"4": "Jeudi",
|
||||
"5": "Vendredi",
|
||||
"6": "Samedi",
|
||||
"0": "Dimanche"
|
||||
},
|
||||
"placement": {
|
||||
"before": "Avant",
|
||||
"after": "Après",
|
||||
"around": "Autour",
|
||||
"auto": "Auto (Meilleur choix)"
|
||||
}
|
||||
},
|
||||
"loading": "Calcul en cours...",
|
||||
"weekdaysShort": ["L", "M", "M", "J", "V", "S", "D"]
|
||||
}
|
||||
Reference in new issue
Block a user