Skip to content

feat: i18n primitives for per-tenant locale flows#1718

Merged
davidpoblador merged 2 commits into
mainfrom
worktree-deep-skipping-wilkinson
Apr 28, 2026
Merged

feat: i18n primitives for per-tenant locale flows#1718
davidpoblador merged 2 commits into
mainfrom
worktree-deep-skipping-wilkinson

Conversation

@davidpoblador

@davidpoblador davidpoblador commented Apr 28, 2026

Copy link
Copy Markdown
Member

Summary

Closes #1716. Adds vibetuner.i18n with three primitives that close the gap between vibetuner's LocaleMiddleware and 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 of register_tenant_theme_provider. Per-tenant locale becomes a one-liner.
  • set_request_language(request, code) — update both the Babel context (drives {% trans %}) and request.state.language (drives <html lang> + Content-Language) atomically so all three stay in sync. Validates with Locale.parse.
  • language_picker(display_locale=None) — returns [{code, name}] sorted by name, with names rendered in the current request's locale via Locale.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_picker is also registered as a Jinja global so templates can call it directly:

{% for entry in language_picker() %}
    <option value="{{ entry.code }}">{{ entry.name }}</option>
{% endfor %}

Non-breaking

The existing supported_languages template variable (set[str] of codes) and locale_names template 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 in test_i18n.py)
  • just lint (Python, markdown, jinja, toml, yaml)
  • just type-check (ty)
  • Smoke test with a scaffolded project: register a resolver, hit a page, confirm <html lang> + Content-Language match
  • Drop {% for entry in language_picker() %} into a template and verify the dropdown renders in the current locale

🤖 Generated with Claude Code

davidpoblador and others added 2 commits April 28, 2026 11:06
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>
@davidpoblador davidpoblador merged commit 177fe5e into main Apr 28, 2026
3 checks passed
@davidpoblador davidpoblador deleted the worktree-deep-skipping-wilkinson branch April 28, 2026 09:12
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>
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.

i18n: missing primitives — register_locale_resolver, language picker context, set_request_language helper

1 participant