Skip to content

fix(search): match language tags against locales as language ranges - #919

Merged
ddeboer merged 1 commit into
mainfrom
fix/language-range-matching
Oct 16, 2026
Merged

ddeboer merged 1 commit into
mainfrom
fix/language-range-matching

Conversation

@ddeboer

@ddeboer ddeboer commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

Fix #794

A language tag was matched against a field’s locales with ===, so a browser’s nl-NL matched no declared nl. A Dutch reader’s sort fell back to locales[0], a publisher’s nl-NL titles got no search or sort key, and the GraphQL language list could put another language first.

matchLanguage in @lde/search/adapter now does the matching everywhere. It compares BCP-47 subtags case-insensitively and ranks the exact tag first, then truncations (nl-NL → nl), then extensions (nl → nl-BE), then sibling regions (nl-NL → nl-BE). und matches only und.

  • Projection (applyText): a literal is keyed under the declared locale its tag best matches; within a locale the exact tag’s values come first, so a document with nl and nl-BE titles sorts on the nl one.
  • Sort (compileSort): nl-NL sorts on title_sort_nl.
  • Query weights (collectSearchable): the same exact comparison decided which locale keeps full weight. Not in the issue, same cause.
  • GraphQL (defaultLanguageOrder): each accepted language pulls in every available tag it matches, best first, without duplicates.

The sibling rank goes beyond the issue. Without it, a request in nl-NL against locales: ['nl-BE', 'en'] got no boost and sorted on the first locale only by accident, while the projection already routed a plain nl literal into nl-BE. The catch: it also ranks zh-Hans above English for a zh-Hant reader, though the scripts are not interchangeable.

I tried @formatjs/intl-localematcher instead of a hand-written helper. It has the same zh-Hant → zh-Hans fallback, also matches across languages (af → nl, nb → no), throws RangeError on malformed tags from publisher data or headers, and returns one match where defaultLanguageOrder needs a ranked list. Negotiator’s matcher, already a dependency, compares the primary subtag only. Neither saves code once wrapped.

Lookups still fetch display fields only for declared locales, so a referenced document’s label tagged nl-NL (or fr) renders without it, as it did before. Widening include_fields changes what a lookup costs, so it stays out of this PR.

- Add matchLanguage to @lde/search/adapter: BCP-47 range matching, exact tag first,
  then truncations, extensions and sibling regions; und matches only und
- Index a literal tagged nl-NL under a declared nl, exact tag first
- Sort and boost a request in nl-NL on the nl keys instead of the first locale
- Order a GraphQL language list by range, so nl-NL lists the nl value first
- Document the matching in the Locales reference
@ddeboer
ddeboer merged commit 80c14b9 into main Oct 16, 2026
4 checks passed
@ddeboer
ddeboer deleted the fix/language-range-matching branch October 16, 2026 12:20
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.

Match a request’s language tag against declared locales as a language range, not by exact string

1 participant