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

+179
View File
@@ -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;
}
}
}