init commit for iPAL-kit plugin
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user