You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Wiring per-tenant locale resolution in radio (PR alltuner/radio#407, closes #398) surfaced three places where vibetuner's i18n surface forced workarounds. None individually warranted a separate issue, but together they make the per-tenant flow harder than per-tenant theming (which was clean — `register_tenant_theme_provider` + `TenantTheme`).
What hurt
1. No public way to inject a custom locale selector
`LocaleMiddleware` is built internally with `selectors=_build_locale_selectors()` driven by `settings.locale_detection` config. Projects that need a custom resolver (e.g. "this tenant's pages always render in the tenant's language regardless of `Accept-Language`") have no way to plug into that chain.
Workaround in radio: reach down to `starlette_babel.set_locale()` directly inside `resolve_podcast()` and update `scope["state"]["language"]` by hand. Works because:
It runs after `LocaleMiddleware` and before response-start.
`LocaleMiddleware`'s `send_wrapper` reads `scope["state"]["language"]` at response-start, so the `content-language` header still matches.
But this is two coupled mutations the caller has to know about. Brittle.
Proposed: `register_locale_resolver(getter, *, priority=...)` analogous to `register_tenant_theme_provider`. The getter receives the request, returns `str | None`. Vibetuner adds it to the selector chain at the configured priority (front by default). Per-tenant locale becomes a one-liner.
2. No `set_request_language()` helper
To force a language mid-request, you need to know that both `set_locale(Locale.parse(code))` (Babel contextvar — drives `{% trans %}`) and `request.state.language = code` (template/header consumer) must be updated. Miss either and you get inconsistent rendering: `` says one thing, translated strings say another.
Proposed: a single `vibetuner.i18n.set_request_language(request, code)` helper that does both, with type checks and a clear docstring. Even if (1) lands, this is still useful for the late-bound case ("after a session login, switch to the user's preferred language").
3. No language picker template helper
Every project that exposes a language switcher in the UI re-implements:
```python
[
{"code": code, "name": Locale.parse(code).english_name}
for code in sorted(settings.project.languages)
]
```
Or worse — hardcodes the list, drifts from `.copier-answers.yml`, and mishandles display names (radio had this exact issue: a hardcoded `SUPPORTED_LANGUAGES` list out of sync with config; merged in alltuner/radio#407).
Proposed: ship `vibetuner.i18n.language_picker(display_locale=None)` returning `list[{code, name}]` — codes from `settings.project.languages`, names from Babel rendered in the requested display locale (defaults to current request locale, so the dropdown shows itself in the user's current language: "English / Spanish / Catalan" if browsing in English; "inglés / español / catalán" if browsing in Spanish).
Bonus: a registered context provider that exposes `supported_languages` (the picker's output) to every template. The header partial in (#1712, if landed) becomes a generic dropdown driven by that.
Out of scope
HTTP Accept-Language parsing changes (the existing chain is fine).
RTL handling.
Per-tenant translation catalogues (radio's policy is one shared catalogue, the active language changes per request — that's the right call).
Context
Wiring per-tenant locale resolution in radio (PR alltuner/radio#407, closes #398) surfaced three places where vibetuner's i18n surface forced workarounds. None individually warranted a separate issue, but together they make the per-tenant flow harder than per-tenant theming (which was clean — `register_tenant_theme_provider` + `TenantTheme`).
What hurt
1. No public way to inject a custom locale selector
`LocaleMiddleware` is built internally with `selectors=_build_locale_selectors()` driven by `settings.locale_detection` config. Projects that need a custom resolver (e.g. "this tenant's pages always render in the tenant's language regardless of `Accept-Language`") have no way to plug into that chain.
Workaround in radio: reach down to `starlette_babel.set_locale()` directly inside `resolve_podcast()` and update `scope["state"]["language"]` by hand. Works because:
But this is two coupled mutations the caller has to know about. Brittle.
Proposed: `register_locale_resolver(getter, *, priority=...)` analogous to `register_tenant_theme_provider`. The getter receives the request, returns `str | None`. Vibetuner adds it to the selector chain at the configured priority (front by default). Per-tenant locale becomes a one-liner.
2. No `set_request_language()` helper
To force a language mid-request, you need to know that both `set_locale(Locale.parse(code))` (Babel contextvar — drives `{% trans %}`) and `request.state.language = code` (template/header consumer) must be updated. Miss either and you get inconsistent rendering: `` says one thing, translated strings say another.
Proposed: a single `vibetuner.i18n.set_request_language(request, code)` helper that does both, with type checks and a clear docstring. Even if (1) lands, this is still useful for the late-bound case ("after a session login, switch to the user's preferred language").
3. No language picker template helper
Every project that exposes a language switcher in the UI re-implements:
```python
[
{"code": code, "name": Locale.parse(code).english_name}
for code in sorted(settings.project.languages)
]
```
Or worse — hardcodes the list, drifts from `.copier-answers.yml`, and mishandles display names (radio had this exact issue: a hardcoded `SUPPORTED_LANGUAGES` list out of sync with config; merged in alltuner/radio#407).
Proposed: ship `vibetuner.i18n.language_picker(display_locale=None)` returning `list[{code, name}]` — codes from `settings.project.languages`, names from Babel rendered in the requested display locale (defaults to current request locale, so the dropdown shows itself in the user's current language: "English / Spanish / Catalan" if browsing in English; "inglés / español / catalán" if browsing in Spanish).
Bonus: a registered context provider that exposes `supported_languages` (the picker's output) to every template. The header partial in (#1712, if landed) becomes a generic dropdown driven by that.
Out of scope
Pointers
Filed by Claude Code.