---
title: "Translate product information"
source: https://support.storeconnect.com/articles/translate-product-information
type: article
format: markdown
site: StoreConnect Support — product and developer documentation for StoreConnect
site_index: https://storeconnect.com/llms.txt
docs_index: https://support.storeconnect.com/llms.txt
note: Append .md to any page or article URL on this site to get its Markdown form.
---
# Translate product information

[Theme locales](language-localization) translate the strings in your theme. They do not reach product information: `Name`, **Display Name** (`s_c__Display_Name__c`) and **Summary** (`s_c__Summary_Markdown__c`) live on the `Product2` record, which is shared by every store in the org. A store serving another language shows translated buttons and navigation around untranslated product names.

This process keeps **one product record per SKU** and stores the translated copy alongside it as JSON, so price, inventory and reporting stay single. Adding another language adds a key to the JSON rather than a second catalog.

## Before you begin

- A store whose **Locale** is set, and a theme locale for that language. See [Language localization](language-localization).
- Permission to add a custom field to `Product2` and to create a custom data mapping.

## Add a field to hold the translations

1. On `Product2`, create a **Long Text Area** custom field. This example uses `Translations__c` with a length of 32,768.
2. Give the field **read** access on the permission set your StoreConnect sync user holds, and on the profile of anyone who will edit it. A field with no access is not merely blank in Liquid: SOQL reports `No such column`, which reads like the field was never created.
3. Add a [custom data mapping](liquid-custom-data-fields) for the object and field, with access level **Read**.

Adding a mapping starts a backfill, so allow time before the value appears on the storefront.

## Store the copy as JSON keyed by locale

Populate the field with one key per locale code, matching the **Locale** value on the store:

```json

{
  "es_MX": {
    "display_name": "Chamarra impermeable de senderismo",
    "summary": "Chamarra impermeable y resistente, ideal para caminar en condiciones climáticas severas."
  }
}
```

Use the store's full **Locale** value as the key. A store set to `es_MX` does not match a JSON key of `es`, and the mismatch is silent: the lookup returns nothing, and the page renders the untranslated fallback with no error to explain why.

This shape is a convention this process introduces, not a platform contract. Pick your own key names if they suit your catalog better, as long as the template reads the same ones.

Load these with Data Loader or a script rather than by hand. A machine translation service is a reasonable way to generate the values, run once against the catalog. Translating on each page render adds latency and cost to every view, and leaves the page dependent on that service being up.

## Read the JSON in your template

Parse the field with [`deserialize`](deserialize-filter-reference), select the current store's locale, and fall back to the untranslated field:


```liquid

{%- assign translations = current_product.data['Translations__c'] | default: '' | unescape | deserialize -%}
{%- assign locale_copy = translations[current_store.locale] -%}
{%- assign product_name = current_product.name -%}
{%- if locale_copy.display_name != blank -%}
  {%- assign product_name = locale_copy.display_name -%}
{%- endif -%}

<h1>{{ product_name }}</h1>
```


The field name is matched case-insensitively, so `Translations__c` and `translations__c` both resolve.

### Unescape before you deserialize

A mapped Long Text Area reaches Liquid **HTML-escaped**: the quotes in the stored JSON arrive as `&quot;`. `deserialize` cannot parse that, and it does not raise. It returns an empty value, every lookup off it is blank, and the template renders the fallback with no error to explain why.

`| unescape` before `| deserialize` is what makes the parse succeed. If a `deserialize` call returns nothing, test the filter against a literal on the same page to tell a parsing problem apart from a data problem:


```liquid

{%- assign check = '{"name":"Dog & Cat"}' | deserialize -%}
{{ check.name }}
```


A literal that parses while the field does not means the value is escaped.

### Guard the field before you unescape

`| default: ''` before `| unescape` is not optional. A product with no value in the mapped field is the ordinary case while a catalog is part-translated, and passing that empty value into `unescape` causes the `{% if %}` tests that follow to be skipped. The assignments inside them never run, the variable keeps whatever it held before, and nothing is raised.

