Skip to main content
← Back to Blog
·Oscar Green

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.

i18ncase-studyopen-sourcerailsi18n-tasks

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:

keyenpt-BR before
spree.inventory_error_flash_for_insufficient_quantity%{names} became unavailable.Um item no seu carrinho está indisponível
spree.reimbursement_mailer…total_refundedTotal refunded: %{total}Total Restituído
spree.shipment_detailsFrom %{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:

  1. Config: i18n/i18n-tasks.yml now also reads ../core/config/locales/%{locale}.yml, so en is the base.
  2. Spec: i18n/spec/i18n_spec.rb, the same interpolation check core already had, now for the 39 translations.
  3. Fixes for what the check found: 19 findings on main with 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::MissingInterpolationArgument instead of showing a message.
    • fr: order confirmation and cancellation emails contained %{total} / %{subtotal}, but the views call t('.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.

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) has i18n/spec/i18n_spec.rb and the base-locale path in i18n/i18n-tasks.yml. The solidus_i18n job in test.yml runs 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 boolean say_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:42three approvals
Sep 16 16:07#6626 merged
Sep 19 18:13#6637 opened: nl + pl %{names}
Sep 21 23:31maintainer re-runs the unrelated failing check
Sep 22 16:56#6637 merged (3 approvals)
Sep 22 17:37re-scan of main posted
Sep 30 14:26#6655 opened: i18n-tasks base locale + spec + 13 locale files
Oct 1 21:21maintainer rebases the branch
Oct 2 18:54first approval
Oct 7 16:25second approval
Oct 8 23:06#6655 merged
Oct 13re-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.

O

Oscar Green

The Shipi18n team builds tools that help developers ship multilingual apps faster. We write about i18n best practices, localization automation, and translation engineering.