Internationalising a Rails store for many markets
Selling across countries means translating not just your interface but your data, your URLs, your formats, and your SEO. A deep-dive on Rails i18n at scale — locale files, translated content, locale routing, and the traps.
Internationalising an application is one of those tasks that everyone underestimates. “Translate the labels” is maybe 20% of the work; the rest is the data, the URLs, the number and date formats, the pluralisation rules, and the SEO — each with its own traps. For an e-commerce store selling across markets, getting this right is the difference between feeling local in every country and feeling like a clumsy machine translation. Here is what the full job actually involves in Rails.
The foundation: I18n and locale files
Rails ships with the I18n gem, and the basic mechanism is sound: text lives in locale
files keyed by a locale, and you look it up by key:
# config/locales/en.yml
en:
cart:
title: "Your basket"
item_count:
one: "%{count} item"
other: "%{count} items"
# config/locales/pl.yml
pl:
cart:
title: "Twój koszyk"
item_count:
one: "%{count} produkt"
few: "%{count} produkty"
many: "%{count} produktów"
<h1><%= t("cart.title") %></h1>
<p><%= t("cart.item_count", count: @cart.size) %></p>
Two things to internalise immediately. First, pluralisation is not just one/other.
English has two plural forms; Polish has four (one/few/many/other) with non-obvious rules;
Arabic has six. The count: interpolation and per-locale plural keys handle this if you
supply the right forms — hard-coding “1 item / N items” logic in your views breaks the
moment you add a Slavic language. Let the I18n pluralisation machinery do it. Second, keep
keys semantic (cart.title), not text-derived, so translators and code stay decoupled.
Translating your data, not just your chrome
Here is the part that separates real internationalisation from a translated menu bar: in a
store, the content needs translating too. Product names, descriptions, category names,
CMS pages — this is data in your database, not strings in a YAML file, and it needs a
per-locale value. The common Rails answer is a gem like globalize or mobility that
stores translations in side tables:
class Product < ApplicationRecord
translates :name, :description # name/description vary by locale
end
I18n.locale = :pl
product.name # => the Polish name
I18n.locale = :en
product.name # => the English name
This is a genuine data-modelling decision with consequences: translations live in separate tables keyed by record and locale, which affects your queries, your search indexing (you now index per locale), and your admin UI (editors need to enter each language). Plan for it — content translation is usually the largest and most ongoing part of i18n, because every new product needs translating, forever, while the interface strings are written once.
Locale in the URL: routing and SEO
How a visitor’s locale is chosen and reflected in the URL is both a UX and an SEO decision, and getting it wrong quietly wrecks your search visibility. The robust patterns put the locale in the URL so each language has its own addressable, crawlable pages:
# path-based: /en/products, /pl/products
scope "/:locale", locale: /en|pl|de/ do
resources :products
end
# set it from the URL on every request
before_action :set_locale
def set_locale
I18n.locale = params[:locale] || I18n.default_locale
end
The alternatives are a locale subdomain (pl.shop.com) or a country-code top-level domain
(shop.pl) — the right choice depends on how distinctly you want markets branded and how
your SEO is structured. What you must not do is serve different languages at the same
URL based on a cookie or Accept-Language header alone: search engines then cannot index
each language separately, and a user cannot share a link in their language. The
non-negotiable SEO requirement is hreflang tags telling search engines which URL
serves which language, so Google shows the right one and does not treat your translations
as duplicate content:
<link rel="alternate" hreflang="en" href="https://shop.com/en/product/42" />
<link rel="alternate" hreflang="pl" href="https://shop.com/pl/product/42" />
Formats: numbers, dates, and money are localised too
A market does not feel local until the formats are right. 1,234.56 in the UK is
1 234,56 in Poland and 1.234,56 in Germany; date order, currency symbol placement, and
first day of the week all vary. Rails localises these through I18n too:
l(Date.today, format: :long) # localised date
number_to_currency(price) # respects the locale's format
This dovetails with the multi-currency work: the currency (what you charge) and the
format (how you display a number) are separate concerns, both locale-aware. A German
customer might pay in EUR shown as 19,99 € while a French one sees 19,99 € with
different spacing — same currency, different formatting, both driven by locale.
The traps worth naming
A few failure modes that bite teams doing this for the first time:
- Missing translations failing silently or loudly. Decide your policy: in production
you usually want a sensible fallback (to a default locale) rather than a raw key shown
to a user; in development and CI you want missing keys to fail, so you catch them
before they ship. The
i18n-tasksgem finds missing and unused keys. - Concatenating translated fragments. Building a sentence by joining translated words
(
t(:you_have) + count + t(:items)) produces garbage in languages with different word order. Translate whole phrases with interpolation, never assemble them from parts. - Hard-coded English in flashes, mailers, validations, and PDFs. The strings hide in the corners — error messages, emails, generated documents. A real i18n pass has to reach all of them, not just the views.
- Text expansion in the UI. German runs ~30% longer than English; a button that fits in English overflows in German. Design and test layouts with the longest language.
Verdict
Internationalising a store is far more than translating labels — it is translating your
data, structuring your URLs for both users and search engines, localising every
format, respecting each language’s pluralisation, and chasing hard-coded English out
of every corner. Lean on Rails’ I18n machinery for the interface and formats, use a
translation gem for database content, put the locale in the URL with hreflang for SEO,
and adopt tooling (i18n-tasks) to keep your keys honest. Treat content translation as
the ongoing cost it is, plan for it in the data model, and your store can feel genuinely
local in every market rather than like English with the words swapped. The work is
substantial, but for a business selling across borders it is the difference between
looking native and looking foreign.