Files
Krystie b5e23d8e3f
CI / Test & Lint (push) Canceled after 0s
CI / Security audit (push) Canceled after 0s
[grade=B] fix: i18n detectLanguage RFC 7231 q-value compliance + stale test fixes
- Fix detectLanguage() to sort by HTTP q-values per RFC 7231 (was first-match-wins)
- Strict qvalue grammar: /^(0(?:\.\d{0,3})?|1(?:\.0{0,3})?)$/
- Exclude q=0 entries (not acceptable per RFC)
- Case-insensitive Q parameter name
- Fix 5 stale tests: zh/ja now supported (31 languages, not 5)
- Add 7 boundary regression tests for q-value parsing
- All 1781 tests pass

Codex grade: B (urn:ump:6yumklcezgiaemcg5t2mebuoi4w2n5dexozm4j7h5pu5g2s4p5ta)
2026-08-13 16:30:20 -07:00

148 lines
4.9 KiB
JavaScript

/**
* DC-077: Tests for the i18n system
*/
const i18n = require('../src/utilities/i18n');
describe('DC-077: i18n system', () => {
describe('t() translation function', () => {
it('translates keys in English by default', () => {
expect(i18n.t('dashboard.title')).toBe('Dashboard');
expect(i18n.t('action.start')).toBe('Start');
});
it('translates keys in Spanish', () => {
expect(i18n.t('dashboard.title', 'es')).toBe('Panel de control');
expect(i18n.t('action.start', 'es')).toBe('Iniciar');
});
it('translates keys in French', () => {
expect(i18n.t('dashboard.title', 'fr')).toBe('Tableau de bord');
expect(i18n.t('action.stop', 'fr')).toBe('Arrêter');
});
it('translates keys in German', () => {
expect(i18n.t('dashboard.title', 'de')).toBe('Dashboard');
expect(i18n.t('action.delete', 'de')).toBe('Löschen');
});
it('translates keys in Arabic', () => {
expect(i18n.t('dashboard.title', 'ar')).toBe('لوحة التحكم');
expect(i18n.t('action.start', 'ar')).toBe('تشغيل');
});
it('falls back to English for unsupported language', () => {
expect(i18n.t('dashboard.title', 'xx')).toBe('Dashboard');
});
it('falls back to key if not found in any language', () => {
expect(i18n.t('nonexistent.key.xyz')).toBe('nonexistent.key.xyz');
});
});
describe('getSupportedLanguages()', () => {
it('returns array of language codes', () => {
const langs = i18n.getSupportedLanguages();
expect(langs).toContain('en');
expect(langs).toContain('es');
expect(langs).toContain('fr');
expect(langs).toContain('de');
expect(langs).toContain('ar');
expect(langs.length).toBeGreaterThanOrEqual(5);
});
});
describe('isSupported()', () => {
it('returns true for supported languages', () => {
expect(i18n.isSupported('en')).toBe(true);
expect(i18n.isSupported('fr')).toBe(true);
});
it('returns false for unsupported languages', () => {
expect(i18n.isSupported('xx')).toBe(false);
expect(i18n.isSupported('klingon')).toBe(false);
});
});
describe('detectLanguage()', () => {
it('detects from Accept-Language header', () => {
expect(i18n.detectLanguage('es-ES,es;q=0.9,en;q=0.8')).toBe('es');
expect(i18n.detectLanguage('fr-FR,fr;q=0.9')).toBe('fr');
expect(i18n.detectLanguage('de-DE,de;q=0.9,en;q=0.8')).toBe('de');
});
it('handles quality values correctly', () => {
expect(i18n.detectLanguage('en;q=0.9,fr;q=1.0')).toBe('fr');
});
it('defaults to English for no header', () => {
expect(i18n.detectLanguage(null)).toBe('en');
expect(i18n.detectLanguage(undefined)).toBe('en');
expect(i18n.detectLanguage('')).toBe('en');
});
it('defaults to English for unsupported languages', () => {
expect(i18n.detectLanguage('xx-XX,xx;q=0.9')).toBe('en');
expect(i18n.detectLanguage('klingon-KL,klingon;q=0.9')).toBe('en');
});
it('strips region codes before matching', () => {
expect(i18n.detectLanguage('en-US,en;q=0.9')).toBe('en');
expect(i18n.detectLanguage('de-AT,de;q=0.9')).toBe('de');
});
it('respects equal q-values by order', () => {
expect(i18n.detectLanguage('en;q=0.5,de;q=0.5')).toBe('en');
});
it('excludes q=0 entries per RFC 7231', () => {
expect(i18n.detectLanguage('en;q=0,fr;q=0.9')).toBe('fr');
});
it('serves default language when all entries have q=0 (intentional fallback)', () => {
expect(i18n.detectLanguage('en;q=0,fr;q=0')).toBe('en');
});
it('handles malformed q-values gracefully', () => {
// 'abc' is not a valid q-value per RFC 7231 grammar, so it is treated as
// "no q-value specified" — per the HTTP spec the default weight is q=1.0.
expect(i18n.detectLanguage('en;q=abc,fr;q=0.9')).toBe('en');
});
it('accepts q=0 boundary (excludes entry)', () => {
expect(i18n.detectLanguage('en;q=0,fr;q=0.9')).toBe('fr');
});
it('accepts q=1 boundary', () => {
expect(i18n.detectLanguage('en;q=1,fr;q=0.9')).toBe('en');
});
it('accepts q=1.0', () => {
expect(i18n.detectLanguage('en;q=1.0,fr;q=0.9')).toBe('en');
});
it('accepts q=0.001 (lowest non-zero weight)', () => {
expect(i18n.detectLanguage('en;q=0.001,fr;q=0.9')).toBe('fr');
});
it('accepts q=0.999', () => {
expect(i18n.detectLanguage('en;q=0.999,fr;q=0.9')).toBe('en');
});
it('rejects q=1.001 (RFC invalid) — defaults to 1.0', () => {
expect(i18n.detectLanguage('en;q=1.001,fr;q=0.9')).toBe('en');
});
it('handles uppercase Q parameter', () => {
expect(i18n.detectLanguage('en;Q=0.5,fr;q=0.9')).toBe('fr');
});
});
describe('RTL support', () => {
it('Arabic is in supported languages', () => {
expect(i18n.isSupported('ar')).toBe(true);
expect(i18n.t('dashboard.title', 'ar')).toBeTruthy();
});
});
});