Three PRs to a check: how Solidus stopped passing its own translations by default
Solidus's 39 translations were never checked against English, because en.yml lives in a different gem. Two small fix PRs, then a third that pointed i18n-tasks at the right file and put the check in their own spec suite.
Key takeaway
Solidus's 39 translations were never checked against English, because en.yml lives in a different gem. Two small fix PRs, then a third that pointed i18n-tasks at the right file and put the check in their own spec suite.
Solidus is an open-source Rails e-commerce platform. English strings
live in solidus_core (core/config/locales/en.yml). The 39 translations live in a separate gem in the same
monorepo, solidus_i18n (i18n/config/locales/*.yml). That split turned out to matter.
Between September 16 and October 8 I sent three PRs. The first two fixed strings. The third fixed the reason the strings could break unnoticed, using the tool Solidus already had (i18n-tasks) and adding no dependency on mine.
The first PR: thirteen pt-BR strings without their variables
Rails t() substitutes %{name} tokens from the arguments the code passes. If a translation leaves the token
out, nothing errors; the user just sees a vaguer sentence. In pt-BR, thirteen strings had lost theirs:
| key | en | pt-BR before |
|---|---|---|
spree.inventory_error_flash_for_insufficient_quantity | %{names} became unavailable. | Um item no seu carrinho está indisponível |
spree.reimbursement_mailer…total_refunded | Total refunded: %{total} | Total Restituído |
spree.shipment_details | From %{stock_location} via %{shipping_method} | Detalhes de envio |
So a Brazilian shopper was told "an item in your cart is unavailable" without being told which one, and a refund email said "Total refunded" with no amount.
#6626 restored all thirteen (one file, 13 lines). Three core maintainers approved it within 16 minutes of each other, the first one saying "Changes all look reasonable to me. Thanks!", and it merged the same day it was opened.
The second PR: the same bug in nl and pl
The #6626 description mentioned that nl and pl drop %{names} from the same flash message, and offered a
follow-up. #6637 was that follow-up: two lines. The Dutch one
was also ungrammatical (is … zijn).
It sat behind one red check, an accessibility scan in solidus_admin that had run mid-page-transition and
had nothing to do with two YAML lines. Fork authors can't re-run checks, so I
explained the failure and offered a
rebase. A maintainer re-ran it five
minutes later. It merged on September 22 with three approvals.
Why the checks hadn't caught it
After the merge I re-scanned main and
posted what was left: the same
%{names} drop in 19 more locales, say_yes/say_no stored as YAML booleans in some files, and duplicate keys
in six files. I also suggested a CI command using my checker. Nobody replied to that comment, and that was a
fair answer: the useful question was why their own checks hadn't caught any of this.
Solidus already runs i18n-tasks. core/spec/i18n_spec.rb checks inconsistent interpolations for
solidus_core. But i18n-tasks compares every locale against a base locale, and i18n/ has no en.yml; it
lives in core. Run from i18n/, i18n-tasks check-consistent-interpolations had nothing to compare against,
so it passed whatever the translations said.
The third PR: point the existing tool at the right file
#6655 did three things:
- Config:
i18n/i18n-tasks.ymlnow also reads../core/config/locales/%{locale}.yml, soenis the base. - Spec:
i18n/spec/i18n_spec.rb, the same interpolation check core already had, now for the 39 translations. - Fixes for what the check found: 19 findings on
mainwith the new config, 0 on the branch. Among them:- ja: two return-item error messages had their values swapped, so one of them interpolated a variable
the code doesn't pass. In Japanese, that validation raised
I18n::MissingInterpolationArgumentinstead of showing a message. - fr: order confirmation and cancellation emails contained
%{total}/%{subtotal}, but the views callt('.total')with no arguments, so French customers got a literal%{total}in their email. - es, es-MX, es-CL:
% {date}and% {amount}with a space, which Rails never substitutes.
- ja: two return-item error messages had their values swapped, so one of them interpolated a variable
the code doesn't pass. In Japanese, that validation raised
What it didn't fix: translations of an older English sentence, still rendering, just without the variable.
Those need a translator, not a token edit, so they went into ignore_inconsistent_interpolations with a
comment to remove each key once it's retranslated. One entry is there because it's correct: Russian one
covers 21, 31, … so it needs the count.
A Solidus maintainer rebased the branch on October 1. The first approval said: "I don't understand all these languages, but what I can understand looks correct." A second approval followed on October 7, and it merged on October 8.
What held
- The check is theirs. On 2026-10-13,
main(84f92a7) hasi18n/spec/i18n_spec.rband the base-locale path ini18n/i18n-tasks.yml. Thesolidus_i18njob intest.ymlruns it, and the Test workflow is green on that commit. A new translation that drops a variable now fails Solidus's own CI. It needs nothing from npm and has nothing to do with my tool. - Still open: the 19-locale
%{names}flash, the booleansay_yes/say_no, and the duplicate keys from the September 22 comment. The stale translations are listed in the ignore file, so they're visible.
What this says about split locale trees
The interesting bug here wasn't in any string. It was a check that ran, passed, and couldn't fail. Any setup where the source locale lives somewhere other than the translations (a gem, a package, a Crowdin export) risks the same thing. Worth one sanity test: break a placeholder on purpose and confirm CI goes red.
Timeline
| date (UTC) | event |
|---|---|
| Sep 16 09:30 | #6626 opened: 13 pt-BR strings |
| Sep 16 15:26–15:42 | three approvals |
| Sep 16 16:07 | #6626 merged |
| Sep 19 18:13 | #6637 opened: nl + pl %{names} |
| Sep 21 23:31 | maintainer re-runs the unrelated failing check |
| Sep 22 16:56 | #6637 merged (3 approvals) |
| Sep 22 17:37 | re-scan of main posted |
| Sep 30 14:26 | #6655 opened: i18n-tasks base locale + spec + 13 locale files |
| Oct 1 21:21 | maintainer rebases the branch |
| Oct 2 18:54 | first approval |
| Oct 7 16:25 | second approval |
| Oct 8 23:06 | #6655 merged |
| Oct 13 | re-checked on main: spec and config present, CI green |
I maintain Shipi18n, the open-source checker that found the first strings. The check Solidus runs today is i18n-tasks, their existing tool. The full list of repos and strings is at shipi18n.com/oss.