Where the product genuinely has no translation, the page comes out right by accident. Where a later branch would have supplied one, it does not. This is what breaks the variant fallback below, which reads the variant's own field first and only then looks to the master: the variant's blank field skips the test that would have reached the master's translation, so the page renders the untranslated summary and looks finished.

Keep the guard on every chain that reads a mapped field. It changes nothing on a product that has a value.

### Translate the summary through a different accessor

The summary needs its own handling, because the drop has no `summary` attribute. It exposes **`summary_content`**, which reads `s_c__Summary_Markdown__c` and returns rendered HTML. A value taken from your JSON is plain text, so pass it through `markdown` to match:


```liquid

{%- assign product_summary = current_product.summary_content -%}
{%- if locale_copy.summary != blank -%}
  {%- assign product_summary = locale_copy.summary | markdown -%}
{%- endif -%}

<div>{{ product_summary }}</div>
```


Apply `markdown` to the JSON value only. Running it over `summary_content`, which is already rendered, mangles the output.

The `markdown` filter needs a recent platform version. On an older store it is not registered, and an unregistered filter raises rather than rendering blank, so the page fails outright instead of quietly falling back. Update your store to the current release before using this pattern.

### Match the platform's inheritance on variants

If your catalog uses master and variant products, the platform and your custom field inherit differently, and the gap between them puts untranslated text on a translated page.

| Value | On a variant whose own value is blank |
|---|---|
| `name` | Reads the variant's own **Display Name**, falling back to the variant's own `Name`. Never the master's. |
| `summary_content`, and `downloads_content`, `features_content`, `specifications_content`, `support_content`, `warranty_content` | Falls back to the master product's value. |
| `data['Translations__c']`, and any other custom field | Stays blank. Custom data is read from that record only. |

The name is consistent: the platform reads the variant's own value and so does your translation, so a missing translation shows the variant's own name rather than the master's. The summary is not. The platform inherits the master's summary while your translation does not inherit the master's JSON, so a variant with no summary of its own and no translation of its own renders the master's untranslated summary. The page looks complete, in the wrong language, with nothing to indicate a translation was missing.

Mirror the platform field by field. Read the name from the variant's own JSON, and let the summary fall back to the master's JSON in the same way `summary_content` falls back to the master's summary:


```liquid

{%- assign summary_copy = locale_copy.summary -%}
{%- if summary_copy == blank -%}
  {%- if current_product.variant? -%}
    {%- assign master_translations = current_product.master.data['Translations__c'] | default: '' | unescape | deserialize -%}
    {%- assign master_copy = master_translations[current_store.locale] -%}
    {%- assign summary_copy = master_copy.summary -%}
  {%- endif -%}
{%- endif -%}

{%- assign product_summary = current_product.summary_content -%}
{%- if summary_copy != blank -%}
  {%- assign product_summary = summary_copy | markdown -%}
{%- endif -%}
```


`current_product.master` is itself a product, so it exposes `data` the same way. Guard the lookup with `variant?`, because a master has no master of its own.

Giving every variant its own JSON also works and needs no fallback logic. It costs a translation for each variant of each product, which is worth it only where variants genuinely differ in description rather than in size or color.

### Repeat the lookup in every snippet that shows a name

A product name renders in more places than the detail page, and the lookup does not travel to them on its own. `{% render %}` does not inherit the caller's assigns, so a variable assigned in a page template is not visible inside the snippet it renders. The base theme renders listing cards this way, from `snippets/products/cards`:


```liquid

{%- cache "product", items: [product, current_store, current_customer] -%}
  {%- render "products/card", product: product, allow_comparisons: allow_comparisons %}
{%- endcache -%}
```


Two things follow from that. The lookup has to live inside the snippet that outputs the name, not in the template that renders the snippet. And inside a card the product is the parameter `product`, not `current_product`: `current_product` is empty on any page that is not a product detail page, so pasting the block above into a card renders a blank title with no error text.

The cache block is not what hides the variable, and moving an assign inside it does not help. `{% render %}` is what isolates the scope.

To avoid maintaining the same six lines in several templates, put the lookup in one snippet of your own that takes the product as a parameter and outputs the translated name, then render that snippet from the product page and the card.

