Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.launchmystore.io/llms.txt

Use this file to discover all available pages before exploring further.

Locales

Every user-facing string in a LaunchMyStore theme — button labels, error messages, ARIA copy, schema names shown in the editor — lives in the locales/ directory and is translatable across any number of languages. This page covers:
  1. The file layout under locales/.
  2. The {{ 'key' | t }} filter for translating storefront copy.
  3. The t: prefix for translating schema labels.
  4. How translations are resolved at install time and at render time.
  5. How the active locale is selected per request.
  6. Translation for navigation menus (linklists).

File layout

A theme’s locales/ directory holds two parallel families of files:
locales/
├── en.default.json           ← storefront copy, fallback locale
├── en.default.schema.json    ← schema labels, fallback locale
├── fr.json                   ← storefront copy, French
├── fr.schema.json            ← schema labels, French
├── de.json
├── de.schema.json
├── es.json
└── es.schema.json
Two suffix patterns matter:
SuffixContent
<lang>.jsonStorefront copy — every string emitted via {{ 'key' | t }}.
<lang>.schema.jsonSchema labels — every "label", "info", "name", and "default" referenced via "t:..." keys in section/block schemas.
Two filename markers matter:
MarkerMeaning
.defaultExactly one locale per theme is marked default (typically en.default.json + en.default.schema.json). Missing keys in other locales fall back to the default.
(no .default)Regular locale file. Used when the request’s active locale matches.
A theme must have an en.default.json (or equivalent default-marked file). It should have an en.default.schema.json for schema labels. Other locales are optional and inherited from the default whenever a key is missing.

Translating storefront copy: the t filter

In templates, snippets, sections, and blocks, translate a string with the t filter:
<button class="btn">{{ 'general.cart.add_to_cart' | t }}</button>

<input
  type="search"
  placeholder="{{ 'general.search.placeholder' | t }}"
  aria-label="{{ 'accessibility.search_input' | t }}"
>

<p>{{ 'products.product.sold_out' | t }}</p>
The key is a dot-delimited path into the locale JSON. Given:
// locales/en.default.json
{
  "general": {
    "cart": {
      "add_to_cart": "Add to cart",
      "checkout":   "Check out"
    },
    "search": {
      "placeholder": "Search the store"
    }
  },
  "accessibility": {
    "search_input": "Search input"
  },
  "products": {
    "product": {
      "sold_out": "Sold out"
    }
  }
}
…the templates above render Add to cart, Search the store, Search input, and Sold out respectively.

Interpolation

A translation value can include {{ name }} placeholders that the t filter fills in from keyword arguments:
{
  "products": {
    "product": {
      "save_amount": "Save {{ amount }}",
      "review_count": "{{ count }} reviews"
    }
  }
}
{{ 'products.product.save_amount' | t: amount: discount }}
{{ 'products.product.review_count' | t: count: product.reviews_count }}

Pluralisation

Provide an object with one / other (CLDR-compliant) keys; pass the count argument and the filter selects the right form:
{
  "accessibility": {
    "filter_count": {
      "one":   "{{ count }} filter applied",
      "other": "{{ count }} filters applied"
    }
  }
}
{{ 'accessibility.filter_count' | t: count: filters.size }}
count: 11 filter applied. count: 55 filters applied. Other CLDR forms (zero, two, few, many) are supported for languages that need them.

Fallback behavior

If the key doesn’t exist in the active locale, the renderer:
  1. Looks in <lang>.json.
  2. Falls back to <default>.default.json.
  3. Renders the key string itself (general.cart.add_to_cart) if still nothing found — visible but not crashed.

Translating schema labels: the t: prefix

