Skip to content

fix: match a language range against a longer tag at a subtag boundary - #104

Open
spokodev wants to merge 1 commit into
jshttp:masterfrom
spokodev:fix-language-subtag-boundary-match
Open

fix: match a language range against a longer tag at a subtag boundary#104
spokodev wants to merge 1 commit into
jshttp:masterfrom
spokodev:fix-language-subtag-boundary-match

Conversation

@spokodev

Copy link
Copy Markdown

Problem

A one-subtag range matches a longer tag (en matches en-US), but a multi-subtag range does not match a still-longer tag, even when it is an exact prefix of it at a - boundary:

const language = require('negotiator/lib/language')

language('en',      ['en-US'])      // ['en-US']      (works)
language('en-US',   ['en-US-x'])    // []             <- expected ['en-US-x']
language('zh-Hant', ['zh-Hant-CN']) // []             <- expected ['zh-Hant-CN']

The behaviour is inconsistent across subtag depth.

Cause

RFC 4647 §3.3.1 (Basic Filtering): a range matches a tag when it equals the tag, or equals a prefix of the tag where the character following the prefix is -.

specify() modelled a tag as just prefix-suffix and compared the range's full value to the tag's first subtag:

} else if (spec.full.toLowerCase() === p.prefix.toLowerCase()) {
  s |= 1;
}

p.prefix is only the first subtag, so for en-US-x it is en, and the range en-US never matches.

Fix

Generalize that branch to the boundary-prefix check it was approximating — the tag begins with the range followed by -:

} else if (p.full.toLowerCase().startsWith(spec.full.toLowerCase() + '-')) {
  s |= 1;
}

For any tag that the old condition matched, the tag has a suffix and p.full is spec.full + '-' + suffix, so the new check is a strict superset — no previously-matching pair stops matching, and the specificity is unchanged (the range is still less specific than the tag). The - boundary is required, so en-U still does not match en-US.

Verification

  • Existing suite passes; added a whenAcceptLanguage('en-US') case asserting ['en-US-x'] matches while ['en-USX'] and ['en-GB'] do not.
  • Differential fuzz (200k random tag sets) vs the published version: 0 matches removed, every newly-added match satisfies the RFC boundary-prefix rule, and the ordering of any unchanged match set is identical.

A one-subtag range already matches a longer tag (`en` matches `en-US`),
but a multi-subtag range did not match a still-longer tag even when it is
an exact prefix at a `-` boundary:

    language('en-US', ['en-US-x'])     // []  (expected ['en-US-x'])
    language('zh-Hant', ['zh-Hant-CN']) // []  (expected ['zh-Hant-CN'])

RFC 4647 §3.3.1 (Basic Filtering): a range matches a tag when it equals the
tag or equals a prefix of the tag where the next character is `-`. The
prefix branch only compared the range's full value to the tag's first
subtag (`spec.full === p.prefix`), which holds only for two-subtag tags.

Generalize that branch to the boundary-prefix check it was approximating.
It keeps the same specificity (the range is less specific than the tag) and
is a strict superset of the old condition, so no previous match changes and
`en-U` still does not match `en-US` (the `-` boundary is required).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants