/** * Detect the user's preferred app language from an Accept-Language header * (or navigator.languages array) — Fase 10.8. * * Supported app languages (in sync with messages/{en,es}.json + Paraglide * languageTags): `en`, `es`. Anything else falls back to `en`. * * Rules (per plan §10.8.1): * * Inputs we accept: * - bare tag: `'es'`, `'en'`, `'es-ES'`, `'en-US'` * - RFC 7231 list: `'es;q=0.9,en;q=0.8'` * - `navigator.languages` array: `['es-ES', 'en-GB']` * - null / undefined / empty → `'en'` * * If the highest q-scored entry whose primary subtag is `es` has * q ≥ 0.5, the result is `'es'`. Otherwise (including unknown * languages, malformed input, or no clear preference) → `'en'`. */ export type AppLanguage = 'en' | 'es'; interface ParsedTag { tag: string; primary: string; q: number; } function parseTags(raw: string): ParsedTag[] { return raw .split(',') .map((part) => { const segments = part.trim().split(';').map((s) => s.trim()); if (!segments[0]) return null; const tag = segments[0].toLowerCase(); let q = 1.0; for (const seg of segments.slice(1)) { const match = seg.match(/^q\s*=\s*([0-9.]+)$/); if (match) { const parsed = parseFloat(match[1]); if (!Number.isNaN(parsed)) q = parsed; } } const primary = tag.split('-')[0]; // Filter junk: primary must be alpha-only and 1+ chars. if (!/^[a-z]+$/.test(primary)) return null; return { tag, primary, q }; }) .filter((p): p is ParsedTag => p !== null); } export function detectLanguage( input: string | string[] | null | undefined ): AppLanguage { if (input == null) return 'en'; let parsed: ParsedTag[]; if (Array.isArray(input)) { // navigator.languages: ordered by user preference, no q-scores. parsed = input .map((tag, index) => { const t = tag.toLowerCase().trim(); if (!t) return null; const primary = t.split('-')[0]; if (!/^[a-z]+$/.test(primary)) return null; // Synthesize a decreasing q-score so first-listed wins. return { tag: t, primary, q: 1 - index * 0.01 }; }) .filter((p): p is ParsedTag => p !== null); } else { const s = input.trim(); if (!s) return 'en'; parsed = parseTags(s); } if (parsed.length === 0) return 'en'; // Sort by q-score descending. parsed.sort((a, b) => b.q - a.q); // First app-known primary wins; if it's es with q ≥ 0.5 → es. for (const p of parsed) { if (p.primary === 'es') { return p.q >= 0.5 ? 'es' : 'en'; } if (p.primary === 'en') return 'en'; } return 'en'; }