Section and block schemas use a different syntax: any string value in the schema can be replaced with "t:..." and the platform resolves it to the corresponding translation.
{# sections/featured-collection.aqua #}
{% schema %}
{
  "name": "t:names.featured_collection",
  "settings": [
    {
      "type": "text",
      "id": "heading",
      "label": "t:settings.heading.label",
      "info":  "t:settings.heading.info",
      "default": "t:settings.heading.default"
    },
    {
      "type": "range",
      "id": "products_count",
      "label": "t:settings.products_count.label",
      "min": 2, "max": 12, "step": 1,
      "default": 4
    }
  ],
  "presets": [
    {
      "name": "t:presets.featured_collection.name",
      "settings": { "products_count": 4 }
    }
  ]
}
{% endschema %}
The matching schema-locale file:
// locales/en.default.schema.json
{
  "names": {
    "featured_collection": "Featured collection"
  },
  "settings": {
    "heading": {
      "label":   "Heading",
      "info":    "Displayed above the product grid.",
      "default": "Featured products"
    },
    "products_count": {
      "label": "Products to show"
    }
  },
  "presets": {
    "featured_collection": {
      "name": "Featured collection"
    }
  }
}
A "t:..." value can appear anywhere in the schema: name, label, info, default, options[].label, blocks[].name, presets[].name, paragraph content, header content. The resolver walks the whole schema recursively and replaces every t:-prefixed string.

Resolution order: schema vs storefront

When resolving a "t:..." key, the renderer first looks in <lang>.schema.json, then falls back to <lang>.json. This lets you keep editor-only strings (section names, preset descriptions) separate from storefront copy without duplicating them.
// locales/en.default.schema.json — editor-only
{
  "names": { "featured_collection": "Featured collection" }
}
// locales/en.default.json — storefront strings
{
  "products": { "product": { "sold_out": "Sold out" } }
}
A "t:names.featured_collection" schema label resolves out of the schema file; a {{ 'products.product.sold_out' | t }} template call resolves out of the storefront file.

Resolution lifecycle

Schema t: keys are resolved in two places:

1. At install time

When the theme is uploaded, the installer parses every section and block schema, runs expandSchemaTranslations() over each one, and writes the resolved values into config/schema-index.json. This is the index the theme editor reads to populate its UI — the merchant sees real labels, not t: keys. The installer also flattens the active default locale into the index so later lookups don’t have to re-parse the locale JSON. For app extensions that ship their own blocks (storefront app blocks), expandSchemaTranslationsForApp() performs the same expansion against the app’s bundled locales/<lang>.json file. See src/pages/api/apps/_localManifests.js for the implementation — specifically expandSchemaTranslations() and the helper expandSchemaTranslationsForApp(schema, domainSlug, appHandle, locale).

2. At render time

During a page render, the renderer reads locales/<lang>.schema.json and locales/<lang>.json via AssetLoader.readJSON() (cached), merges them, and caches the merged result keyed by theme directory in AssetLoader.schemaTranslationsCache (12-hour TTL, invalidated on theme republish). Any t: key still present in a schema is resolved against this merged map. The merge is { ...mainTranslations, ...schemaTranslations } — schema-specific keys take precedence over storefront copy if there’s a namespace collision.
A t: key that can’t be resolved at install or render time will be displayed in the editor as the literal key string. That’s the symptom to watch for: if you see t:settings.heading.label in the theme editor’s sidebar, it means a key is missing from your .schema.json files.

Locale detection

The renderer picks the active locale for a request from the following sources, in order. The first match wins.
SourceNotes
?locale=<code> queryUsed by the theme editor and section-AJAX fetches.
locale_code cookieSet on first visit by middleware based on auto-detection.
Accept-Language headerFalls back to the primary language tag (e.g. fr-CA → fr).
Theme defaultThe locale whose filename ends in .default.json.

Auto-detection on first visit

For a brand-new visitor, middleware reads the cf-ipcountry header (populated by Cloudflare’s edge), maps it to a default language, and combines it with the user’s Accept-Language header. The result is written to three cookies:
CookieHolds
country_codeTwo-letter ISO country code (e.g. "US", "AE").
currency_codeISO-4217 currency code (e.g. "USD", "AED").
locale_codeTwo-letter language code (e.g. "en", "ar").
Subsequent requests use the cookies directly — middleware doesn’t re-detect.

Exposing the locale in templates

The active locale is exposed via request.locale:
<html lang="{{ request.locale.iso_code }}">
For form submissions that need a hidden locale field, the form_locale filter or a direct render of request.locale.iso_code will both work.

Available locales

A theme’s available locales are inferred from the filenames present in locales/. Use them to render a language switcher:
<form method="post" action="/localization">
  <select name="locale_code" onchange="this.form.submit()">
    {% for locale in localization.available_languages %}
      <option
        value="{{ locale.iso_code }}"
        {% if locale.iso_code == request.locale.iso_code %}selected{% endif %}
      >
        {{ locale.endonym_name }}
      </option>
    {% endfor %}
  </select>
</form>
The localization global is populated from the merchant’s enabled markets and the locales the theme ships. See localization for the full shape.
Storefront navigation menus (linklists.main-menu, linklists.footer-menu, etc.) are owned by the merchant — they’re not part of the theme bundle. Translations are managed in the admin Navigation editor, not in locales/ files. In templates, render a linklist as you normally would:
<nav>
  {% for link in linklists.main-menu.links %}
    <a href="{{ link.url }}" {% if link.active %}aria-current="page"{% endif %}>
      {{ link.title }}
    </a>
  {% endfor %}
</nav>
link.title and link.url are returned in the request’s active locale — the backend resolves translations server-side based on request.locale before handing the linklist to the renderer. Theme code never has to call | t on a linklist’s title. If you ship a fallback menu that the merchant might not have configured, guard with default_menu:
{% assign menu = linklists.main-menu | default: linklists.default-menu %}
{% for link in menu.links %}
  <a href="{{ link.url }}">{{ link.title }}</a>
{% endfor %}

Worked example: a full set of locale files

A small theme that ships English (default) and French.

locales/en.default.json

{
  "general": {
    "cart": {
      "add_to_cart": "Add to cart",
      "checkout": "Check out",
      "empty": "Your cart is empty"
    },
    "search": {
      "placeholder": "Search…",
      "no_results": "No results for {{ terms }}"
    },
    "404": {
      "title": "Page not found",
      "subtitle": "Try searching for what you're looking for."
    }
  },
  "products": {
    "product": {
      "sold_out": "Sold out",
      "save_amount": "Save {{ amount }}",
      "review_count": {
        "one": "{{ count }} review",
        "other": "{{ count }} reviews"
      }
    }
  },
  "accessibility": {
    "skip_to_text": "Skip to content",
    "cart_count": "Total items in cart",
    "menu": "Menu",
    "search_input": "Search input"
  }
}

locales/en.default.schema.json

{
  "names": {
    "featured_collection": "Featured collection",
    "rich_text": "Rich text",
    "image_banner": "Image banner"
  },
  "categories": {
    "basic": "Basic",
    "products": "Products",
    "storytelling": "Storytelling"
  },
  "settings": {
    "heading": {
      "label": "Heading",
      "default": "Featured products"
    },
    "products_count": {
      "label": "Products to show"
    },
    "columns": {
      "label": "Columns"
    }
  },
  "presets": {
    "featured_collection": {
      "name": "Featured collection"
    }
  }
}

locales/fr.json

{
  "general": {
    "cart": {
      "add_to_cart": "Ajouter au panier",
      "checkout": "Passer à la caisse",
      "empty": "Votre panier est vide"
    },
    "search": {
      "placeholder": "Rechercher…",
      "no_results": "Aucun résultat pour {{ terms }}"
    },
    "404": {
      "title": "Page introuvable",
      "subtitle": "Essayez de rechercher ce que vous cherchez."
    }
  },
  "products": {
    "product": {
      "sold_out": "Épuisé",
      "save_amount": "Économisez {{ amount }}",
      "review_count": {
        "one": "{{ count }} avis",
        "other": "{{ count }} avis"
      }
    }
  },
  "accessibility": {
    "skip_to_text": "Aller au contenu",
    "cart_count": "Total des articles dans le panier",
    "menu": "Menu",
    "search_input": "Champ de recherche"
  }
}

locales/fr.schema.json

{
  "names": {
    "featured_collection": "Collection en vedette",
    "rich_text": "Texte enrichi",
    "image_banner": "Bannière image"
  },
  "categories": {
    "basic": "Basique",
    "products": "Produits",
    "storytelling": "Narration"
  },
  "settings": {
    "heading": {
      "label": "Titre",
      "default": "Produits en vedette"
    },
    "products_count": {
      "label": "Produits à afficher"
    },
    "columns": {
      "label": "Colonnes"
    }
  },
  "presets": {
    "featured_collection": {
      "name": "Collection en vedette"
    }
  }
}
When the active locale is fr, both storefront templates and editor labels render in French. Any key missing from fr.json or fr.schema.json falls back to en.default.json / en.default.schema.json respectively.

Tips and gotchas

  • One default per theme: only one file may end in .default.json. Marking two locales as default is undefined behaviour and the installer may pick either.
  • JSON, with comments: locale files are parsed by the platform’s safe JSON parser, which accepts //-style comments. Use them generously — they’re stripped at parse time and don’t ship to the storefront.
  • Don’t translate IDs: id, type, and value fields in schemas are identifiers, not labels. The t: prefix is only meaningful on display strings.
  • Refresh after editing: when you change a .schema.json file, republish the theme (or call AssetLoader.invalidateTheme(themeId)) to rebuild the merged schema-translations cache. The editor pulls from the baked schema-index.json, which is regenerated on install.
  • App-block translations: third-party app blocks ship their own locales/<lang>.json inside the app extension. Their t: keys are resolved by expandSchemaTranslationsForApp() against that file, not the theme’s locales.

Next steps

Theme structure

Where locales/ fits in the overall theme directory.

Filters

The t, format_address, and money filters for I18n.

Schema

Where t: keys can appear in a section schema.

Objects

The request, localization, and linklists globals.