### Prefer an explicit condition to a default

Test the translated value with `{% if ... != blank %}` rather than chaining `| default:` off it. The explicit form states the fallback, and it behaves the same whether the locale is absent, the key is absent, or the mapping has not finished backfilling.

This is a different use from the `| default: ''` above. That one guards the raw field on its way into `unescape`, before anything has been parsed. This one is about choosing what the reader sees once the lookup has run.

## Verify

1. Open a product page on the translated store and confirm the name and summary are translated.
2. Open the same product on a store using the original language and confirm it is unchanged. Both read the same product record, so this is the check that the fallback works.
3. Open a category page and confirm the listing cards are translated as well as the detail page.
4. If your catalog uses variants, open a variant that has no summary of its own and confirm its summary is translated. An untranslated summary here means the master fallback is not being reached.

If a product renders in the original language, confirm the locale code in the JSON matches the store's **Locale** exactly, then see [Verify custom data is available in Liquid](verify-custom-data-in-liquid).

## What this does not cover

Purchase button labels are not part of this. They come from the **Add to Cart Text** and **Buy it Now Text** fields, which resolve in order: the price book entry, then the price book (`Pricebook2`), then the `sc.products.default_text.add_to_cart` and `sc.products.default_text.buy_it_now` locale keys. Setting the text on the price book that serves the translated store covers the whole catalog at once. Leave both field levels blank to fall through to the locale keys, then translate those on the theme locale, and allow a few minutes and a cache refresh before checking.

---

## Follow StoreConnect

- [Email Newsletter](https://storeconnect.com/c/lp-newsletter)
- [LinkedIn Newsletter](https://www.linkedin.com/build-relation/newsletter-follow?entityUrn=7444956928444862464)
- [YouTube](https://www.youtube.com/channel/UCngKdP2x8l1wcbAKW3tvU8g)
- [LinkedIn](https://www.linkedin.com/company/storeconnect)
- [X / Twitter](https://x.com/storeconnecthq)

## Popular Links

- [Partners](https://storeconnect.com/partners)
- [Become a Partner](https://storeconnect.com/become-a-partner)
- [News](https://storeconnect.com/articles/news)
- [Events](https://storeconnect.com/articles/events)
- [Live Events](https://storeconnect.com/live-events)
- [Feature Comparison](https://storeconnect.com/how-we-compare)
- [Download a free trial](https://appexchange.salesforce.com/appxListingDetail?listingId=a0N3A00000FMkeKUAT)
- [Book a Demo](https://storeconnect.com/contact)

## Documentation

- [Help documentation](https://support.storeconnect.com/help-documentation)
- [AI agents](https://support.storeconnect.com/ai)
- [Videos & tutorials](https://support.storeconnect.com/videos-tutorials)
- [Developer reference](https://support.storeconnect.com/developer-reference)
- [Release notes](https://support.storeconnect.com/release-notes)
- [Troubleshooting](https://support.storeconnect.com/troubleshooting)
- [Trust Center](https://trust.getstoreconnect.com/)
- [Status Page](https://status.storeconnect.com/)

## Contact

- info@getstoreconnect.com
- US +1 415 745 3230
- AUS +61 2 8365 2308

100 S Ashley Dr, Suite 600-2461
Tampa FL 33602-600 USA

Level 22, Sydney Place
180 George Street
Sydney, NSW, 2000, AUS

## Machine-readable

- [Site index for agents](https://storeconnect.com/llms.txt): curated map of the StoreConnect site in llms.txt format
- [Documentation index for agents](https://support.storeconnect.com/llms.txt): full technical and product documentation map

Every page and article on this site has a Markdown rendering: append `.md` to its URL.

Continue in Markdown: [Help documentation](https://support.storeconnect.com/help-documentation.md) · [Developer reference](https://support.storeconnect.com/developer-reference.md) · [Videos & tutorials](https://support.storeconnect.com/videos-tutorials.md) · [Release notes](https://support.storeconnect.com/release-notes.md)

---

StoreConnect Support — https://support.storeconnect.com/articles/translate-product-information