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
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user