feat(i18n): pick language from ?lang, saved choice or browser language
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
614b331188
commit
28b1b88ace
4 files changed
+72
-37
No files matched your search
@@ -0,0 +1,52 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { LanguagePreferenceService } from './language-preference.service';
|
||||||
|
|
||||||
|
describe('LanguagePreferenceService', (): void => {
|
||||||
|
let service: LanguagePreferenceService;
|
||||||
|
|
||||||
|
beforeEach((): void => {
|
||||||
|
localStorage.clear();
|
||||||
|
window.history.replaceState(null, '', '/');
|
||||||
|
service = TestBed.inject(LanguagePreferenceService);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach((): void => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
window.history.replaceState(null, '', '/');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fall back to English for unsupported browser languages', (): void => {
|
||||||
|
vi.spyOn(navigator, 'language', 'get').mockReturnValue('fr-FR');
|
||||||
|
expect(service.initialize()).toBe('en');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use the browser language on the first visit', (): void => {
|
||||||
|
vi.spyOn(navigator, 'language', 'get').mockReturnValue('de-AT');
|
||||||
|
expect(service.initialize()).toBe('de');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should prefer a stored choice over the browser language', (): void => {
|
||||||
|
vi.spyOn(navigator, 'language', 'get').mockReturnValue('de-AT');
|
||||||
|
localStorage.setItem('portfolio-language', 'en');
|
||||||
|
expect(service.initialize()).toBe('en');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should prefer the lang query parameter over everything else', (): void => {
|
||||||
|
localStorage.setItem('portfolio-language', 'en');
|
||||||
|
window.history.replaceState(null, '', '/?lang=de');
|
||||||
|
expect(service.initialize()).toBe('de');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore an unsupported lang query parameter', (): void => {
|
||||||
|
vi.spyOn(navigator, 'language', 'get').mockReturnValue('en-US');
|
||||||
|
window.history.replaceState(null, '', '/?lang=fr');
|
||||||
|
expect(service.initialize()).toBe('en');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should persist a confirmed language', (): void => {
|
||||||
|
service.confirmLanguage('de');
|
||||||
|
expect(service.languageCode()).toBe('de');
|
||||||
|
expect(localStorage.getItem('portfolio-language')).toBe('de');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,31 +2,35 @@ import { Injectable, signal } from '@angular/core';
|
|||||||
|
|
||||||
import type { LanguageCode } from '../../languages/language.types';
|
import type { LanguageCode } from '../../languages/language.types';
|
||||||
|
|
||||||
|
const isLanguageCode = (value: string | null): value is LanguageCode =>
|
||||||
|
value === 'en' || value === 'de';
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class LanguagePreferenceService {
|
export class LanguagePreferenceService {
|
||||||
private static readonly storageKey = 'portfolio-language';
|
private static readonly storageKey = 'portfolio-language';
|
||||||
private readonly currentLanguageCode = signal<LanguageCode>('en');
|
private readonly currentLanguageCode = signal<LanguageCode>('en');
|
||||||
private readonly languageConfirmed = signal(true);
|
|
||||||
|
|
||||||
readonly languageCode = this.currentLanguageCode.asReadonly();
|
readonly languageCode = this.currentLanguageCode.asReadonly();
|
||||||
readonly isLanguageConfirmed = this.languageConfirmed.asReadonly();
|
|
||||||
|
|
||||||
initializeFromStorage(): LanguageCode {
|
/** Picks the language from `?lang=`, then the stored choice, then the browser language. */
|
||||||
|
initialize(): LanguageCode {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
return this.currentLanguageCode();
|
return this.currentLanguageCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
const storedLanguage = window.localStorage.getItem(LanguagePreferenceService.storageKey);
|
const candidates = [
|
||||||
if (storedLanguage === 'de' || storedLanguage === 'en') {
|
new URLSearchParams(window.location.search).get('lang'),
|
||||||
this.currentLanguageCode.set(storedLanguage);
|
window.localStorage.getItem(LanguagePreferenceService.storageKey),
|
||||||
}
|
window.navigator.language.slice(0, 2).toLowerCase(),
|
||||||
|
];
|
||||||
|
const language = candidates.find(isLanguageCode) ?? 'en';
|
||||||
|
this.currentLanguageCode.set(language);
|
||||||
|
|
||||||
return this.currentLanguageCode();
|
return language;
|
||||||
}
|
}
|
||||||
|
|
||||||
confirmLanguage(code: LanguageCode): void {
|
confirmLanguage(code: LanguageCode): void {
|
||||||
this.currentLanguageCode.set(code);
|
this.currentLanguageCode.set(code);
|
||||||
this.languageConfirmed.set(true);
|
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
window.localStorage.setItem(LanguagePreferenceService.storageKey, code);
|
window.localStorage.setItem(LanguagePreferenceService.storageKey, code);
|
||||||
|
|||||||
@@ -2,26 +2,15 @@ import { Injectable, computed, inject } from '@angular/core';
|
|||||||
|
|
||||||
import { deLanguage } from '../../languages/de';
|
import { deLanguage } from '../../languages/de';
|
||||||
import { enLanguage } from '../../languages/en';
|
import { enLanguage } from '../../languages/en';
|
||||||
import type { LanguageCode, LanguagePack } from '../../languages/language.types';
|
import type { LanguagePack } from '../../languages/language.types';
|
||||||
import { LanguagePreferenceService } from './language-preference.service';
|
import { LanguagePreferenceService } from './language-preference.service';
|
||||||
|
|
||||||
|
/** Page content for the lazy-loaded routes; kept apart from the shell texts to stay out of the initial bundle. */
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class LanguageService {
|
export class LanguageService {
|
||||||
private readonly languagePreferenceService = inject(LanguagePreferenceService);
|
private readonly languageCode = inject(LanguagePreferenceService).languageCode;
|
||||||
|
|
||||||
readonly languageCode = this.languagePreferenceService.languageCode;
|
readonly content = computed<LanguagePack>(() =>
|
||||||
readonly isLanguageConfirmed = this.languagePreferenceService.isLanguageConfirmed;
|
this.languageCode() === 'de' ? deLanguage : enLanguage,
|
||||||
readonly content = computed<LanguagePack>(() => this.getPack(this.languageCode()));
|
);
|
||||||
|
|
||||||
initializeFromStorage(): LanguageCode {
|
|
||||||
return this.languagePreferenceService.initializeFromStorage();
|
|
||||||
}
|
|
||||||
|
|
||||||
confirmLanguage(code: LanguageCode): void {
|
|
||||||
this.languagePreferenceService.confirmLanguage(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
getPack(code: LanguageCode): LanguagePack {
|
|
||||||
return code === 'de' ? deLanguage : enLanguage;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -6,20 +6,10 @@ import { LanguagePreferenceService } from './language-preference.service';
|
|||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class ShellLanguageService {
|
export class ShellLanguageService {
|
||||||
private readonly languagePreferenceService = inject(LanguagePreferenceService);
|
private readonly languageCode = inject(LanguagePreferenceService).languageCode;
|
||||||
|
|
||||||
readonly languageCode = this.languagePreferenceService.languageCode;
|
|
||||||
readonly isLanguageConfirmed = this.languagePreferenceService.isLanguageConfirmed;
|
|
||||||
readonly content = computed<LanguageShellPack>(() => this.getPack(this.languageCode()));
|
readonly content = computed<LanguageShellPack>(() => this.getPack(this.languageCode()));
|
||||||
|
|
||||||
initializeFromStorage(): LanguageCode {
|
|
||||||
return this.languagePreferenceService.initializeFromStorage();
|
|
||||||
}
|
|
||||||
|
|
||||||
confirmLanguage(code: LanguageCode): void {
|
|
||||||
this.languagePreferenceService.confirmLanguage(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
getPack(code: LanguageCode): LanguageShellPack {
|
getPack(code: LanguageCode): LanguageShellPack {
|
||||||
return code === 'de' ? deShellLanguage : enShellLanguage;
|
return code === 'de' ? deShellLanguage : enShellLanguage;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user