import type { I18nConfig } from './types.js' import { getLocaleCodes, isValidLocale } from './helpers.js' /** Cookie name the template uses to persist a visitor's locale choice. */ export const LOCALE_COOKIE_NAME = 'ipal-locale' type NegotiateLocaleArgs = { /** Raw Accept-Language header value */ acceptLanguage?: null | string config: I18nConfig /** Value of the locale cookie, if present (from LOCALE_COOKIE_NAME) */ cookieLocale?: null | string } /** * Resolves which locale to serve, in priority order: * 1. Cookie (explicit prior choice) * 2. Accept-Language header (best match against configured locales) * 3. Configured default locale * * Pure function — the template feeds it request data and acts on the result * (redirect, cookie set). No Next.js or request objects here. */ export function negotiateLocale({ acceptLanguage, config, cookieLocale, }: NegotiateLocaleArgs): string { // 1. Explicit prior choice wins if (cookieLocale && isValidLocale(cookieLocale, config)) { return cookieLocale } // 2. Best match from Accept-Language const fromHeader = matchAcceptLanguage(acceptLanguage, config) if (fromHeader) { return fromHeader } // 3. Fall back to configured default return config.defaultLocale } /** * Parses an Accept-Language header and returns the best-matching configured * locale, or undefined if none match. * * Matches on the primary subtag (e.g. "en-US" matches configured "en"), * respecting the header's quality-value ordering. */ export function matchAcceptLanguage( acceptLanguage: null | string | undefined, config: I18nConfig, ): string | undefined { if (!acceptLanguage) {return undefined} const available = getLocaleCodes(config) const ranked = parseAcceptLanguage(acceptLanguage) for (const tag of ranked) { // Exact match (e.g. "pt-BR" === "pt-BR") const exact = available.find((code) => code.toLowerCase() === tag) if (exact) {return exact} // Primary-subtag match (e.g. "en-us" → "en") const primary = tag.split('-')[0] const partial = available.find((code) => code.toLowerCase().split('-')[0] === primary) if (partial) {return partial} } return undefined } /** * Parses an Accept-Language header into locale tags ordered by descending * quality value. Tags are lowercased for comparison. * * "en-US,en;q=0.9,pl;q=0.8" → ["en-us", "en", "pl"] */ function parseAcceptLanguage(header: string): string[] { return header .split(',') .map((part) => { const [tag, ...params] = part.trim().split(';') const qParam = params.find((p) => p.trim().startsWith('q=')) const quality = qParam ? parseFloat(qParam.split('=')[1]) : 1 return { quality: Number.isNaN(quality) ? 0 : quality, tag: tag.trim().toLowerCase() } }) .filter((entry) => entry.tag && entry.tag !== '*') .sort((a, b) => b.quality - a.quality) .map((entry) => entry.tag) }