fix: negotiate Accept-Language with language-only subtag fallback (#1973)#1975
Merged
Merged
Conversation
) The Accept-Language locale selector matched header tokens by full identifier only, so a region-qualified top preference was dropped in favor of a lower-ranked exact match: a browser sending `ca-ES,es;q=0.9` was served Castilian instead of Catalan. Add a framework-owned `LocaleFromAcceptLanguage` selector (backed by a reusable `negotiate_accept_language` helper) that resolves each preference by full tag first, then by its language-only subtag, so the highest-quality acceptable language wins. Wire it into the locale selector chain in place of starlette_babel's `LocaleFromHeader`, whose matching is the upstream root cause. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Member
Author
|
Upstream root cause reported: alex-oleshkevich/starlette_babel#16. This PR fixes it framework-side regardless of the upstream timeline. |
davidpoblador
pushed a commit
that referenced
this pull request
Jun 2, 2026
🤖 I have created a release *beep* *boop* --- ## [10.22.4](v10.22.3...v10.22.4) (2026-06-02) ### Bug Fixes * fast-path worker-health to skip CLI bootstrap ([#1974](#1974)) ([#1977](#1977)) ([ee77f6c](ee77f6c)) * load ProjectConfiguration deploy-config from .env ([#1970](#1970)) ([#1971](#1971)) ([13b67a7](13b67a7)) * negotiate Accept-Language with language-only subtag fallback ([#1973](#1973)) ([#1975](#1975)) ([0738181](0738181)) ### Documentation Updates * warn that backgrounded SSE tabs drop events and document resync ([#1978](#1978)) ([6bb1a91](6bb1a91)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #1973. The Accept-Language locale selector matched each header token against the supported locales by full identifier only, so a region-qualified top preference was silently dropped in favor of a lower-ranked exact match. A Catalan browser sending
Accept-Language: ca-ES,es;q=0.9,en;q=0.8was served Castilian (es) instead of Catalan (ca), and a bareca-ESresolved to nothing.The root cause is upstream in
starlette_babel.LocaleFromHeader, which never falls back to the language-only subtag (ca_ES→ca) even though the rest of the library (LocaleMiddleware._find_variant,negotiate_locale) is built around exactly that fallback. We're already on the latest release (1.1.0), so this fixes it framework-side now; the upstream behaviour is being reported separately.Change
negotiate_accept_language(header, supported)invibetuner.i18n: a pure helper that walks the header's tags highest-to-lowest quality and returns the first that matches a supported locale by full tag, then by language-only subtag. The highest-quality acceptable language wins;q=0tags and the*wildcard are skipped.LocaleFromAcceptLanguage, the selector wrapper, and use it in_build_locale_selectorsin place ofLocaleFromHeader.llms.txt,llms-full.txt.Tests
New unit tests in
tests/unit/test_i18n.pycover the negotiation (region-qualified top preference beats lower-q exact, bare region fallback, unsupported-top-token defer, exact region preservation, language-only → first supported variant,q=0skip, wildcard, empty/no-match, case-insensitivity, malformedq) plus the selector wrapper.test_locale_selectors.pyupdated to mirror the new selector. Full unit suite: 960 passed.Closes #1973