Skip to content

fix: negotiate Accept-Language with language-only subtag fallback (#1973)#1975

Merged
davidpoblador merged 1 commit into
mainfrom
worktree-fix-accept-language-negotiation
Jun 2, 2026
Merged

fix: negotiate Accept-Language with language-only subtag fallback (#1973)#1975
davidpoblador merged 1 commit into
mainfrom
worktree-fix-accept-language-negotiation

Conversation

@davidpoblador

Copy link
Copy Markdown
Member

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.8 was served Castilian (es) instead of Catalan (ca), and a bare ca-ES resolved to nothing.

The root cause is upstream in starlette_babel.LocaleFromHeader, which never falls back to the language-only subtag (ca_ESca) 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

  • Add negotiate_accept_language(header, supported) in vibetuner.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=0 tags and the * wildcard are skipped.
  • Add LocaleFromAcceptLanguage, the selector wrapper, and use it in _build_locale_selectors in place of LocaleFromHeader.
  • Docs: development guide language-detection section, llms.txt, llms-full.txt.

Tests

New unit tests in tests/unit/test_i18n.py cover 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=0 skip, wildcard, empty/no-match, case-insensitivity, malformed q) plus the selector wrapper. test_locale_selectors.py updated to mirror the new selector. Full unit suite: 960 passed.

Closes #1973

)

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>
@davidpoblador

Copy link
Copy Markdown
Member Author

Upstream root cause reported: alex-oleshkevich/starlette_babel#16. This PR fixes it framework-side regardless of the upstream timeline.

@davidpoblador davidpoblador merged commit 0738181 into main Jun 2, 2026
2 checks passed
@davidpoblador davidpoblador deleted the worktree-fix-accept-language-negotiation branch June 2, 2026 16:41
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>
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.

LocaleFromHeader drops region-qualified top Accept-Language preference (ca-ES loses to es)

1 participant