Files
vac-optimizer/src/app/services/translation-service.ts
T
nessar 3dc601982a feat: enhance user input component with new styling and functionality
- Added informational styling to warning cards in user input component.
- Improved remaining days details and history display with new computed properties.
- Updated user input service to manage planning outcomes and traces more effectively.
- Enhanced leave planner service to classify remaining balances with detailed reasons.
- Expanded translation service to support new balance reasoning and suggestions.
- Updated translations for multiple languages to reflect new features and terminology.
2026-10-07 21:49:33 +02:00

443 lines
10 KiB
TypeScript

/// <reference types="vite/client" />
import { Injectable, inject, Injector, computed } from '@angular/core';
import { storedSignal } from '../shared/utils/stored-signal';
import type { PlanOutcome, RemainingBalance } from './leave-planner.service';
import { addPlanningDays } from './planning-date';
export interface AppTranslations {
locale: string;
bookedDays: {
enter: string;
booked: string;
help: string;
fixed: string;
removed: string;
erase: string;
rangeSaved: string;
rangeRemoved: string;
};
app: {
title: string;
description: string;
skipToPlan: string;
};
userInput: {
title: string;
description: string;
addVacationDay: string;
calculate: string;
durationEstimate: string;
durationUnits: { seconds: string; minutes: string };
remainingDaysWarning: string;
remainingWhy: string;
remainingValidity: string;
remainingHorizon: string;
remainingEarnedFrom: string;
remainingHistoryHelp: string;
remainingNoEvaluations: string;
remainingSuggestions: Record<string, string>;
remainingCandidateReasons: Record<string, string>;
deleteEntry: string;
chooseDate: string;
availableDate: string;
days: string;
type: 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;
selectTheme: string;
themeOptions: {
system: string;
light: string;
dark: string;
};
systemAppearance: string;
planningHorizon: string;
planningStart: string;
lastPlanningDate: string;
planningDatesHelp: string;
invalidDate: string;
horizonOrder: string;
invalidHorizon: string;
};
rules: {
title: string;
subtitle: string;
active: string;
moveRuleUp: string;
moveRuleDown: string;
dragRule: string;
ruleMoved: string;
priorityInfo: string;
ifThenTemplate: string;
timeWindowTemplate: string;
editRule: string;
deleteRule: string;
addRule: string;
place: string;
ruleName: string;
holidayFalls: string;
days: string;
type: string;
position: string;
getBonus: string;
force: string;
obligation: string;
preference: 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;
};
};
planning: {
calculationDate: string;
futureEntitlement: string;
includedEntitlement: string;
comparison: {
title: string;
candidate: string;
preview: string;
nearby: string;
noAlternatives: string;
chooseDate: string;
dateRange: string;
invalidDate: string;
changes: string;
available: string;
unavailable: string;
backToPlan: string;
backToTrace: string;
};
details: {
intro: string;
balance: string;
available: string;
expires: string;
criteria: string;
criteriaHelp: string;
traceImpactTitle: string;
traceImpactHelp: string;
traceStageTitle: string;
traceStageHelp: string;
firstDifference: string;
higher: string;
lower: string;
tieBreaks: string;
audit: string;
auditHelp: string;
supporting: string;
remainingReasons: Record<string, string>;
};
explanation: {
breakTitle: string;
leaveDays: string;
freeDays: string;
daysOff: string;
wholeBreak: string;
leaveUsed: string;
expires: string;
linkedRules: string;
atRiskContribution: string;
impact: string;
impactHelp: string;
criterion: string;
withoutDay: string;
withDay: string;
noChange: string;
priorityHelp: string;
tryDate: string;
plannedDate: string;
currentPlan: string;
alternative: string;
sameDate: string;
moveHelp: string;
traceHelp: string;
beforeDecision: string;
withCandidate: string;
comparisonUnavailable: string;
objectives: Record<string, { label: string; help: string }>;
preferenceHelp: Record<string, string | undefined>;
stages: Record<string, string>;
unavailable: Record<string, string | undefined>;
};
title: string;
calendarHelp: string;
explanationHelp: string;
traceHelp: string;
technicalDetails: string;
compareDates: string;
stopComparing: string;
replayNotice: string;
daysPlaced: string;
daysGained: string;
longestWorkSequence: string;
legend: string;
invalidInput: string;
conflict: string;
incomplete: string;
cancel: string;
cancelling: string;
cancelled: string;
cancelledPartial: string;
error: string;
complete: string;
whyPlacement: string;
placementNavigation: string;
placement: string;
placements: string;
decision: string;
of: string;
first: string;
previous: string;
next: string;
last: string;
remaining: string;
balanceRemaining: string;
counterfactual: string;
counterfactualLoading: string;
counterfactualReady: string;
counterfactualError: string;
retryCounterfactual: string;
historicalEligibilityLoading: string;
historicalEligibilityReady: string;
historicalEligibilityError: string;
retryHistoricalEligibility: string;
eligible: string;
ineligible: string;
historical: string;
reason: string;
explanationError: string;
conflictDetails: string;
conflictingRules: string;
bottleneck: string;
incompleteDetails: string;
incumbentExplanation: string;
completedStages: string;
uncompletedStages: string;
none: string;
currentPlacement: string;
explainedPeriod: string;
outsideHorizon: string;
traceTitle: string;
traceLoading: string;
traceReady: string;
traceUnavailable: string;
phases: string;
firstPhase: string;
previousPhase: string;
nextPhase: string;
lastPhase: string;
decisions: string;
firstDecision: string;
previousDecision: string;
nextDecision: string;
lastDecision: string;
replay: string;
before: string;
after: string;
finalPlan: string;
dates: string;
candidates: string;
previousCandidates: string;
nextCandidates: string;
page: string;
compareAlternatives: string;
deltas: string;
activations: string;
noDecisions: string;
traceStatuses: {
chosen: string;
'evaluated-rejected': string;
pruned: string;
'not-examined': string;
ineligible: string;
};
tracePhaseStatuses: {
completed: string;
interrupted: string;
'not-run': string;
};
traceChanges: {
added: string;
'moved-from': string;
'moved-to': string;
removed: string;
shared: string;
unchanged: string;
};
};
benchmark: {
title: string;
instructions: string;
run: string;
running: string;
copy: 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,
0,
);
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 formatPlanningDate(date: string): string {
return new Date(`${date}T00:00:00Z`).toLocaleDateString(this.locale(), {
timeZone: 'UTC',
day: 'numeric',
month: 'long',
year: 'numeric',
});
}
public getRemainingBalanceReason(
balance: RemainingBalance,
plan: PlanOutcome | undefined,
incomplete = false,
): string {
const reasons = this.tr().planning.details.remainingReasons;
if (!plan) return reasons['not-established'];
const code =
incomplete && ['preserved', 'not-established'].includes(balance.reason.code)
? 'calculation-incomplete'
: balance.reason.code;
return (reasons[code] ?? reasons['not-established'])
.replace('{available}', this.formatPlanningDate(balance.availableFrom))
.replace('{expiry}', this.formatPlanningDate(balance.expiryDate))
.replace('{start}', this.formatPlanningDate(plan.horizon.start))
.replace('{end}', this.formatPlanningDate(addPlanningDays(plan.horizon.end, -1)));
}
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;
}
}
}