Widgets can support multi-language configuration by implementing the BuilderOperationsExtension. This allows users to translate widget configuration properties, such as labels and tooltips, in the builder translation settings panel.

If your widget settings page does not include any configurable string input, you do not need to implement multi-language support.

How it works

To support multiple languages, define translatable configuration keys through an extension:

  • Implement BuilderOperationsExtension and return an array of TranslationKey objects from getTranslationKey.
  • Each TranslationKey maps a widget configuration field to a translation entry that appears in the builder translation settings panel.

How to use multi-language support

Here are the steps to work with multi-language support in your widgets:

1. Declare the extension

Add the builderOperations extension point to your widget's manifest.json:

Use dark colors for code blocksCopy
1
2
3
4
5
6
7
"extensions": [
  {
    "name": "builderOperations",
    "point": "BUILDER_OPERATIONS",
    "uri": "tools/builder-operations"
  }
]

2. Create the extension file

Once you have declared the extension in manifest.json, create a file named tools/builder-operations.ts in your widget folder. This file will contain the implementation of the BuilderOperationsExtension class.

3. Implement the extension class

Implement the BuilderOperationsExtension interface, specifically the getTranslationKey method. This method should return an array of TranslationKey objects that define which configuration properties are translatable.

Example:

Use dark colors for code blocksCopy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import type { extensionSpec, IMAppConfig } from 'jimu-core';
import { defaultMessages as jimuUIMessage } from 'jimu-ui';
import defaultMessage from '../setting/translations/default';

const messages = Object.assign({}, jimuUIMessage, defaultMessage);

export default class BuilderOperations implements extensionSpec.BuilderOperationsExtension {
  id = 'button-builder-operation'
  widgetId: string

  getTranslationKey(appConfig: IMAppConfig): Promise<extensionSpec.TranslationKey[]> {
    const functionConfig = appConfig.widgets[this.widgetId].config.functionConfig;
    const keys: extensionSpec.TranslationKey[] = [];

    if (functionConfig.toolTip) {
      keys.push({
        keyType: 'value',
        key: `widgets.${this.widgetId}.config.functionConfig.toolTip`,
        label: {
          key: 'tooltip',
          enLabel: messages.tooltip
        },
        valueType: 'text'
      });
    }

    if (functionConfig.text) {
        keys.push({
        keyType: 'value',
        key: `widgets.${this.widgetId}.config.functionConfig.text`,
        label: {
          key: 'text',
          enLabel: messages.text
        },
        valueType: 'text'
      });
    }

    return Promise.resolve(keys);
  }
}

4. Test your changes

In the the builder's translation setting panel:

  1. Add a widget to a page in Experience Builder.
  2. Set some values for the properties you defined in the widget configuration.
  3. Open the translation setting panel in the builder.
  4. Add languages other than the main locale.
  5. Expand the panel to see the translation keys you defined.
  6. Save the app.

Once the translation keys are defined in the builder, you can test them in runtime:

  1. Add a Language Switcher widget
  2. Preview the app to test. The Language Switcher widget does not work when the app is opened in the builder.

TranslationKey properties

The TranslationKey object has the following properties:

PropertyTypeDescription
keyType'value' | 'group'Required. Use 'value' for translatable fields and 'group' for structural grouping.
keystringRequired. Unique identifier used to correlate with the config.
labelstring | I18nLabelRequired. The display label. Can be a plain string or an I18nLabel object.
groupKeystringOptional. Set this to the key of the parent group entry if this key belongs to a group. This creates an explicit parent-child relationship used to render children inside the group's collapsible panel. The parent can be either a top-level group or another nested group.
valueType'text' | 'textarea' | 'rich-text' | 'rich-text-with-plugins' | 'expression'Optional. Input control type. 'text' for single line, 'textarea' for multi-line, 'rich-text' for rich text editor, 'rich-text-with-plugins' for rich text with additional plugins, 'expression' for expression builder. Defaults to 'text'.
defaultValuestringOptional. Default value if the value cannot be retrieved from the config by the key.

I18nLabel interface

The I18nLabel interface allows you to define a label that supports interpolation of dynamic values. This is useful when the label needs to include variable content, such as layer names or field names.

PropertyTypeDescription
keystringRequired. The i18n key in translation resources.
enLabelstringRequired. The English label, used as a fallback or reference.
values{[key: string]: string}Optional. Values for interpolation in the i18n string.

Code examples

The following examples cover common TranslationKey scenarios. You can combine these examples as needed.

Refer to the flowchart below to determine the best structure for your widget.

Translation key structure flowchart

Single level with only a label and an input box

Use this when you need one translatable field with a direct label-to-input mapping.

