fix(login): address review feedback on language dropdown PR

- Fix import path: use i18n barrel instead of TranslationContext directly
- Encapsulate localStorage key behind hasStoredLanguage() helper in settingsStore
- Fix pt-BR detection: only map pt-BR to br, pt-PT now returns null correctly
- Add comment linking server SUPPORTED_LANG_CODES to canonical client source
- Extract /api/config inline handler to routes/publicConfig.ts
- Add aria-haspopup, aria-expanded, role=listbox/option, aria-selected to dropdown
- Add 8 tests for detectBrowserLanguage (FE-COMP-I18N-016–023)
- Add 3 tests for setLanguageTransient (FE-STORE-SETTINGS-015–017)
This commit is contained in:
jubnl
2026-04-15 03:04:25 +02:00
parent f35c503658
commit a07e76c740
8 changed files with 116 additions and 6 deletions
+51
View File
@@ -8,6 +8,7 @@ import {
getIntlLanguage,
isRtlLanguage,
SUPPORTED_LANGUAGES,
detectBrowserLanguage,
} from '../../../src/i18n'
import { resetAllStores, seedStore } from '../../helpers/store'
import { useSettingsStore } from '../../../src/store/settingsStore'
@@ -96,6 +97,56 @@ describe('SUPPORTED_LANGUAGES', () => {
})
})
// ── FE-COMP-I18N-016 to 023: detectBrowserLanguage ───────────────────────────
describe('detectBrowserLanguage', () => {
afterEach(() => {
Object.defineProperty(navigator, 'languages', { value: [], configurable: true })
Object.defineProperty(navigator, 'language', { value: '', configurable: true })
})
it('FE-COMP-I18N-016: exact match returns the matched code', () => {
Object.defineProperty(navigator, 'languages', { value: ['de'], configurable: true })
expect(detectBrowserLanguage()).toBe('de')
})
it('FE-COMP-I18N-017: region-tagged exact match (zh-TW) returns zh-TW', () => {
Object.defineProperty(navigator, 'languages', { value: ['zh-TW'], configurable: true })
expect(detectBrowserLanguage()).toBe('zh-TW')
})
it('FE-COMP-I18N-018: prefix match (de-AT → de)', () => {
Object.defineProperty(navigator, 'languages', { value: ['de-AT'], configurable: true })
expect(detectBrowserLanguage()).toBe('de')
})
it('FE-COMP-I18N-019: pt-PT returns null (European Portuguese is a distinct language)', () => {
Object.defineProperty(navigator, 'languages', { value: ['pt-PT'], configurable: true })
expect(detectBrowserLanguage()).toBeNull()
})
it('FE-COMP-I18N-020: pt-BR maps to br', () => {
Object.defineProperty(navigator, 'languages', { value: ['pt-BR'], configurable: true })
expect(detectBrowserLanguage()).toBe('br')
})
it('FE-COMP-I18N-021: first-match-wins across multiple entries', () => {
Object.defineProperty(navigator, 'languages', { value: ['xx-XX', 'fr'], configurable: true })
expect(detectBrowserLanguage()).toBe('fr')
})
it('FE-COMP-I18N-022: unknown language returns null', () => {
Object.defineProperty(navigator, 'languages', { value: ['xx'], configurable: true })
expect(detectBrowserLanguage()).toBeNull()
})
it('FE-COMP-I18N-023: falls back to navigator.language when navigator.languages is empty', () => {
Object.defineProperty(navigator, 'languages', { value: [], configurable: true })
Object.defineProperty(navigator, 'language', { value: 'es', configurable: true })
expect(detectBrowserLanguage()).toBe('es')
})
})
// ── FE-COMP-I18N-010 to 015: TranslationProvider + useTranslation ─────────────
describe('TranslationProvider + useTranslation integration', () => {
@@ -170,6 +170,37 @@ describe('settingsStore', () => {
});
});
describe('FE-STORE-SETTINGS-015: setLanguageTransient updates state without touching localStorage', () => {
it('sets language in state but does not write to localStorage', () => {
localStorage.clear();
useSettingsStore.getState().setLanguageTransient('fr');
expect(useSettingsStore.getState().settings.language).toBe('fr');
expect(localStorage.getItem('app_language')).toBeNull();
});
});
describe('FE-STORE-SETTINGS-016: setLanguageTransient rejects unsupported language code', () => {
it('leaves state unchanged for an unknown code', () => {
const before = useSettingsStore.getState().settings.language;
useSettingsStore.getState().setLanguageTransient('xx');
expect(useSettingsStore.getState().settings.language).toBe(before);
});
});
describe('FE-STORE-SETTINGS-017: setLanguageTransient does not overwrite an explicit localStorage choice', () => {
it('localStorage remains unchanged after a transient set', () => {
localStorage.setItem('app_language', 'de');
useSettingsStore.getState().setLanguageTransient('es');
expect(localStorage.getItem('app_language')).toBe('de');
});
});
describe('FE-STORE-SETTINGS-014: updateSetting API failure leaves optimistic state', () => {
it('throws on API failure but keeps the optimistic state', async () => {
server.use(