---
title: 'Storefront: Working With Translations | Shopware Community Hub'
description: >-
  Learn how to create, use, and override storefront snippets. Render
  translations safely in Twig templates and pass translated strings to
  storefront JavaScript.
canonical_url: 'https://hub.shopware.com/learn/unit/storefront-working-with-translations'
---

# Storefront: Working With Translations

<LearningObjectives>

- Understand how storefront snippets work and how to create snippet files for multiple languages.
- Learn how to use and override translations in Twig templates.
- Understand how snippet keys are resolved.
- Know how to override existing translations from plugins or the administration.
- Learn how to render translated snippets safely.
- Understand how to pass translated strings to storefront JavaScript for dynamic UI interactions.

</LearningObjectives>

# Storefront: Working With Translations

Many storefront extensions require language-specific content. In Shopware, this is handled through **snippets**, which allow you to manage translations centrally without modifying the base files.

A **translation** is the localized text that appears in the storefront. A **snippet** is Shopware's technical key-value entry that resolves to that translated text.

In this learning unit, you will learn how to create snippet files, use translations in Twig templates, safely render translated content, and pass translated strings to storefront JavaScript for dynamic UI interactions.

## Storefront Snippets and Translations

Snippets are Shopware’s translation mechanism for storefront UI text. They allow you to provide language-specific content without modifying templates or logic.