Scenario

  • Widget structure is simple
  • No item list
  • All labels are unique

Structure

  • Label
  • Input

Example widgets: Button.

Use dark colors for code blocksCopy
1
2
3
4
5
6
7
8
9
{
  keyType: 'value',
  key: `widgets.${this.widgetId}.config.functionConfig.toolTip`,
  label: {
    key: 'tooltip',
    enLabel: messages.tooltip
  },
  valueType: 'text'
}
Multi-language button example

Single level with multiple items, each only containing a label and an input box

Use this when a widget contains an item list, each item has only one configurable input, and labels may repeat across items.

Scenario

  • Widget contains an item list
  • Each item contains only one configurable input box
  • Labels may repeat

Structure

  • Label / Hint for Item_num
  • Input

Core rule: Differentiate labels by item content or index. If the item contains only one input, use the item name directly in the label, such as Name for Bookmark 1.

Example widgets: Basemap Gallery.

Use dark colors for code blocksCopy
1
2
3
4
5
6
7
8
9
10
{
  keyType: 'value',
  key: `widgets.${this.widgetId}.config.items.${itemId}.label`,
  label: {
    key: 'labelForItem',
    enLabel: 'Label for {item}',
    values: { item: itemName }
  },
  valueType: 'text'
}

Grouping translation keys

Widget with multiple items/maps/layers: each contains multiple labels and input boxes.

Scenario

  • Widget contains an item list
  • Each item contains two or more input boxes

Structure

Grouped translation key structure

Use keyType: 'group' to create a collapsible grouping panel for related translation keys. Set the groupKey property on each child key to the key of the parent group to establish the parent-child relationship. Children are rendered inside the collapsible panel indented under the group header. Groups can be nested by pointing a child group's groupKey to another group.

Use this when multiple related fields should be organized under one collapsible section.

For widgets with nested items, include enough context in labels to avoid ambiguous translation entries. For example, a Filter clause label can use Hint for clause 1 or Label for predefined value "abc" in clause 2.

Use dark colors for code blocksCopy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Top-level group header
{
  keyType: 'group',
  key: `widgets.${this.widgetId}.config.group1`,
  label: 'Group Label'
}

// Nested group header
{
  keyType: 'group',
  key: `widgets.${this.widgetId}.config.group1.layer1`,
  groupKey: `widgets.${this.widgetId}.config.group1`,
  label: 'Layer 1'
}

// Child value - linked to the group via groupKey
{
  keyType: 'value',
  key: `widgets.${this.widgetId}.config.group1.layer1.title`,
  groupKey: `widgets.${this.widgetId}.config.group1.layer1`,
  label: 'Title',
  valueType: 'text'
}
Multi-language select example

Example widgets: Select, Filter.

Multi-line text with default value

Use valueType: 'textarea' for multi-line input.

Use this when users need to edit paragraph-length content.

Use dark colors for code blocksCopy
1
2
3
4
5
6
{
  keyType: 'value',
  key: `widgets.${this.widgetId}.config.description`,
  label: 'Description', // Simple string label
  valueType: 'textarea'
}

Rich text content

Use valueType: 'rich-text' for rich text editor input.

Use this when you need formatted text without additional rich-text plugins.

Use dark colors for code blocksCopy
1
2
3
4
5
6
7
8
9
{
  keyType: 'value',
  key: 'cookieBanner.displayRichText',
  label: {
    key: 'optionalBannerDefaultText',
    enLabel: messages.cookieNotice
  },
  valueType: 'rich-text'
}

Rich text with plugins

Use valueType: 'rich-text-with-plugins' for rich text editor with additional plugins. Such as inserting images and expressions.

Use this when you need rich-text content plus plugin-enabled authoring features.

Use dark colors for code blocksCopy
1
2
3
4
5
6
7
8
9
{
  keyType: 'value',
  key: `widgets.${this.widgetId}.config.text`,
  label: {
    key: 'textContent',
    enLabel: messages.textContent
  },
  valueType: 'rich-text-with-plugins'
}

Expression builder

Use valueType: 'expression' for expression-based values.

Use this when field values are dynamic and should be defined with expressions.

Example widgets: Button connected to a data source with tooltip configured using an expression.

Use dark colors for code blocksCopy
1
2
3
4
5
6
7
8
9
{
  keyType: 'value',
  key: `widgets.${this.widgetId}.config.functionConfig.toolTipExpression`,
  label: {
    key: 'tooltip',
    enLabel: messages.tooltip
  },
  valueType: 'expression'
}
Expression builder example

Your browser is no longer supported. Please upgrade your browser for the best experience. See our browser deprecation post for more details.