fix(dashboard): render pagination in the dashboard's own locale - #152
Merged
Conversation
The dashboard rendered Pagy labels in whatever language the host app last
used — Russian on one refresh, Portuguese on the next.
Pagy stores its locale in Thread.current[:pagy_locale] and never resets it
(pagy 43.6.1, modules/i18n/i18n.rb:24-31; the "for the duration of a single
request" comment there is aspirational, nothing in the gem enforces it). A host
app that assigns a locale per request leaves that value on the Puma thread, so a
dashboard request landing on a recycled thread inherits it. The language you get
is a function of which pooled thread served you, which is why it looked random.
Reproduced deterministically without a Rails app: setting the locale on a thread
and then rendering info_tag on that same thread yields ru/pt/fr/es output from
identical dashboard code.
Pagy 43 has no per-call locale option (verified across every helper signature),
so the only correct fix is to set and restore around the request:
- around_action, not before_action — a before_action would strand the
dashboard's locale on the thread for the host app's next request, the same
leak pointing the other way. The ensure also covers the rescue_from handlers,
which still render through the view layer.
- The previous value is read from Thread.current directly, not from
Pagy::I18n.locale. The getter coerces nil to "en", so restoring through it
would stamp "en" onto a thread that started clean, making the dashboard a
source of the very leak it fixes.
Adds config.dashboard_locale (default "en") rather than hardcoding English, so
the mechanism is already in place for translating the rest of the dashboard.
Locale resolution is defensive: an unknown lowercase tag ("xx") warns and falls
back in Pagy itself, but a wrong-cased one ("EN") passes Pagy's format check and
then raises NoMethodError mid-render, because the dictionary is looked up by an
exact top-level YAML key. Values are matched case-insensitively against the
shipped dictionaries and fall back to "en", so a config typo cannot break every
dashboard page.
Test plan:
- RSpec: 3648 examples, 0 failures (+8)
- RuboCop: 414 files, no offenses
- Verified the two core specs fail with the around_action removed and pass with
it, including the exact reported symptom
- Locale resolution checked against nil, "", " ", en, EN, fr, FR, ru, pt-BR,
pt-br, PT-BR, zh-CN, :fr, "xx" and "not-a-locale"
Fixes #148
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
3 tasks
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.
Fixes #148.
The bug
The dashboard rendered Pagy labels in whatever language the host app last used — Russian on one refresh, Portuguese on the next.
Root cause
Pagy stores its locale in a thread-local it never resets (
pagy-43.6.1/lib/pagy/modules/i18n/i18n.rb:24-31):That comment is aspirational — there is no
around_action, railtie hook, orensureanywhere in the gem. So under Puma:Pagy::I18n.locale = "ru""ru"pt→ PortugueseThe language is a function of which pooled thread picked up the request, which is why it looked random and changed on refresh.
Reproduction
Deterministic, no Rails app needed — set the locale on a thread, then render
info_tagon that same thread:Identical dashboard code, four languages.
The fix
Pagy 43 has no per-call locale option (verified across every helper signature), so set-and-restore around the request is the only correct approach. Two details are load-bearing:
around_action, notbefore_action. Abefore_actionwould strand the dashboard's locale on the thread for the host app's next request — the same leak pointing the other way. Theensurealso covers therescue_fromhandlers, which still render through the view layer.Thread.current, notPagy::I18n.locale. The getter coercesnil→"en", so restoring through it would stamp"en"onto a thread that started clean, making the dashboard a source of the very leak it fixes.config.dashboard_localeAdded (default
"en") rather than hardcoding English, so the mechanism is already in place for translating the rest of the dashboard later. Today it only affects Pagy labels; every other string in the UI is still English.Resolution is defensive. An unknown lowercase tag (
"xx") warns and falls back inside Pagy, but a wrong-cased one ("EN") passes Pagy'sLOCALE_PATTERN, findsen.yml, then reads a nil dictionary because the YAML's top-level key is lowercase — raisingNoMethodErrormid-render on every dashboard page. Values are matched case-insensitively against the shipped dictionaries and fall back to"en", so a config typo cannot take the dashboard down.Note for the reporter
The workaround in #148 has this same bug in the opposite direction — the admin controller sets
Pagy::I18n.localein abefore_actionand never restores it, leaking"en"into the public site's threads. Thearound_action/ensurepattern here fixes that class of bug.Test plan
around_actionremoved and pass with it, including the exact reported symptomnil,""," ",en,EN,fr,FR,ru,pt-BR,pt-br,PT-BR,zh-CN,:fr,"xx","not-a-locale"🤖 Generated with Claude Code