In Shopware, storefront snippets are rendered server-side using the [Symfony Translator](https://symfony.com/doc/current/translation.html).

In Twig templates, translations are applied using the [trans](https://symfony.com/doc/current/reference/twig_reference.html#trans) filter provided via `symfony/twig-bridge`.

This means that storefront snippets are fully translated server-side before the HTML is sent to the browser.

### Folder and File Structure

In Shopware, snippet files for plugins are stored under `[Your_Plugin]/src/Resources/snippet/` folder. Inside this folder, you create `.json` files for each required language.

Each snippet file uses the naming pattern `<domain>.<locale>.json` (for example, `sw-academy.de.json`, `sw-academy.en.json`, `sw-academy.en-GB.json`).

The `domain` can be freely chosen (we recommend your extension name in kebab case).

The `locale` follows IETF BCP 47 (restricted to 2-letter language codes) - for example `de`, `en`, or `es-AR`. Shopware also supports region-specific locales (such as `de-DE`). When using region-specific files, always provide the corresponding base language file as a fallback:

- `sw-academy.de-DE.json` requires `sw-academy.de.json`

Your JSON structure should be consistent across languages to avoid missing translations.

**Example: Basic structure**

```txt
[shop_root]
 └── custom
     └── plugins
         └── [your_plugin]
              └── src
                  └── Resources
                      │── app
                      │   │── administration
                      │   │   └── ...
                      │   └── storefront
                      │       └── ...
                      │── snippet
                      │   │── sw-academy.de.json
                      │   │── sw-academy.en.json
                      │   │── sw-academy.fr.json
                      │   │── sw-academy.it.json
                      │   └──...
                      └── views 
```

**Example: Organizing snippets by context**

```txt
[shop_root]
 └── custom
     └── plugins
         └── [your_plugin]
              └── src
                  └── Resources
                      │── app
                      │   │── administration
                      │   │   └── ...
                      │   └── storefront
                      │       └── ...
                      │── snippet
                      │   │── sw-academy.de.json
                      │   │── sw-academy.en.json
                      │   │── checkout // Or any other context or special cases
                      │   │   │── sw-academy-checkout.de.json
                      │   │   └── sw-academy-checkout.en.json
                      │   └── ...
                      └── views 
```

**Example: Organizing snippets by language folders**

```txt
[shop_root]
 └── custom
      └── plugins
          └── [your_plugin]
               └── src
                   └── Resources
                       │── app
                       │   │── administration
                       │   │   └── ...
                       │   └── storefront
                       │          └── ...
                       │── snippet
                       │      │── de
                       │      │   │── sw-academy-case-one.de.json
                       │      │   └── sw-academy-case-two.de.json
                       │      │── en
                       │      │   │── sw-academy-case-one.en.json
                       │      │   └── sw-academy-case-two.en.json
                       │      │── it
                       │      │   │── sw-academy-case-one.it.json
                       │      │   └── sw-academy-case-two.it.json
                       │      └── ...
                       └── views 
```

You can organize snippet files in subdirectories when it helps you keep larger extensions readable. The important things are:

- Every snippet file must be located under `[Your_Plugin]/src/Resources/snippet/` folder. Shopware detects all `.json` files inside this folder (including subdirectories) that follow the `<domain>.<locale>.json` naming pattern.
- The snippet file names must follow the `<domain>.<locale>.json` naming pattern (e.g., `sw-academy.de.json`, `sw-academy.en-GB.json`, etc.).
- Keep snippet keys consistent across languages to ensure that all translations are available.

### How Snippet Keys are Built

Snippet JSON files are structured as nested objects. Each entry consists of a snippet key (the identifier) and its translated value.

The recommended naming convention for snippet keys is camelCase, typically grouped by feature or module.

For the examples in this learning unit, assume that Shopware Academy builds a storefront extension. Therefore, the snippet files use the domain `sw-academy`, and the snippet keys use the prefix `swAcademy.*`.

This mirrors a real-world approach: Use a vendor, company, or plugin prefix to make your snippet keys unique and easier to recognize. In your own projects, replace `swAcademy.*` with a prefix that fits your extension or company.

**Example: `sw-academy.de.json`**

```json
{
  "swAcademy": {
    "pdp": {
      "customSection": {
        "headerText": "Weitere Produktdetails",
        "specialProductInfo": "Besondere Produktinformationen",
        "soldOut": "Ausverkauft"
      }
    }
  }
}
```

When providing snippet files for additional languages, **the JSON structure must remain identical**; Only the translation values differ between languages:

**Example: `sw-academy.en.json`**

```json
{
  "swAcademy": {
    "pdp": {
      "customSection": {
        "headerText": "Additional product details",
        "specialProductInfo": "Special product information",
        "soldOut": "Sold out"
      }
    }
  }
}
```

#### When to Use Shopware Core Snippet Keys

The examples above show the normal case: You create your own storefront texts and keep them under your own prefix, such as `swAcademy.*`.

Generally, avoid placing your own custom UI texts under Shopware core namespaces such as `checkout.*`, `account.*`, `detail.*`, or `listing.*`, unless you intentionally want to override existing core snippets.

But there are valid exceptions. Sometimes you intentionally provide or override Shopware-specific texts in the namespace where Shopware expects them. A common real-world example is cart error messages: If your project needs custom wording for a cart error, the snippet key must match the key that Shopware uses for that error. In that case, you are not creating a new plugin-owned UI text, but overriding or providing text for an existing Shopware mechanism.

Also treat `messages.*` as reserved/system-level translation space because `messages` is Symfony's default translation domain. See the [Symfony translation documentation](https://symfony.com/doc/current/translation.html#translation-domains) for more context.

### Using Snippets in Twig

In Shopware storefront, Twig resolves nested snippet keys using the **dot notation** together with the `trans` filter. Translations are rendered using the <code v-pre>{{ }}</code> Twig output syntax, where the snippet key is provided as a string.

For example, the following Twig expression accesses the snippet key `headerText`:

```twig
{{ 'swAcademy.pdp.customSection.headerText'|trans }}
```

During server-side rendering, Shopware resolves the snippet key based on the currently active language.

- `de` -> `Weitere Produktdetails`
- `en` -> `Additional product details`

This allows you to define translations in a **single central location**, while Shopware automatically renders the correct language in the storefront.

Depending on where the translated text is rendered, additional filters or output handling such as sanitizing may be required.

#### Storefront Context vs. Other Twig Contexts

This learning unit focuses on the storefront. In the storefront, the active language is resolved through the sales channel context, the storefront domain, and its assigned snippet set. That is why the same Twig expression can render German in one sales channel and English in another.

Other Twig-rendered areas, such as e-mails or documents, also use translated text, but they do not always use the same rendering context or the same tools as storefront pages. If a translation appears in the wrong language, first check which context renders the template: The active sales channel language, the domain's snippet set, the customer's or order's language, and the fallback language can all affect the final result.

### Output With `sw_sanitize` vs. `striptags` vs. `raw`

When rendering translated snippets, it is important to consider the **output safety** of the rendered text.

Some snippets may intentionally contain simple markup (e.g., a link). Potentially dangerous tags such as `<script>` must never be rendered and should be removed by sanitization.

You already learned `sw_sanitize` in the predecessor Frontend Development Essentials learning path. Before using it for translated output, briefly recall what it does:

<ArticleMultipleQuestionnaire>
  <ArticleQuestionnaire>
    <ArticleQuestionnaireQuestion>What is correct about Twig filter `sw_sanitize`?</ArticleQuestionnaireQuestion>
    <ArticleQuestionnaireAnswer correct>It is provided by Shopware</ArticleQuestionnaireAnswer>
    <ArticleQuestionnaireAnswer correct>It removes potentially dangerous tags and attributes (e.g., the `script` tag)</ArticleQuestionnaireAnswer>
    <ArticleQuestionnaireAnswer>It trims the translation</ArticleQuestionnaireAnswer>
    <ArticleQuestionnaireAnswer correct>It keeps only a defined set of safe HTML tags such as 'p,' 'b,' 'i,' 'a,'</ArticleQuestionnaireAnswer>
    <ArticleQuestionnaireAnswer>It keeps everything as it defined</ArticleQuestionnaireAnswer>
  </ArticleQuestionnaire>
</ArticleMultipleQuestionnaire>

In this learning unit, you will apply this existing knowledge to translated storefront text.

In most cases, it is recommended to use `sw_sanitize` to ensure that your translated text is safe to render in the storefront.

Nevertheless, there are situations where you may need different output handling. Twig provides additional filters such as the [raw](https://twig.symfony.com/doc/3.x/filters/raw.html) filter, which renders the output without escaping or filtering.

A possible use case is when your snippet intentionally contains HTML that should be rendered as-is, e.g., a link:

**Example snippet:**

```json
{
  "swAcademy": {
    "pdp": {
      "customSection": {
        "warningText": "Important update <script>alert('test')</script> Please refresh the page."
      }
    }
  }
}
```

**Twig usage:**

```twig
{{ 'swAcademy.pdp.customSection.warningText'|trans|sw_sanitize }}
{{ 'swAcademy.pdp.customSection.warningText'|trans|raw }}
```

- Using `sw_sanitize`, the `<script>` tag is removed and only a safe text remains.
- Using `raw`, the snippet is rendered exactly as defined, including the `<script>` tag (not recommended).

<Callout title="Important" type="warning">

Use the `raw` filter with caution. Only apply it when you are certain that the snippet content is trusted and safe.

</Callout>

There is also the [striptags](https://twig.symfony.com/doc/3.x/filters/striptags.html) filter. Unlike `sw_sanitize`, `striptags` removes **all HTML tags** from the output and returns a plain text.

This is especially useful when rendering translations inside HTML attributes, where HTML markup is not allowed. In an attribute, you should also escape the final translated value with `escape('html_attr')`.

This means: after the snippet was translated and possible HTML tags were removed, Twig escapes characters that are sensitive inside HTML attributes, such as quotes. This prevents the translated value from breaking the surrounding markup.

**Example snippet:**

```json
{
  "swAcademy": {
    "pdp": {
      "customSection": {
        "tooltipText": "View <strong>more details</strong> about this product"
      }
    }
  }
}
```

**Twig usage (attribute context):**

```twig
title="{{ 'swAcademy.pdp.customSection.tooltipText'|trans|striptags|escape('html_attr') }}"
```

The same pattern also applies when the translated text contains parameters:

```twig
<button
    type="button"
    aria-label="{{ stockMessageKey|trans({
        '%count%': product.availableStock,
        '%minutes%': remainingMinutes
    })|striptags|escape('html_attr') }}">
    {{ 'detail.addProduct'|trans|sw_sanitize }}
</button>
```

Use the output filter that matches the place where the translation is rendered: body text, attribute text, or intentionally allowed HTML.

**Summary:**

- `sw_sanitize`: Keeps safe HTML tags and removes potentially dangerous ones.
- `striptags`: Remove all HTML tags from the output.
- `escape('html_attr')`: Escapes the final translated value for an HTML attribute, so quotes and similar characters cannot break the markup.
- `raw`: Render the output exactly as defined without any filtering.

### Snippets With Parameters

Snippets can include placeholders. In Twig, pass a map of parameters to `trans`:

This is a useful real-world pattern when wording depends on a value. For example, `1 item` and `10 items` need different text. A simple approach is to prepare two snippet keys and choose the right key in Twig.

**Example snippet:**

```json
{
  "swAcademy": {
    "pdp": {
      "customSection": {
        "stockMessageSingular": "Only %count% item left in stock. Order within %minutes% minutes to receive it tomorrow.",
        "stockMessagePlural": "Only %count% items left in stock. Order within %minutes% minutes to receive it tomorrow."
      }
    }
  }
}
```

**Twig usage:**

```twig
{% set stockMessageKey = product.availableStock is same as(1)
    ? 'swAcademy.pdp.customSection.stockMessageSingular'
    : 'swAcademy.pdp.customSection.stockMessagePlural'
%}

{{ stockMessageKey|trans({
    '%count%': product.availableStock,
    '%minutes%': remainingMinutes // Value from plugin logic, or plugin config, or from external system
  })|sw_sanitize
}}
```

Instead of `|trans|sw_sanitize`, you pass the placeholder parameters directly to the `trans` filter:

```twig
`|trans({'%paramKeyOne%': Value, '%paramKeyTwo%': Value})|sw_sanitize`
```

Shopware then replaces the placeholder keys with the provided values. The rendered output may look like this: `Only 1 item left in stock. Order within 15 minutes to receive it tomorrow.` or `Only 10 items left in stock. Order within 15 minutes to receive it tomorrow.`.

The same output rules from the previous section still apply after the placeholders are replaced. The example above renders normal HTML text, so `sw_sanitize` is appropriate.

<Callout title="Advanced: ICU MessageFormat" type="info">

Symfony also supports pluralization with ICU MessageFormat. This lets you express singular and plural forms inside one translated message.

That approach is powerful, but it uses a different placeholder syntax (`{count}` instead of `%count%`) and requires ICU translation resources such as `messages+intl-icu.en.yaml` in Symfony projects. For this storefront snippet example, the two-key approach keeps the behavior explicit and easy to read.

</Callout>

### Override Snippets

In Shopware there are two ways to override storefront snippets without modifying the base files (from the core or other plugins):

- From the administration (Settings -> Localisation -> Snippets)
- From your plugin (`[Your_Plugin]/src/Resources/snippet/`)

The easiest way is to override snippets from the administration. This allows you to edit snippets in the admin UI and see the changes immediately in the storefront.

Overriding snippets via plugin requires creating a snippet file in your plugin and redefining the snippet key with a new value.

For example, if you want to override this snippet from the core:

```json
{
  "general": {
    "homeLink": "Home"
  }
}
```

To override this value in your plugin:

1. Create a new snippet file `[Your_Plugin]/src/Resources/snippet/overrides/sw-academy.en.json`.
2. Copy the JSON structure from the base file you want to override: `{ "general": { ... } }`
3. Override the `homeLink` value: `{ "general": { "homeLink": "Start page" } }`

Shopware merges snippet files and uses the value from your plugin instead of the default one.

In Shopware, the storefront core snippets are defined in:

- `vendor/shopware/storefront/Resources/snippet/storefront.de.json`
- `vendor/shopware/storefront/Resources/snippet/storefront.en.json`

### Troubleshooting Checklist

When overriding snippets, check the following points:

1. Ensure that snippet keys are **consistent across languages**.
2. Snippet overrides created in the administration have a **higher priority** than snippet values provided by plugins or themes.
3. When overriding snippets, make sure your snippet file contains the same key path as the original snippet.
4. If changes are not visible, clear the cache via `bin/console cache:clear` and check the browser console for errors.
5. Verify plugin and theme priority if multiple plugins or themes are overriding the same snippet.

<Callout title="Note" type="info">

For larger projects or CI pipelines, tools like [PHPUnuhi](https://github.com/boxblinkracer/phpunuhi) can help validate translation files. This is useful for checking missing keys, inconsistent structures, or empty translation values before they reach the storefront.

</Callout>

### Using Translations in Storefront JavaScript

In most storefront projects, you **do not** translate snippet keys inside storefront JavaScript the way you do in administration JavaScript (Vue.js) using `$t()`.

Storefront JavaScript runs in the browser and typically does **not** have access to a translation service that resolves snippet keys at runtime.

Instead, storefront translations are only rendered **server-side** in Twig using Symfony Translator.

Once a page is rendered, you can work with the **already translated strings** in your storefront JavaScript.
A common pattern is to translate values in Twig and pass them into your storefront JavaScript plugin via the `data-...-options` attribute.

Let's see an example.

#### Step 1: Pass Translated Values From Twig

```twig
{# Core pattern: pass translated texts as "snippets" via data options. #}
{% set pluginOptions = {
    snippets: {
        activeLabel: 'detail.addProduct'|trans|striptags,
        inactiveLabel: 'swAcademy.detail.addProductInactive'|trans|striptags
    }
} %}

<div data-sw-academy-plugin data-sw-academy-plugin-options="{{ pluginOptions|json_encode|escape('html_attr') }}">
    <button type="button" class="btn btn-buy">
        {{ 'detail.addProduct'|trans|sw_sanitize }}
    </button>
</div>
```

Your storefront JavaScript plugin can then read the translated values and use them directly without performing any additional translation logic.

<Callout title="Tip" type="info">

`data-...-options` is just JSON. This means you can structure your options as key-value pairs and nested objects. A common pattern is to group UI texts under `snippets` and keep the rest of the config next to it:

```json
{
  "snippets": {
    "activeLabel": "Add to cart",
    "inactiveLabel": "Not available"
  },
  "someFeatureFlag": true
}
```

Because the JSON is rendered inside an HTML attribute, use `escape('html_attr')` after `json_encode`. This escapes quotes and other attribute-sensitive characters so the JSON remains valid and cannot accidentally break the surrounding HTML.

</Callout>

#### Step 2: Use the Translation in JavaScript

In your storefront plugin, the options are automatically available via `this.options`.
With that you can implement UI behavior without resolving snippet keys in JavaScript:

```js
const { PluginBaseClass } = window;

export default class SwAcademyPlugin extends PluginBaseClass {
    static options = {
        snippets: {
            activeLabel: 'Add to cart',
            inactiveLabel: 'Inactive',
        },
        buttonSelector: 'button',
    };

    init() {
        const button = this.el.querySelector(this.options.buttonSelector);
        if (null === button) {
            return;
        }

        // We do NOT translate in JS. We use texts that were translated in Twig and passed via data options.
        button.textContent = true === button.disabled ? this.options.snippets.inactiveLabel : this.options.snippets.activeLabel;
    }
}
```

<Callout title="Shopware 6.6 and Newer" type="info">

This example uses the current public pattern from the Shopware documentation: `const { PluginBaseClass } = window;`.

In older projects, especially Shopware 6.5 and below, you may still see this import style:

```js
import Plugin from 'src/plugin-system/plugin.class';

export default class ExamplePlugin extends Plugin {
}
```

Both patterns describe the same idea: your storefront plugin extends Shopware's base plugin class. For new Shopware 6.6+ code, prefer the `window.PluginBaseClass` pattern shown above.

</Callout>

To connect the plugin to the Twig element, register it in your storefront entry file (for example `main.js`):

```js
const PluginManager = window.PluginManager;

PluginManager.register('SwAcademyPlugin', () => import('./sw-academy.plugin'), '[data-sw-academy-plugin]');
```

This approach is useful when the UI text needs to change **after** the server rendered the page (e.g., after user interaction, async requests, or state changes), or when it depends on backend context such as customer groups or configuration.

It allows you to handle dynamic, language-specific UI feedback such as validation messages, notifications, or modal dialogs without implementing custom translation logic in JavaScript.

#### Avoid Repeating the Same Options in Listings

The example above fits a product detail page or another single-element use case. The plugin root appears once, so passing translated options directly to that element is fine.

On listing pages, the situation can be different. If the same translated texts are needed for every product box, avoid rendering the same JSON options on every product box. A listing with 72 products would otherwise repeat the same translated strings 72 times in the HTML.

Instead, place shared translated options once on a parent element and let the JavaScript plugin work from that container:

```twig
{% set swAcademyListingOptions = {
    snippets: {
        activeLabel: 'detail.addProduct'|trans|striptags,
        inactiveLabel: 'swAcademy.detail.addProductInactive'|trans|striptags
    }
} %}

<div data-sw-academy-listing-plugin
     data-sw-academy-listing-plugin-options="{{ swAcademyListingOptions|json_encode|escape('html_attr') }}">
    {# The product listing or product boxes are rendered inside this container. #}
</div>
```

Your JavaScript plugin can then use the container as its root and query the product boxes inside it:

```js
const { PluginBaseClass } = window;

export default class SwAcademyListingPlugin extends PluginBaseClass {
    static options = {
        snippets: {
            activeLabel: 'Add to cart',
            inactiveLabel: 'Inactive',
        },
        productBoxSelector: '.product-box',
    };

    init() {
        this.productBoxes = this.el.querySelectorAll(this.options.productBoxSelector);
    }
}
```

Use this pattern when the translated texts are shared across many repeated elements. Keep product-specific values, such as product IDs or availability states, close to the individual product box.

## Summary

In this learning unit, you learned:

- How storefront snippets work, how to create and override them.
- How to use snippet keys in Twig templates with the `trans` filter.
- How to render translated content using `sw_sanitize` (safely), `striptags`, and `raw`.
- How to pass translated strings to storefront JavaScript for dynamic UI interactions.

With this knowledge, you can implement localization-ready storefront plugins and themes while keeping translations maintainable and secure.
