Liquid filters
Full reference for t, translate, and pluralize.
Every piece of visible text in a theme — button labels, empty-state messages,
editor setting labels — should come from a translation key, not be hard-coded in
a section. Keys live in the theme’s locales/ folder, and the engine resolves
them to the shopper’s locale at render time. This page explains the file layout,
the difference between storefront and schema strings, and the two ways you pull a
string out: the t filter in Liquid and the t: prefix in schema JSON.
locales/ folderTranslation files live in locales/ at the root of the theme (alongside
templates/, sections/, and config/ — see
Theme architecture). There are four
kinds of file, split along two axes: who reads the string (storefront vs the
editor’s schema), and whether the file is the default fallback for its
language.
| File | Used for | Role |
|---|---|---|
{locale}.json | Storefront strings (rendered by t) | Translation for that locale |
{locale}.default.json | Storefront strings | The default file for that language — also the source the editor reads when generating translation entries |
{locale}.schema.json | Editor/schema strings (t: labels, info, options) | Translation for that locale |
{locale}.default.schema.json | Editor/schema strings | The default schema file for that language |
{locale} is a language tag such as en, en-IN, or hi. A minimal theme
ships just two files — en.default.json and en.default.schema.json — and that
is enough for a single-language store.
The two groups never mix, and they are loaded separately:
.json, .default.json) are everything a shopper
sees — {{ 'products.product.add_to_cart' | t }}, cart copy, account-page
labels. These are resolved by the t / translate Liquid filter while a page
renders..schema.json, .default.schema.json) are everything the
theme editor shows you — the labels, info help text, content, and
option labels in a {% schema %}. These are referenced with the t: prefix
(covered below) and resolved when the editor builds its settings panels.Keeping them apart means a key like products.product.title can exist in your
storefront file without bloating the editor bundle, and an editor label like
t:settings.heading never leaks into rendered HTML.
t (translate) filtert and translate are the same filter — use whichever reads better. Pass a
dotted key that matches the structure of your storefront JSON:
{{ 'products.product.add_to_cart' | t }}{ "products": { "product": { "add_to_cart": "Add to cart" } } }If the key is missing in every file in the fallback chain, the filter returns the
key itself rather than throwing — so a typo shows up as literal
products.product.add_to_cart on the page, which is easy to spot.
Pass named arguments and reference them with {{ name }} inside the string:
{{ 'customer.greeting' | t: name: customer.first_name }}{ "customer": { "greeting": "Welcome back, {{ name }}" } }By default the rendered string is HTML-escaped in full, so any markup in the
translation shows as literal text. To allow HTML in a translation — for example a
string that wraps part of itself in a link — end the key with _html. For a
key ending in _html, the surrounding markup is rendered as HTML; the values you
interpolate into it are still escaped, so a {{ name }} you pass in can’t inject
tags:
{ "cart": { "terms_html": "I agree to the <a href=\"{{ url }}\">terms</a>" } }Pass count and the filter picks the right plural form. Provide the forms as an
object whose entries can include zero, one, and other (plus any
CLDR plural category the locale uses, selected via
Intl.PluralRules):
{{ 'cart.items_count' | t: count: cart.item_count }}{ "cart": { "items_count": { "zero": "Your cart is empty", "one": "{{ count }} item", "other": "{{ count }} items" } }}With cart.item_count of 3, this renders 3 items. The matching form is then
interpolated, so {{ count }} is available inside it.
t: convention in schemasInside a {% schema %}, you don’t call the t filter — you set a value to a
string that starts with t: followed by a key in your schema locale files.
The editor resolves it to the active locale before drawing the panel. It works
for label, info, content, and the label of each select / radio
option:
{ "name": "t:sections.featured.name", "settings": [ { "type": "text", "id": "heading", "label": "t:sections.featured.heading.label", "info": "t:sections.featured.heading.info" }, { "type": "select", "id": "layout", "label": "t:sections.featured.layout.label", "options": [ { "value": "grid", "label": "t:sections.featured.layout.grid" }, { "value": "list", "label": "t:sections.featured.layout.list" } ] } ]}{ "sections": { "featured": { "name": "Featured products", "heading": { "label": "Heading", "info": "Shown above the grid" }, "layout": { "label": "Layout", "grid": "Grid", "list": "List" } } }}A value that doesn’t start with t: is treated as a literal string and shown
as-is, so you can mix translated and plain labels freely. If a t: key can’t be
found, the editor falls back to showing the key text — another easy-to-spot
signal that the entry is missing from your schema files.
When you ask for a key in a given locale, the engine tries a sequence of files
and returns the first match. For a request locale like en-IN, the storefront
chain is, in order:
locales/en-IN.json, then locales/en-IN.default.json
locales/en.json, then locales/en.default.json (the base language,
en-IN stripped to en)
locales/en.default.json, then locales/en.json (an explicit final
fall-through to English)
Schema strings follow the identical chain with the .schema.json /
.default.schema.json variants. The practical upshot: en is always the
ultimate fallback, so as long as your English default files are complete, no
key ever renders as a raw key in production — even for a partially-translated
locale.
For each request the engine resolves one active locale, in this order:
An explicit locale passed to the renderer. This is how the editor and preview
render in a chosen language (the editor preview endpoint also accepts a
?locale= query parameter); it is not used by the public storefront.
A published locale prefix in the URL path — for example
/hi/products/.... A published non-primary locale is served from its
lowercase prefix; the primary locale is never duplicated behind a prefix.
The store’s primary locale, used when nothing else is specified.
The resolved tag is what t and t: look up, and it’s also exposed to Liquid on
the request and localization objects (see
Liquid objects).
Use the localization form rather than constructing a POST URL. The tag keeps
the current locale prefix and adds CSRF protection:
{% form 'localization', id: 'LanguageForm', return_to: request.path %} <select name="locale_code" onchange="this.form.submit()"> {% for language in localization.available_languages %} <option value="{{ language.iso_code }}" {% if language.iso_code == localization.language.iso_code %}selected{% endif %} > {{ language.endonym_name | default: language.name | escape }} </option> {% endfor %} </select> <noscript><button type="submit">Change language</button></noscript>{% endform %}The generated action posts to /localization. locale_code must name a
published locale (locale and language_code are accepted aliases). When the
buyer changes language, Sellerlane replaces an existing published prefix,
removes it for the primary locale, and preserves the remaining path, query, and
fragment. return_to can be back or a same-store relative path; external and
protocol-relative values are rejected.
Product, collection, page, blog, article, search, filter, pagination, and form URLs are generated with the active prefix. Canonical and hreflang links follow the same published-locale mapping. Language selection does not change country, catalog, currency, or Shopify Markets state.
Add the key to locales/en.default.json (storefront) or
locales/en.default.schema.json (editor), nested to match how you’ll
reference it.
Reference it — {{ 'your.key' | t }} in a .liquid file, or
"label": "t:your.key" in a {% schema %}.
Save or import the theme. Schema strings are validated on save; a
locales object that isn’t a JSON object is rejected. See
Edit theme code and
Import a theme ZIP.
Verify by rendering the page (or opening the editor). A raw key on screen means the lookup missed — check the spelling and that the entry lives in the right file kind.
| Symptom | Likely cause |
|---|---|
A literal key like products.product.add_to_cart renders | The key is missing from the storefront files in the fallback chain, or it’s misspelled |
A t: label shows the key text in the editor | The key is missing from the .schema.json files, or it was put in a storefront .json file by mistake |
{{ name }} shows up literally in output | You referenced an argument you didn’t pass — check the named args on the filter call |
| HTML in a translation renders as escaped text | The key doesn’t end in _html; rename it or move the markup into the template |
A /xx/ URL 404s | The locale is not published (or the primary locale was incorrectly prefixed); only real published non-primary prefixes are routable |
Liquid filters
Full reference for t, translate, and pluralize.
Setting types
Where t: labels are used — see setting types.
Theme architecture
How locales/ fits with the rest of the theme structure.
Section schema
Section-scoped locales and t: labels in section schema.