Files
zeekay 595bb3b930 79 locales: right-to-left by script, regional variants in resolve
Add 43 locales to the registry, their loaders and catalogs: Persian,
Dari, Hebrew, Urdu, Pashto, Central Kurdish, Sindhi, Uyghur, Punjabi in
both scripts, four regional Arabics, Hong Kong Chinese, Mexican and
Argentine Spanish, European Portuguese, Canadian French, and 24 more.
34 wait for a native reader in review.json; ti, qu, gn, ay, bo and dv
are not shipped.

Every locale in Arabic or Hebrew script is rtl, and the registry test
derives the script from the tag. rtl.css selects on :dir(rtl), so it
holds inside a right-to-left region of a left-to-right page.

resolve no longer leans on registry order: after the exact tag and the
script, a request takes its own region, then the variant that serves it
(CLDR parents for es-419 and pt-PT, Macau to zh-HK, the Arabic dialect
areas), then the variant with no region, then the language's own region.
pnb reaches pa-PK.
2026-10-22 05:30:36 -07:00

98 lines
4.0 KiB
TypeScript

import assert from 'node:assert/strict'
import { test } from 'node:test'
import { resolve } from '../src/index.ts'
import { accept, negotiate } from '../src/accept.ts'
test('a header is its tags, best first', () => {
assert.deepEqual(accept('en-GB,en;q=0.9,fr;q=0.8'), ['en-GB', 'en', 'fr'])
})
test('weight ranks, it does not filter', () => {
// The header is written in whatever order the client felt like; the q values
// are the ranking. Reading it as written serves Dutch to someone who asked
// for Japanese.
assert.deepEqual(accept('nl;q=0.2,ja;q=0.9,de;q=0.5'), ['ja', 'de', 'nl'])
assert.equal(negotiate('nl;q=0.2,ja;q=0.9,de;q=0.5'), 'ja-JP')
})
test('an unweighted tag outranks a weighted one', () => {
// Omitted q is 1, which is the top. Chrome and Firefox both write their first
// choice this way, so reading a missing q as 0 inverts every real request.
assert.deepEqual(accept('fr,en;q=0.9'), ['fr', 'en'])
assert.deepEqual(accept('en;q=0.9,fr'), ['fr', 'en'])
})
test('equal weights keep the order the client wrote', () => {
assert.deepEqual(accept('de;q=0.9,fr;q=0.9,es;q=0.9'), ['de', 'fr', 'es'])
assert.deepEqual(accept('es;q=0.9,fr;q=0.9,de;q=0.9'), ['es', 'fr', 'de'])
})
test('q=0 is a preference against', () => {
assert.deepEqual(accept('de;q=0,fr;q=0.5'), ['fr'])
assert.deepEqual(accept('de;q=0'), [])
})
test('whitespace and case in the header are not meaning', () => {
assert.deepEqual(accept(' fr-CA ; q=0.8 , en-US ; Q=0.9 '), ['en-US', 'fr-CA'])
})
test('a header we cannot read still renders a page', () => {
// This runs on every request. It has no failure mode.
assert.deepEqual(accept(''), [])
assert.deepEqual(accept(null), [])
assert.deepEqual(accept(undefined), [])
assert.deepEqual(accept(',,,'), [])
assert.deepEqual(accept('fr;q=nonsense'), ['fr'])
assert.equal(negotiate(null), 'en-US')
assert.equal(negotiate('☃'), 'en-US')
})
test('a wildcard asks for anything, which is the default', () => {
assert.deepEqual(accept('*'), [])
assert.equal(negotiate('*'), 'en-US')
assert.deepEqual(accept('*;q=0.5,ja;q=0.9'), ['ja'])
})
test('one header cannot ask for unbounded work', () => {
// Anyone who can reach the port writes this string.
const flood = Array.from({ length: 5000 }, (_, i) => `x${i};q=0.5`).join(',')
assert.ok(accept(flood).length <= 32)
assert.equal(negotiate(`${flood},de-DE`), 'en-US')
})
test('negotiate and resolve cannot disagree', () => {
// The server exists to ANSWER what was asked, never to re-decide what we
// serve. If it ever grew its own matching, a laptop and a phone set to the
// same language would read different pages.
for (const header of ['zh-Hant-HK', 'en-GB', 'es-MX,en;q=0.4', 'ko-KR', 'cy-GB']) {
assert.equal(negotiate(header), resolve(accept(header)), header)
}
})
test('a caller may state its own supported set', () => {
assert.equal(negotiate('de-DE', ['en-US', 'ja-JP']), 'en-US')
assert.equal(negotiate('ja,de', ['en-US', 'ja-JP']), 'ja-JP')
})
test('real browsers', () => {
assert.equal(negotiate('en-US,en;q=0.9'), 'en-US')
assert.equal(negotiate('zh-TW,zh;q=0.9,en-US;q=0.8,en;q=0.7'), 'zh-Hant')
assert.equal(negotiate('zh-HK,zh;q=0.9,en;q=0.8'), 'zh-HK')
assert.equal(negotiate('es-CO,es;q=0.9,en;q=0.8'), 'es-419')
assert.equal(negotiate('fa-IR,fa;q=0.9,en;q=0.8'), 'fa')
assert.equal(negotiate('pt-BR,pt;q=0.9,en-US;q=0.8'), 'pt-BR')
})
test('a q with no number is a typo, not a refusal', () => {
// `Number('')` is 0 — finite — so an empty q scored zero and the tag was
// dropped as "not acceptable", discarding the language someone asked for over
// a missing character. Unreadable means full strength, which is what the rule
// beside it has always said.
assert.deepEqual(accept('en-US;q='), ['en-US'])
assert.deepEqual(accept('fr-FR;q=,de-DE;q=,ja-JP'), ['fr-FR', 'de-DE', 'ja-JP'])
assert.deepEqual(accept('en-US;q= '), ['en-US'])
// A real zero still means what RFC 9110 says it means.
assert.deepEqual(accept('en-US;q=0,fr-FR'), ['fr-FR'])
})