Skip to content

Prefer the closest locale in language negotiation - #51

Merged
akesson merged 2 commits into
mainfrom
henrik/langneg-exact-match
Nov 7, 2026
Merged

akesson merged 2 commits into
mainfrom
henrik/langneg-exact-match

Conversation

@akesson

@akesson akesson commented Nov 7, 2026 •

Copy link
Copy Markdown
Owner

Summary

With both en and en-gb built in, L10n::langneg("en-GB") returned L10n::En. Negotiation compared only the language subtag and took the first locale that matched.

Now each Accept-Language entry, in q order, picks the closest locale:

  1. the exact locale (en-GB → en-gb);
  2. the same region with no conflicting script (sr-Latn-RS → sr-RS);
  3. the generic locale (en-AU → en, even if en-gb comes first);
  4. another region (en-AU → en-gb when there's no en);
  5. last, a locale with a conflicting script (sr-Latn → sr-Cyrl only when no other sr exists).

Ties go to the locale without a script the request didn't ask for (en → en, not en-Latn). The header's own order still comes first. In de-AT, en-GB;q=0.5, a plain de beats an exact en-gb, because the user prefers German.

Entries with q=0 ("not acceptable") or an invalid q are now skipped. Before, a q=0 entry could still be picked, and an invalid weight counted as q=1.

Known limit: scripts aren't inferred from regions, so zh-TW with both zh-Hans and zh-Hant available takes the first one. Fixing that needs likely-subtags data (icu_locale). That would add 18 crates and about 17 KB to langneg builds, so it's left for a separate decision. The docs and a test state this limit.

Before and after, with en, en-gb and fr:

Header Before After
en-GB En EnGb
en-US En En
fr-CA Fr Fr

Changes

  • prelude::best_match ranks each candidate once with a plain rank function. Both the generated L10n::langneg (through negotiate_languages) and L10nLanguageVec::langneg use it, so they can't disagree, and neither allocates per call any more.
  • negotiate_languages no longer requires PartialEq on the language type.
  • negotiate_languages had no tests; there are now tests for each tier and for header order.
  • The languages guide describes the new behaviour with assert_eq! examples. docs-check runs them, now against an added en-gb locale.
  • CHANGELOG entry under Fixed.

This changes which locale some requests get, but only towards the exact or more generic match, so I've listed it as a fix rather than a breaking change.

With both en and en-gb, L10n::langneg("en-GB") returned en: the first
locale with the same language subtag won. Now, for each header entry in
q order, the best match is the exact locale, then the same language and
region with another script, then the generic locale (en for en-AU), then
any locale of the language.

Both the generated L10n::langneg (via negotiate_languages) and
L10nLanguageVec::langneg use one shared best_match. The languages guide
shows the behaviour in examples that docs-check runs (with a new en-gb
locale there).
- Ranking takes scripts into account: a locale whose script conflicts
  with the requested one (sr-Cyrl for sr-Latn) is a last resort, the
  "same region" tier only applies when the request has a region, and a
  locale without an unrequested script wins ties (en over en-Latn).
- Accept-Language entries with q=0 or an invalid q are skipped instead
  of matching (q=0) or being promoted to q=1 (invalid). Other
  parameters next to q are allowed.
- best_match ranks each candidate once through a plain rank() function
  and takes a candidate accessor instead of a slice, so
  negotiate_languages no longer allocates per call.
- negotiate_languages drops its unused PartialEq bound.
- Docs list every tier, and both the docs and a test state the known
  limit: scripts aren't inferred from regions (zh-TW vs zh-Hans/Hant).
@akesson
akesson merged commit a604ce8 into main Nov 7, 2026
7 checks passed
@akesson
akesson deleted the henrik/langneg-exact-match branch November 7, 2026 10:51
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.

1 participant