Appearance
Multilingual Templates
This feature is available with the Plugin for Business plan and higher.
If you're on the Plugin for Startup or Plugin Expansion plan, consider upgrading to access this feature. For more details, visit our pricing page or contact support.
Multilingual Templates store several language versions (mutations) of one template in a single JSON file. That file holds the main (Primary) template structure together with the text translations for every defined mutation, so no separate HTML and JSON files per language are needed.
When you need HTML output for a specific language mutation, send the multilingual JSON along with the selected language code to a special API endpoint. The endpoint then returns the corresponding HTML. The list of available languages for the template is stored in a mutations array, which is always included alongside json and html when saving a multilingual template via the onSave callback.
How to Enable
To activate the Multilingual Templates feature, add the following option to your TOPOL_OPTIONS configuration object:
ts
multilingual: true,Defining template language versions through defaultTemplateSettings.langs turns the feature on by itself, so multilingual can be left out in that case.
Once enabled, the editor's Top Bar will display a Create mutation dropdown.

This dropdown serves as the main hub for managing all language variations in the Plugin UI. It allows you to create, switch, and delete mutations, or change which mutation is marked as Primary.
Workflow Example
When the editor opens with the Multilingual feature enabled, the user can begin in two ways:
- Design the template first and then create a language version (mutation), or
- Create a mutation first and then start designing.
In both cases, the current template becomes linked to that mutation.
Once the first language version is ready, the user can add another language and select it. Initially, the new mutation looks identical because no content in that language has been added yet. While that language is active, the user can rewrite the content in the chosen language. This process can be repeated for any number of languages. When switching to a mutation that already has content, the editor instantly loads that version.
One language must always be marked as Primary, serving as the default version. The Primary language cannot be deleted, but its role can be reassigned to another mutation at any time. The Primary flag is especially important when saving the template (explained later). If a non-Primary mutation is deleted, the editor automatically returns to the Primary version.
AI-Powered Translation
The Template Translation feature translates template content into any language mutation using AI. When Multilingual Templates is enabled, a translate button appears next to each language in the language settings panel.
Saving Multilingual Templates
When the user saves the multilingual template via the default or custom Save button, the onSave callback always receives four parameters: json, html, mutations, and syncedSections.
onSave callback structure:
ts
const TOPOL_OPTIONS = {
callbacks: {
onSave(json, html, mutations, syncedSections) {
// Implement your save logic here.
// If multiple mutations exist,
// define how to store each language version.
},
},
};If your onSave callback integration currently only handles json and html, you must extend it to also process the mutations parameter in order to support multilingual templates feature.
Even if no mutations exist (or if the Multilingual feature is not enabled), the mutations parameter will still be passed. However, in these scenarios, it will be an empty array [], which can be ignored.
The fourth syncedSections parameter is documented in detail in the Callbacks reference. It is an array of synced section IDs (number[]), so the matching definitions have to be fetched on your side. For multilingual templates that use synced sections, pass that content to the Convert JSON template to HTML endpoint when rendering per-language HTML, otherwise translations defined on the synced section will not be applied. See Synced Sections in Multilingual Templates below.
Parameter details:
In this section, we will provide the necessary details for the json, html, and mutations parameters of the onSave callback. For the syncedSections parameter, see the Callbacks reference.
html
The HTML returned depends on how the save action is triggered:
1. Saving with the default Save button
The onSave callback always returns only the HTML of the Primary mutation. Even if other mutations exist, they are not included.
To retrieve HTML in a specific Language, call our Convert JSON template to HTML API. Jump to this section for more instructions.
2. Saving with your custom Save button
If you implement your own Save button, it must call this function:
ts
// The lang property is a language code and is optional
window.TopolPlugin.save({ lang: "de" });WARNING
The argument is an object, not a bare string. TopolPlugin.save("de") is silently treated as a call with no language and returns the Primary mutation's HTML.
This call then triggers the usual onSave(json, html, mutations) callback, but the optional lang property gives you control over the HTML output:
If you include the
langparameter, you receive the HTML output of that specific mutation.If the
langparameter is omitted, the response holds only the HTML output of the Primary mutation.
INFO
The lang value has to be known before calling window.TopolPlugin.save({ lang }).
To handle this, create your own mechanism that lets users choose a language, such as a dropdown or selector. Store the selected language in your application state, and when the user clicks your custom save button, pass that stored value into window.TopolPlugin.save({ lang }).
The mutations array, which contains information about available language mutations, is only returned inside the onSave(json, html, mutations) callback. Because this happens after the window.TopolPlugin.save({ lang }) call, you cannot rely on the mutations array to decide which language to save.
WARNING
When saving a multilingual template, you may be tempted to call window.TopolPlugin.save({ lang }) inside the onSave(json, html, mutations) callback for every language in the received mutations array. However, this approach is strongly discouraged. Each call to window.TopolPlugin.save({ lang }) triggers another onSave callback, which would create an infinite loop.
json
The json parameter matches the structure of the Primary mutation. It also includes a special langs array that holds all defined language mutations and their translations.
Example:
ts
// ...Primary mutation JSON
"langs": [
{
"key": "en", // Language code
"primary": true, // false if not the primary mutation
"translations": { // All translated blocks for this mutation
"ZF-vfFO4c": { // UID of a block (same as in the primary)
"content": "<p>Content in English.</p>" // Its content in this mutation
},
"w5ubrEmMN": { // UID of another block (same as in the primary)
"content": "Click me!", // Its content in this mutation
"attributes": { // Its attributes in this mutation
"href": "https://google.com"
}
},
"mj-subject": { // Email subject line for this mutation
"content": "Welcome to our newsletter!"
},
"mj-preview": { // Preheader text for this mutation
"content": "Everything new this month"
}
// ...other translated blocks in this mutation
}
},
// ...other language mutations
]This structure ensures that all language-specific content is stored on one place (in this multilingual JSON file) alongside the Primary template.
mutations
The mutations parameter is always an array containing information about all language mutations defined in the current multilingual JSON. Each object inside this array holds only the language code and whether this language is Primary.
Data structure:
ts
mutations: {
lang: string,
primary: boolean
}[]Example:
ts
[
{
"lang": "en", // language code
"primary": true
},
{
"lang": "de",
"primary": false
},
// ...other mutations stored in multilingual JSON
]API for Retrieving HTML in a Specific Language
In order to retrieve the HTML version of a specific language from a Multilingual template, call our Convert JSON template to HTML API.
INFO
This API call requires authorization using your Secret Key, which must be included in the X-Secret-Key header. Follow this guide to learn how to obtain your Secret Key.
In the POST request, you must include the JSON code of your Multilingual template and the requested language (using the language code from the mutations array), along with all other required parameters.
The response is an object containing the HTML code of the template in the specified language.
External Language Management
If you want to manage language mutations from your own UI (outside the editor iframe), you can use the Plugin's public API methods for full programmatic control and register callbacks to keep your UI synchronized with the editor's internal state.
Available Methods
| Method | Description |
|---|---|
TopolPlugin.createLanguage(lang) | Creates a new language mutation |
TopolPlugin.selectLanguage(lang) | Switches the active editing language |
TopolPlugin.setPrimaryLanguage(lang) | Changes the primary language |
TopolPlugin.deleteLanguage(lang) | Deletes a language mutation (shows confirmation modal) |
TopolPlugin.getMutations() | Requests the current list of mutations via onGetMutations callback |
TopolPlugin.translateLanguage(lang, sourceLang?) | AI-translates a single mutation (from the primary language by default, or from sourceLang) |
TopolPlugin.translateAllLanguages() | AI-translates every non-primary mutation from the primary language |
State Synchronization Callbacks
Register these callbacks in your TOPOL_OPTIONS to receive notifications when the language state changes:
| Callback | Parameters | Fires when |
|---|---|---|
onLanguageCreated | (lang, mutations) | A new mutation is created |
onLanguageDeleted | (lang, mutations) | A mutation is deleted |
onLanguageSelected | (lang) | The active editing language changes |
onPrimaryLanguageChanged | (lang, mutations) | The primary language changes |
onGetMutations | (mutations) | TopolPlugin.getMutations() is called |
The mutations parameter is always an array of { lang: string, primary: boolean } objects representing the current state of all language mutations. Note that the mutation objects use lang, while the langs array inside the saved JSON uses key for the same value.
These callbacks fire for both UI-initiated and API-initiated actions, so your external UI stays synchronized regardless of how the change was triggered.
INFO
onLanguageSelected also fires as a side effect of other actions, because creating a mutation, deleting one, or changing the Primary language all change which mutation is active. Treat it as a report of the current editing language rather than as a signal that the user picked one.
Example: External Language Selector
ts
const TOPOL_OPTIONS = {
multilingual: true,
callbacks: {
onLanguageCreated(lang, mutations) {
// A new language was added, so refresh the dropdown options
updateLanguageDropdown(mutations);
},
onLanguageDeleted(lang, mutations) {
// A language was removed, so refresh the dropdown options
updateLanguageDropdown(mutations);
},
onLanguageSelected(lang) {
// The active language changed, so highlight it in the selector
setActiveLanguage(lang);
},
onPrimaryLanguageChanged(lang, mutations) {
// The primary language changed, so move the primary badge
updateLanguageDropdown(mutations);
setActiveLanguage(lang);
},
onGetMutations(mutations) {
// Initial load: populate the language selector
updateLanguageDropdown(mutations);
},
onInit() {
// Request the current mutations when the editor is ready
TopolPlugin.getMutations();
},
},
};
// Switch language from your external UI
document.querySelector("#lang-selector").addEventListener("change", (e) => {
TopolPlugin.selectLanguage(e.target.value);
});For detailed callback parameter documentation, see the Language State Callbacks section.
Synced Sections in Multilingual Templates
When a multilingual template contains synced sections, the language mutations and the synced section content come from two different places and have to be reconciled at render time.
Where the translations live
- The template's own translations live in the saved JSON, under
langs[].translations, keyed by block UID - exactly as described in the json section above. Each mutation has its own copy. - The synced section's translations live on the synced section entity itself, in its
translationfield - a locale-keyed map keyed by the synced section's own block UIDs. There is a single copy, shared by every template that references the synced section.
When the canvas renders, the editor pulls the synced section's translation map, remaps the synced section's UIDs to the new UIDs generated inside the template, and merges the translated content into each mutation. The result is that every mutation that already exists in the template gets translated content for the synced section in its own language, as long as the synced section provides a translation entry for that locale.
What integrators must do at HTML render time
When the user inserts a synced section into a multilingual template, the editor immediately merges the synced section's per-locale translations into the template's own langs[].translations (mapped onto freshly generated block UIDs). The json from onSave therefore already contains a snapshot of the synced section's translations alongside the regular syncedId reference on the section node. The fourth syncedSections argument of the same callback lists the synced section IDs used in the template, which saves walking the JSON to discover which sections need fetching at render time.
That snapshot is point-in-time. So that each render reflects the latest copy of the synced section, the Convert JSON template to HTML endpoint re-resolves the synced section on every call:
- Look up each referenced synced section by
syncedIdin thesyncedSectionsarray supplied in the request. - Drop any existing
langs[].translationsentries that belong to translatable blocks inside the placeholder section (so the snapshot does not stick around). - Inline the synced section's MJML definition into the template, regenerating child block UIDs.
- For each translatable block in the inlined section, merge the appropriate locale's entry from the synced section's
translationmap into every mutation'slangs[].translations. - Render the resulting MJML to HTML for the requested language.
Omitting a referenced synced section from the syncedSections request field skips that section entirely: it keeps its existing children and its snapshotted langs[].translations, so it renders the point-in-time copy captured when the section was inserted. Nothing breaks, but the rendered HTML no longer matches the latest synced section content.
Locale coverage inside a synced section
A synced section's translation map does not have to contain every locale that the host template defines. The rendering pipeline handles missing entries differently depending on whether the mutation is the primary one:
- Primary mutation, locale missing from the synced section's
translation: falls back to the first locale present in the synced section's translation map. This is the source-of-truth content for the section. - Non-primary mutation, locale missing from the synced section's
translation: gets an empty translation entry ({}). An empty entry carries no content, so at render time the block falls back to the Primary mutation's content for that block.
Plan your translation coverage accordingly. If you only translate synced sections into a subset of the languages used by your templates, the untranslated mutations display the Primary mutation's copy of the section, which is itself the synced section's first available locale.
What happens when a mutation is added after a synced section
If you create a new language mutation in a template that already contains a synced section, the new mutation starts with no entries for the section's blocks. At the next convertJson2Html call, the server will populate the mutation according to the rules above: it uses the synced section's translation for that locale if one exists, or leaves the entry empty otherwise. This means you can roll out a new language to a synced section centrally and every template that uses it will pick up the new translation on its next render without needing to be edited.
Custom Language Preset
By default, the language selection dropdown shows Topol's predefined set of languages. If you want to customize this list, you can define your own languages by adding the customLanguagePreset option to your TOPOL_OPTIONS configuration object.
The customLanguagePreset object supports two properties:
langs- an array of language definitions. Each language must include:
name- the display name shown in the dropdowncode- the language code used internally (for example, en, en-US, pt-BR)nativeName- the language name written in its native form
override- a boolean that controls how your custom languages are applied:
true- replaces the default language list (default)false- appends your custom languages to the default list
ts
customLanguagePreset: {
langs: [
{
name: "Custom name",
code: "custom-code",
nativeName: "Custom native name",
},
// ...other custom languages
],
override: true, // defaults to true
}Example:
ts
customLanguagePreset: {
langs: [
{ name: "Spanish (Spain)", code: "es-ES", nativeName: "Español (España)" },
{ name: "Spanish (Mexico)", code: "es-MX", nativeName: "Español (México)" },
{ name: "Portuguese (Brazil)", code: "pt-BR", nativeName: "Português (Brasil)" },
{ name: "Portuguese (Portugal)", code: "pt-PT", nativeName: "Português (Portugal)" },
],
override: true,
}This example configuration replaces the default language list with a custom set of regional Spanish and Portuguese variants, giving users more precise language options when creating templates.
WARNING
Each of name, code, and nativeName is required on every entry, and an invalid langs array fails validation as a whole: the editor keeps its default language list and shows an error notification. Entries sharing a code are collapsed to one, and only override: false appends, so any other value replaces the defaults.
Default Template Languages
This feature works nicely with Default Template Settings, where you can define which languages are automatically created for every new template. For more details, see the documentation here.
