"Translation missing" in a theme app extension
Your app block renders Translation missing: en.Find my size on the storefront — with your own text sitting after the locale prefix. That detail tells you exactly what's wrong, and the fix is one line of Liquid.
What the error is actually telling you
Liquid's t filter outputs Translation missing: <locale>.<key> when it can't resolve a key. So if you're seeing your own English sentence after the en., then that sentence was passed to t as a translation key.
Which means somewhere you wrote this:
{{ block.settings.button_label | t }} {# WRONG — merchant text is data, not a key #}
The rule
Never pipe a merchant-editable setting through | t. A settings value is content the merchant typed. The t filter is for keys you defined in your locale files. The moment a merchant types anything, the lookup fails and their own words get echoed back inside an error string.
The fix that keeps every language working
You want a default that's translated in all your locales, but which a merchant can override in their own words. Leave the setting's default blank, and fall back to a real translation key when it's empty:
{%- liquid
assign label = block.settings.button_label | strip
if label == blank
assign label = 'sizecast.button' | t
endif
-%}
<button type="button">{{ label }}</button>
With the schema leaving the default empty:
{
"type": "text",
"id": "button_label",
"label": "t:settings.button_label",
"info": "Leave empty to use the translated default."
}
Now every storefront language gets its own translation for free, and a merchant override wins everywhere — with no error string possible in either direction.
Why "default": "t:..." doesn't save you
In theme sections, translatable defaults in the schema work. In theme app extensions they do not resolve — the editor shows a translation-missing value even when the identical key works correctly for a label. This has been reported since app blocks launched and is still the behaviour.
So: t: is for editor-facing strings only (name, label, info, select options), resolved from your *.schema.json. Storefront strings use | t from *.json. Defaults are either a plain string or blank.
The two locale files, and which is which
| File | Audience | Consumed by |
|---|---|---|
locales/en.default.json | Your customers | {{ 'key' | t }} in block Liquid, at storefront render |
locales/en.default.schema.json | The merchant | "t:..." references inside {% schema %} |
Ship both from day one — the CLI will only let you add a storefront locale file if a schema locale file already exists. Exactly one .default per type, and the | t lookup is scoped to your extension: the theme's locale files are invisible to your block, and yours are invisible to the theme.
Two related traps worth knowing
- Put
{{ block.shopify_attributes }}on your block's root element. Without it the theme editor can't map the DOM to the block, so click-to-highlight and the select/deselect events don't target it. - The app-block JavaScript budget is measured on RAW size, not gzip. The
AssetSizeAppBlockJavaScriptcheck fails a 14 KB file that gzips to 4 KB. Minify as a build step and keep the readable source outside the extension folder — which only permitsassets/,blocks/,locales/andsnippets/. A straysrc/directory fails the deploy outright.
Related: the real Built for Shopify requirements, and five metafield and metaobject traps that fail silently.
This came out of shipping a real app
We build theme app extensions that render in pure Liquid on the storefront and localise properly — including Arabic with full RTL, where most extensions quietly break.
Book a call →