feat: i18n primitives for per-tenant locale flows#1718
Merged
Conversation
Closes #1716. Adds three helpers to close the gap between vibetuner's LocaleMiddleware and per-tenant / user-driven language flows: - register_locale_resolver(getter, *, priority=0): inject custom selectors at the front of the chain, fail-soft, ordered by priority. - set_request_language(request, code): update both the Babel context and request.state.language atomically so {% trans %}, <html lang>, and Content-Language stay in sync. - language_picker(display_locale=None): [{code, name}] sorted, with names rendered in the current request's locale (no English-only default). A built-in context provider exposes the picker output as the supported_languages template variable. Migration: previously a set[str] of codes; now a list of {code, name} dicts. The shipped debug page and hreflang_tags helper are updated. The locale_names template variable (locale-independent native names) is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rriding supported_languages Drops the breaking change in #1718. The previous revision repurposed the existing supported_languages template variable (a set[str] of codes) as the picker's [{code, name}] output, which broke any user template iterating it for codes. Now language_picker is registered as a Jinja global so templates call it directly: {% for entry in language_picker() %}. The existing supported_languages and locale_names template variables are unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 task
davidpoblador
added a commit
that referenced
this pull request
Apr 28, 2026
…1724) ## Summary Expand `vibetuner-template/.claude/rules/localization.md` to cover the new `vibetuner.i18n` primitives shipped in #1718 so scaffolded projects (and their AI assistants) discover them. Adds three sections to the rules file: - **Language switcher (Jinja)** — `language_picker()` Jinja global, with example - **Forcing a language mid-request** — `set_request_language(request, code)` - **Custom locale resolvers** — `register_locale_resolver(getter, *, priority=0)` Plus a deep link to https://vibetuner.alltuner.com/development-guide/#custom-locale-resolvers-register_locale_resolver for full details. `AGENTS.md` is unchanged — its existing pointer to the framework `llms.txt` covers the high-level reference; the rules file is the canonical home for agent-targeted detail. ## Test plan - [x] `just lint-md` — clean 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
davidpoblador
pushed a commit
that referenced
this pull request
Apr 28, 2026
🤖 I have created a release *beep* *boop* --- ## [10.8.0](v10.7.0...v10.8.0) (2026-04-28) ### Features * add BrandSettings env-var surface for favicon and email branding ([#1720](#1720)) ([3d22717](3d22717)) * i18n primitives for per-tenant locale flows ([#1718](#1718)) ([177fe5e](177fe5e)) ### Bug Fixes * restore gettext plural-form support in lint-po ([#1725](#1725)) ([fe0dacb](fe0dacb)) * **template:** scope babel.cfg python extraction to src/ ([#1722](#1722)) ([b729964](b729964)) * theme default screens with DaisyUI semantic tokens ([#1714](#1714)) ([4467168](4467168)) ### Performance Improvements * **testing:** share MongoDB across vibetuner_db tests ([#1726](#1726)) ([c9064e4](c9064e4)) ### Miscellaneous Chores * **py:** use PEP 639 SPDX license expression ([#1727](#1727)) ([855f9f4](855f9f4)) ### Documentation Updates * **i18n:** clarify locale_names vs language_picker overlap ([#1723](#1723)) ([87733bd](87733bd)) * **template:** document i18n primitives in scaffolded agent rules ([#1724](#1724)) ([27a3b98](27a3b98)) --- 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
Closes #1716. Adds
vibetuner.i18nwith three primitives that close the gap between vibetuner'sLocaleMiddlewareand per-tenant / user-driven language flows.register_locale_resolver(getter, *, priority=0)— inject a custom selector at the front of the locale chain. Fail-soft (a raising resolver is logged and the chain falls through), priority-ordered, mirrors the shape ofregister_tenant_theme_provider. Per-tenant locale becomes a one-liner.set_request_language(request, code)— update both the Babel context (drives{% trans %}) andrequest.state.language(drives<html lang>+Content-Language) atomically so all three stay in sync. Validates withLocale.parse.language_picker(display_locale=None)— returns[{code, name}]sorted by name, with names rendered in the current request's locale viaLocale.get_display_name(display_locale). Browsing in Spanish gives "inglés / español / catalán"; browsing in Catalan gives "anglès / espanyol / català". No English-only default.language_pickeris also registered as a Jinja global so templates can call it directly:Non-breaking
The existing
supported_languagestemplate variable (set[str]of codes) andlocale_namestemplate variable (dict[str, str]of native names) are unchanged. Templates that iterate either continue to work.Test plan
uv run pytest tests/unit/— 773 pass (17 new tests intest_i18n.py)just lint(Python, markdown, jinja, toml, yaml)just type-check(ty)<html lang>+Content-Languagematch{% for entry in language_picker() %}into a template and verify the dropdown renders in the current locale🤖 Generated with Claude Code