Multi-Language Forms - i18n & Localization Guide
FormEngine Core provides comprehensive support for localization and internationalization, allowing you to create forms that can be displayed in multiple languages. This guide covers all aspects of localization in FormEngine, from basic property localization to advanced validation error translation.
Types of Localization
FormEngine supports two main types of localization:
- Component Properties Localization - Localization of component properties such as labels, placeholders, tooltips, and other text properties.
- Validation Error Localization - Localization of validation error messages for form validation rules.
Localization in JSON Forms
Localization data is stored directly within the form JSON, making forms self-contained and easily distributable across different language environments.
Basic Structure
A localized form includes three key properties:
{
"form": {
// Form definition
},
"localization": {
"en-US": {
// English translations
},
"de-DE": {
// German translations
}
},
"languages": [
{
"code": "en",
"dialect": "US",
"name": "English",
"description": "American English",
"bidi": "ltr"
},
{
"code": "de",
"dialect": "DE",
"name": "Deutsch",
"description": "German",
"bidi": "ltr"
}
],
"defaultLanguage": "en-US"
}
The localization Property
The localization property contains translations organized
by language code and component. Each language code (like en-US, de-DE) maps to an object containing translations for specific components:
{
"localization": {
"en-US": {
"componentKey1": {
"component": {
"label": "Enter your name",
"helperText": "Type here..."
}
},
"componentKey2": {
"component": {
"label": "Email address"
}
}
},
"de-DE": {
"componentKey1": {
"component": {
"label": "Geben Sie Ihren Namen ein",
"helperText": "Hier eingeben..."
}
},
"componentKey2": {
"component": {
"label": "E-Mail-Adresse"
}
}
}
}
}
Variables in Localized Strings
Localized strings can include variables that are replaced at runtime with actual values. Variables are enclosed in curly braces with a
dollar sign prefix: {$variableName}.
Example:
{
"localization": {
"en-US": {
"componentKey": {
"component": {
"content": "Welcome, {$userName}! You have {$messageCount} new messages."
}
}
}
}
}
At runtime, {$userName} and {$messageCount} will be replaced with actual values provided to the component. This allows for dynamic
content within localized strings.
Variable names are case-sensitive and should match the property names available in the component's data context.
Fluent values and spaces:
- Leading spaces — Use a placeable with a quoted run of spaces, for example
{" "}, when you need leading spaces in the rendered text (adjust the number of spaces as needed). - Trailing spaces — Trailing spaces in the value are preserved.
- Intentional empty render — Use
{""}when you intentionally want an empty rendered value from Fluent while still keeping a concrete message pattern. - Whitespace-only patterns — A value that contains only whitespace can format to an empty rendered string. For component properties, this then follows the property-level fallback behavior described in How Fallback Works.
Variables in localized arrays and objects
When a localized property resolves to a JSON array or object (for example, dropdown items, radio options, or any custom structure stored
under localization), FormEngine still applies the same {$variableName} interpolation as for plain strings. The
FluentLocalizationEngine walks the value recursively:
every string in object values and in keys is formatted with Fluent using the current form data; numbers, booleans, and null
are left unchanged.
Use this when option labels (or values) should reflect data the user entered elsewhere on the form, or when you store nested configuration per language.
Where the data comes from: Pass field values through
initialData on
FormViewer (or the equivalent in your host). Top-level keys in
initialData become Fluent variables with the same name.
Nested fields in form data: Use a normal dot path in the placeholder (for example {$user.name} or {$profile.displayName}).
FormEngine resolves nested initialData against those paths automatically—for example, form data { "user": { "name": "Alex" } } is
referenced as {$user.name} in patterns.
Missing variables: If a referenced key is not present in the form data, the placeholder resolves to an empty string (same behavior as for localized strings).
Example — localized items with dynamic labels:
{
"localization": {
"en-US": {
"sizeSelect": {
"component": {
"items": [
{
"value": "custom",
"label": "Custom size for {$productName}"
},
{
"value": "standard",
"label": "Standard"
}
]
}
}
}
}
}
With initialData such as { "productName": "Acme XL" }, the first option label renders as Custom size for Acme XL; the second option
has no placeholders and stays Standard.
Example — placeholders in both label and value:
{
"localization": {
"en-US": {
"orderSelect": {
"component": {
"items": [
{
"value": "order-{$orderId}",
"label": "Order {$orderId}"
}
]
}
}
}
}
}
Example — nested object (metadata per language):
{
"localization": {
"en-US": {
"summaryCard": {
"component": {
"config": {
"title": "Hello, {$displayName}",
"meta": {
"subtitle": "Role: {$role}"
},
"tags": [
"{$region}",
"static-tag"
]
}
}
}
}
}
}
All string leaves (title, subtitle, each array element that is a string) are interpolated; a key that were a string with placeholders
would be interpolated as well.
If the same property is stored as a JSON string inside the Fluent bundle (for example, a single message whose value is a stringified
array starting with [ or {), that value is parsed and interpolated the same way after parsing.
The languages Property
The languages array defines which languages are available for the form. Each language includes:
code: Language code (e.g., "en")dialect: Regional dialect (e.g., "US")name: Language name in its native scriptdescription: Language descriptionbidi: Text direction ("ltr" for left-to-right, "rtl" for right-to-left)
The defaultLanguage Property
The defaultLanguage property specifies which language
should be used by default when the form loads. Must match one of the language codes in the localization object.
Localizing Component Properties
Component properties can be localized by
setting computeType to localization in the property
definition. This tells FormEngine to look up the actual value from the localization data.
When a property has computeType set to localization, FormEngine will look up the actual value from the localization object based on
the component key and
current language:
{
"key": "emailInput",
"type": "RsInput",
"props": {
"label": {
"computeType": "localization"
},
"helperText": {
"computeType": "localization"
}
}
}
With this approach, the actual values for label and helperText will be retrieved from:
localization["en-US"]["emailInput"]["component"]["label"](for English)localization["de-DE"]["emailInput"]["component"]["label"](for German)- etc.
Example: Basic Component Localization
{
"form": {
"key": "Screen",
"type": "Screen",
"children": [
{
"key": "langSelect",
"type": "MuiSelect",
"props": {
"items": {
"value": [
{
"value": "en-US",
"label": "American English"
},
{
"value": "es-ES",
"label": "Español"
}
]
},
"label": {
"computeType": "localization"
}
},
"events": {
"onChange": [
{
"name": "onLangChange",
"type": "custom"
}
]
}
},
{
"key": "nameInput",
"type": "MuiTextField",
"props": {
"label": {
"computeType": "localization"
},
"helperText": {
"computeType": "localization"
}
}
}
]
},
"localization": {
"en-US": {
"langSelect": {
"component": {
"label": "Language"
}
},
"nameInput": {
"component": {
"label": "Full Name",
"helperText": "Enter your full name"
}
}
},
"es-ES": {
"nameInput": {
"component": {
"label": "Nombre Completo",
"helperText": "Introduce tu nombre completo"
}
},
"langSelect": {
"component": {
"label": "Idioma"
}
}
}
},
"languages": [
{
"code": "en",
"dialect": "US",
"name": "English",
"description": "American English",
"bidi": "ltr"
},
{
"code": "es",
"dialect": "ES",
"name": "Español",
"description": "Spanish",
"bidi": "ltr"
}
],
"defaultLanguage": "en-US"
}
Live example
function App() { const form = useMemo(() => ({ "form": { "key": "Screen", "type": "Screen", "children": [ { "key": "langSelect", "type": "MuiSelect", "props": { "items": { "value": [ { "value": "en-US", "label": "American English" }, { "value": "es-ES", "label": "Español" } ] }, "label": { "computeType": "localization" } }, "events": { "onChange": [ { "name": "onLangChange", "type": "custom" } ] } }, { "key": "nameInput", "type": "MuiTextField", "props": { "label": { "computeType": "localization" }, "helperText": { "computeType": "localization" } } } ] }, "localization": { "en-US": { "langSelect": { "component": { "label": "Language" } }, "nameInput": { "component": { "label": "Full Name", "helperText": "Enter your full name" } } }, "es-ES": { "nameInput": { "component": { "label": "Nombre Completo", "helperText": "Introduce tu nombre completo" } }, "langSelect": { "component": { "label": "Idioma" } } } }, "languages": [ { "code": "en", "dialect": "US", "name": "English", "description": "American English", "bidi": "ltr" }, { "code": "es", "dialect": "ES", "name": "Español", "description": "Spanish", "bidi": "ltr" } ], "defaultLanguage": "en-US" }), []) const getForm = useCallback(() => JSON.stringify(form), [form]) const [language, setLanguage] = useState('en-US') const actions = useMemo(() => ({ onLangChange: (e)=> { setLanguage(e.args[0] ?? 'en-US') } }), []) const initialData = useMemo(() => ({langSelect: 'en-US'}), []) return <FormViewer initialData={initialData} view={muiView} getForm={getForm} actions={actions} language={language} /> }
Example: Multiple Components with Localization
{
"tooltipType": "MuiTooltip",
"form": {
"key": "Screen",
"type": "Screen",
"children": [
{
"key": "langSelect",
"type": "MuiSelect",
"props": {
"items": {
"value": [
{
"value": "en-US",
"label": "American English"
},
{
"value": "fr-FR",
"label": "Français"
}
]
},
"label": {
"computeType": "localization"
}
},
"events": {
"onChange": [
{
"name": "onLangChange",
"type": "custom"
}
]
}
},
{
"key": "header",
"type": "MuiTypography",
"props": {
"children": {
"computeType": "localization"
}
}
},
{
"key": "email",
"type": "MuiTextField",
"props": {
"label": {
"computeType": "localization"
},
"helperText": {
"computeType": "localization"
}
},
"tooltipProps": {
"title": {
"computeType": "localization"
}
}
},
{
"key": "submitButton",
"type": "MuiButton",
"props": {
"children": {
"computeType": "localization"
}
}
}
]
},
"localization": {
"en-US": {
"header": {
"component": {
"children": "Registration Form"
}
},
"email": {
"component": {
"label": "Email Address",
"helperText": "user@example.com"
},
"tooltip": {
"title": "We'll never share your email with anyone else"
}
},
"submitButton": {
"component": {
"children": "Submit Registration"
}
},
"langSelect": {
"component": {
"label": "Language"
}
}
},
"fr-FR": {
"header": {
"component": {
"children": "Formulaire d'Inscription"
}
},
"email": {
"component": {
"label": "Adresse Email",
"helperText": "utilisateur@exemple.fr"
},
"tooltip": {
"title": "Nous ne partagerons jamais votre email avec qui que ce soit"
}
},
"submitButton": {
"component": {
"children": "Soumettre l'Inscription"
}
},
"langSelect": {
"component": {
"label": "Idioma"
}
}
}
},
"languages": [
{
"code": "en",
"dialect": "US",
"name": "English",
"description": "American English",
"bidi": "ltr"
},
{
"code": "fr",
"dialect": "FR",
"name": "Français",
"description": "French",
"bidi": "ltr"
}
],
"defaultLanguage": "en-US"
}
Live example
function App() { const form = useMemo(() => ({ "tooltipType": "MuiTooltip", "form": { "key": "Screen", "type": "Screen", "children": [ { "key": "langSelect", "type": "MuiSelect", "props": { "items": { "value": [ { "value": "en-US", "label": "American English" }, { "value": "fr-FR", "label": "Français" } ] }, "label": { "computeType": "localization" } }, "events": { "onChange": [ { "name": "onLangChange", "type": "custom" } ] } }, { "key": "header", "type": "MuiTypography", "props": { "children": { "computeType": "localization" } } }, { "key": "email", "type": "MuiTextField", "props": { "label": { "computeType": "localization" }, "helperText": { "computeType": "localization" } }, "tooltipProps": { "title": { "computeType": "localization" } } }, { "key": "submitButton", "type": "MuiButton", "props": { "children": { "computeType": "localization" } } } ] }, "localization": { "en-US": { "header": { "component": { "children": "Registration Form" } }, "email": { "component": { "label": "Email Address", "helperText": "user@example.com" }, "tooltip": { "title": "We'll never share your email with anyone else" } }, "submitButton": { "component": { "children": "Submit Registration" } }, "langSelect": { "component": { "label": "Language" } } }, "fr-FR": { "header": { "component": { "children": "Formulaire d'Inscription" } }, "email": { "component": { "label": "Adresse Email", "helperText": "utilisateur@exemple.fr" }, "tooltip": { "title": "Nous ne partagerons jamais votre email avec qui que ce soit" } }, "submitButton": { "component": { "children": "Soumettre l'Inscription" } }, "langSelect": { "component": { "label": "Idioma" } } } }, "languages": [ { "code": "en", "dialect": "US", "name": "English", "description": "American English", "bidi": "ltr" }, { "code": "fr", "dialect": "FR", "name": "Français", "description": "French", "bidi": "ltr" } ], "defaultLanguage": "en-US" }), []) const getForm = useCallback(() => JSON.stringify(form), [form]) const [language, setLanguage] = useState('en-US') const actions = useMemo(() => ({ onLangChange: (e)=> { setLanguage(e.args[0] ?? 'en-US') } }), []) const initialData = useMemo(() => ({langSelect: 'en-US'}), []) return <FormViewer initialData={initialData} view={muiView} getForm={getForm} actions={actions} language={language} /> }
Localizing Array Data
In addition to string properties, you can localize array-type properties such as dropdown options, radio group items, checkbox lists, and other components that display lists of labeled values. This is useful when the labels shown to users need to change based on the selected language while the underlying values stay the same.
How it works
Array data localization stores the entire translated array as a JSON value in the localization object. When FormEngine resolves the
property for the current language, it parses the JSON array and passes it to the component instead of the default value.
After the value is resolved, Fluent placeholders in any string inside that array or object (for example {$userName} in an option
label) are interpolated using the same rules as for localized strings. See
Variables in localized arrays and objects above.
The localization entry for an array property is stored under the same path as string properties:
localization[languageCode][componentKey][type][propertyName]
The value is a JSON-encoded array instead of a plain string.
Defining a localizable array property
To make an array property localizable in a component definition, chain the localize getter on the array annotation builder:
import {array, define, string, toLabeledValues} from '@react-form-builder/core'
export const mySelect = define(MySelect, 'MySelect')
.props({
label: string.default('Select'),
items: array.localize.default(toLabeledValues(['Option A', 'Option B', 'Option C']))
})
The localize getter sets localizable: true on the annotation, which tells the designer to show the localization editor for this
property. Without localize, the property will not have a localization option in the designer UI.
Example: Localizing radio group items
In this example, a radio group component has its items property localized. The option values (small, medium, large) remain the
same across languages, but the labels change.
{
"form": {
"key": "Screen",
"type": "Screen",
"children": [
{
"key": "sizeSelect",
"type": "RsRadioGroup",
"props": {
"name": {
"value": "Size"
},
"label": {
"computeType": "localization"
},
"items": {
"computeType": "localization"
}
}
}
]
},
"localization": {
"en-US": {
"sizeSelect": {
"component": {
"label": "Size",
"items": [
{
"value": "small",
"label": "Small"
},
{
"value": "medium",
"label": "Medium"
},
{
"value": "large",
"label": "Large"
}
]
}
}
},
"de-DE": {
"sizeSelect": {
"component": {
"label": "Gr\u00f6\u00dfe",
"items": [
{
"value": "small",
"label": "Klein"
},
{
"value": "medium",
"label": "Mittel"
},
{
"value": "large",
"label": "Gro\u00df"
}
]
}
}
}
},
"languages": [
{
"code": "en",
"dialect": "US",
"name": "English",
"description": "American English",
"bidi": "ltr"
},
{
"code": "de",
"dialect": "DE",
"name": "Deutsch",
"description": "German",
"bidi": "ltr"
}
],
"defaultLanguage": "en-US"
}
Array localization values are stored as JSON arrays directly in the localization object, not as Fluent message strings. FormEngine automatically detects whether a localization value is a JSON array or object and parses it accordingly.
Localizing Validation Errors
Validation error messages can also be localized. Each validator can have its error message specified in the localization data.
Example: Validation Error Localization
{
"tooltipType": "MuiTooltip",
"errorType": "MuiErrorWrapper",
"form": {
"key": "Screen",
"type": "Screen",
"children": [
{
"key": "langSelect",
"type": "MuiSelect",
"props": {
"items": {
"value": [
{
"value": "en-US",
"label": "American English"
},
{
"value": "de-DE",
"label": "Deutsch"
}
]
},
"label": {
"computeType": "localization"
}
},
"events": {
"onChange": [
{
"name": "onLangChange",
"type": "custom"
}
]
}
},
{
"key": "email",
"type": "MuiTextField",
"props": {
"label": {
"value": "Email"
}
},
"schema": {
"validations": [
{
"key": "email"
},
{
"key": "required"
}
]
}
},
{
"key": "age",
"type": "MuiTextField",
"props": {
"label": {
"computeType": "localization"
}
},
"schema": {
"validations": [
{
"key": "code",
"args": {
"code": " return parseInt(value) >= 18"
}
}
]
}
},
{
"key": "validateButton",
"type": "MuiButton",
"props": {
"children": {
"computeType": "localization"
}
},
"events": {
"onClick": [
{
"name": "validate",
"type": "common"
}
]
}
}
]
},
"localization": {
"en-US": {
"langSelect": {
"component": {
"label": "Language"
}
},
"email": {
"validator-email": {
"message": "Please enter a valid email address"
},
"validator-required": {
"message": "Email is required"
}
},
"age": {
"validator-code": {
"message": "You must be at least 18 years old"
},
"component": {
"label": "Age"
}
},
"validateButton": {
"component": {
"children": "Validate"
}
}
},
"de-DE": {
"langSelect": {
"component": {
"label": "Sprache"
}
},
"email": {
"validator-email": {
"message": "Bitte geben Sie eine gültige E-Mail-Adresse ein"
},
"validator-required": {
"message": "E-Mail ist erforderlich"
}
},
"age": {
"validator-code": {
"message": "Sie müssen mindestens 18 Jahre alt sein"
},
"component": {
"label": "Alter"
}
},
"validateButton": {
"component": {
"children": "Validieren"
}
}
}
},
"languages": [
{
"code": "en",
"dialect": "US",
"name": "English",
"description": "American English",
"bidi": "ltr"
},
{
"code": "de",
"dialect": "DE",
"name": "Deutsch",
"description": "German",
"bidi": "ltr"
}
],
"defaultLanguage": "en-US"
}
The validator type in the localization object uses the format validator-{validatorKey} (e.g., validator-email, validator-required).
Live example
function App() { const form = useMemo(() => ({ "tooltipType": "MuiTooltip", "errorType": "MuiErrorWrapper", "form": { "key": "Screen", "type": "Screen", "children": [ { "key": "langSelect", "type": "MuiSelect", "props": { "items": { "value": [ { "value": "en-US", "label": "American English" }, { "value": "de-DE", "label": "Deutsch" } ] }, "label": { "computeType": "localization" } }, "events": { "onChange": [ { "name": "onLangChange", "type": "custom" } ] } }, { "key": "email", "type": "MuiTextField", "props": { "label": { "value": "Email" } }, "schema": { "validations": [ { "key": "email" }, { "key": "required" } ] } }, { "key": "age", "type": "MuiTextField", "props": { "label": { "computeType": "localization" } }, "schema": { "validations": [ { "key": "code", "args": { "code": " return parseInt(value) >= 18" } } ] } }, { "key": "validateButton", "type": "MuiButton", "props": { "children": { "computeType": "localization" } }, "events": { "onClick": [ { "name": "validate", "type": "common" } ] } } ] }, "localization": { "en-US": { "langSelect": { "component": { "label": "Language" } }, "email": { "validator-email": { "message": "Please enter a valid email address" }, "validator-required": { "message": "Email is required" } }, "age": { "validator-code": { "message": "You must be at least 18 years old" }, "component": { "label": "Age" } }, "validateButton": { "component": { "children": "Validate" } } }, "de-DE": { "langSelect": { "component": { "label": "Sprache" } }, "email": { "validator-email": { "message": "Bitte geben Sie eine gültige E-Mail-Adresse ein" }, "validator-required": { "message": "E-Mail ist erforderlich" } }, "age": { "validator-code": { "message": "Sie müssen mindestens 18 Jahre alt sein" }, "component": { "label": "Alter" } }, "validateButton": { "component": { "children": "Validieren" } } } }, "languages": [ { "code": "en", "dialect": "US", "name": "English", "description": "American English", "bidi": "ltr" }, { "code": "de", "dialect": "DE", "name": "Deutsch", "description": "German", "bidi": "ltr" } ], "defaultLanguage": "en-US" }), []) const getForm = useCallback(() => JSON.stringify(form), [form]) const [language, setLanguage] = useState('en-US') const actions = useMemo(() => ({ onLangChange: (e)=> { setLanguage(e.args[0] ?? 'en-US') } }), []) const initialData = useMemo(() => ({langSelect: 'en-US'}), []) return <FormViewer initialData={initialData} view={muiView} getForm={getForm} actions={actions} language={language} /> }
Validation error messages are not automatically translated when the language is changed, but they change after calling the validate
action.
Localizing Tooltips and Modal Windows
FormEngine also supports localization of tooltips and modal windows, allowing you to provide translated help text and modal content for different languages.
Tooltip Localization
Tooltips can be localized using the tooltip type in the localization object. Component tooltip properties are typically defined in the
tooltipProps section of a component.
Example: Tooltip Localization
{
"tooltipType": "MuiTooltip",
"errorType": "MuiErrorWrapper",
"form": {
"key": "Screen",
"type": "Screen",
"children": [
{
"key": "langSelect",
"type": "MuiSelect",
"props": {
"items": {
"value": [
{
"value": "en-US",
"label": "American English"
},
{
"value": "de-DE",
"label": "Deutsch"
},
{
"value": "fr-FR",
"label": "Français"
}
]
},
"label": {
"computeType": "localization"
}
},
"events": {
"onChange": [
{
"name": "onLangChange",
"type": "custom"
}
]
}
},
{
"key": "email",
"type": "MuiTextField",
"props": {
"label": {
"value": "Email"
}
},
"tooltipProps": {
"title": {
"computeType": "localization"
}
}
}
]
},
"localization": {
"en-US": {
"langSelect": {
"component": {
"label": "Language"
}
},
"email": {
"tooltip": {
"title": "Enter your email address for notifications"
}
}
},
"de-DE": {
"langSelect": {
"component": {
"label": "Sprache"
}
},
"email": {
"tooltip": {
"title": "Geben Sie Ihre E-Mail-Adresse für Benachrichtigungen ein"
}
}
},
"fr-FR": {
"langSelect": {
"component": {
"label": "Langue"
}
},
"email": {
"tooltip": {
"title": "Entrez votre adresse e-mail pour les notifications"
}
}
}
},
"languages": [
{
"code": "en",
"dialect": "US",
"name": "English",
"description": "American English",
"bidi": "ltr"
},
{
"code": "de",
"dialect": "DE",
"name": "Deutsch",
"description": "German",
"bidi": "ltr"
},
{
"code": "fr",
"dialect": "FR",
"name": "Français",
"description": "French",
"bidi": "ltr"
}
],
"defaultLanguage": "en-US"
}
The tooltip type in the localization object uses the format tooltip (e.g., tooltip.title).
Live example
function App() { const form = useMemo(() => ({ "tooltipType": "MuiTooltip", "errorType": "MuiErrorWrapper", "form": { "key": "Screen", "type": "Screen", "children": [ { "key": "langSelect", "type": "MuiSelect", "props": { "items": { "value": [ { "value": "en-US", "label": "American English" }, { "value": "de-DE", "label": "Deutsch" }, { "value": "fr-FR", "label": "Français" } ] }, "label": { "computeType": "localization" } }, "events": { "onChange": [ { "name": "onLangChange", "type": "custom" } ] } }, { "key": "email", "type": "MuiTextField", "props": { "label": { "value": "Email" } }, "tooltipProps": { "title": { "computeType": "localization" } } } ] }, "localization": { "en-US": { "langSelect": { "component": { "label": "Language" } }, "email": { "tooltip": { "title": "Enter your email address for notifications" } } }, "de-DE": { "langSelect": { "component": { "label": "Sprache" } }, "email": { "tooltip": { "title": "Geben Sie Ihre E-Mail-Adresse für Benachrichtigungen ein" } } }, "fr-FR": { "langSelect": { "component": { "label": "Langue" } }, "email": { "tooltip": { "title": "Entrez votre adresse e-mail pour les notifications" } } } }, "languages": [ { "code": "en", "dialect": "US", "name": "English", "description": "American English", "bidi": "ltr" }, { "code": "de", "dialect": "DE", "name": "Deutsch", "description": "German", "bidi": "ltr" }, { "code": "fr", "dialect": "FR", "name": "Français", "description": "French", "bidi": "ltr" } ], "defaultLanguage": "en-US" }), []) const getForm = useCallback(() => JSON.stringify(form), [form]) const [language, setLanguage] = useState('en-US') const actions = useMemo(() => ({ onLangChange: (e)=> { setLanguage(e.args[0] ?? 'en-US') } }), []) const initialData = useMemo(() => ({langSelect: 'en-US'}), []) return <FormViewer initialData={initialData} view={muiView} getForm={getForm} actions={actions} language={language} /> }
Modal Window Localization
Modal windows can be localized using the modal type in the localization object. Modal content typically comes from templates defined in the form, and these templates can contain localized content.
{
"key": "confirmationModal",
"type": "Modal",
"props": {
"modalTemplate": {
"value": "Template:confirmation-dialog"
}
},
"modal": {
"props": {
"cancelButton": {
"computeType": "localization"
},
"confirmButton": {
"computeType": "localization"
},
"title": {
"computeType": "localization"
}
}
}
}
Example: Modal Window Localization
Below is an example of a simple dialog box for which localization will be applied:
import type {DialogProps} from '@mui/material'
import {Button, Dialog, DialogActions, DialogContent, DialogTitle} from '@mui/material'
import {boolean, define, oneOf} from '@react-form-builder/core'
import type {SyntheticEvent} from 'react'
import {useCallback} from 'react'
export interface MyDialogProps extends DialogProps {
handleClose?: () => void
cancelButton?: string
confirmButton?: string
title?: string
}
const MyDialog = (props: MyDialogProps) => {
const {children, cancelButton, confirmButton, handleClose, onClose, title, ...rest} = props
const close = useCallback((e: SyntheticEvent, reason: 'backdropClick' | 'escapeKeyDown') => {
handleClose?.()
onClose?.(e, reason)
}, [handleClose, onClose])
const closeDialog = useCallback(() => {
handleClose?.()
}, [handleClose])
return <Dialog {...rest} onClose={close}>
<DialogTitle>{title}</DialogTitle>
<DialogContent>
{children}
</DialogContent>
<DialogActions>
<Button onClick={closeDialog}>{cancelButton}</Button>
<Button onClick={closeDialog}>{confirmButton}</Button>
</DialogActions>
</Dialog>
}
export const myDialog = define(MyDialog, 'MyDialog')
.props({
open: boolean.default(false),
fullWidth: boolean.default(false),
scroll: oneOf('paper', 'body').default('paper'),
})
.componentRole('modal')
.build()
Live example
function App() { const MyDialog = (props) => { const {children, cancelButton, confirmButton, handleClose, onClose, title, ...rest} = props const close = useCallback((e, reason) => { handleClose?.() onClose?.(e, reason) }, [handleClose, onClose]) const closeDialog = useCallback(() => { handleClose?.() }, [handleClose]) return <Dialog {...rest} onClose={close}> <DialogTitle>{title}</DialogTitle> <DialogContent> {children} </DialogContent> <DialogActions> <Button onClick={closeDialog}>{cancelButton}</Button> <Button onClick={closeDialog}>{confirmButton}</Button> </DialogActions> </Dialog> } const myDialog = define(MyDialog, 'MyDialog') .props({ open: boolean.default(false), fullWidth: boolean.default(false), scroll: oneOf('paper', 'body').default('paper'), }) .componentRole('modal') .build() const confirmationDialog = useMemo(() => ({ "form": { "key": "Screen", "type": "Screen", "children": [ { "key": "content", "type": "MuiTypography", "props": { "children": { "computeType": "localization" } } } ] }, "localization": { "en-US": { "content": { "component": { "children": "Are you sure you want to proceed with this action?" } } }, "es-ES": { "content": { "component": { "children": "¿Está seguro de que desea proceder con esta acción?" } } } }, "languages": [ { "code": "en", "dialect": "US", "name": "English", "description": "American English", "bidi": "ltr" }, { "code": "es", "dialect": "ES", "name": "Español", "description": "Spanish", "bidi": "ltr" } ], "defaultLanguage": "en-US" }), []) const form = useMemo(() => ({ "modalType": "MyDialog", "form": { "key": "Screen", "type": "Screen", "children": [ { "key": "confirmationModal", "type": "Modal", "props": { "modalTemplate": { "value": "Template:confirmation-dialog" } }, "modal": { "props": { "cancelButton": { "computeType": "localization" }, "confirmButton": { "computeType": "localization" }, "title": { "computeType": "localization" } } } }, { "key": "langSelect", "type": "MuiSelect", "props": { "items": { "value": [ { "value": "en-US", "label": "American English" }, { "value": "es-ES", "label": "Español" } ] }, "label": { "computeType": "localization" } }, "events": { "onChange": [ { "name": "onLangChange", "type": "custom" } ] } }, { "key": "showModalButton", "type": "MuiButton", "props": { "children": { "computeType": "localization" } }, "events": { "onClick": [ { "name": "openModal", "type": "common", "args": { "modalKey": "confirmationModal" } } ] } } ] }, "localization": { "en-US": { "langSelect": { "component": { "label": "Language" } }, "showModalButton": { "component": { "children": "Show Confirmation" } }, "confirmationModal": { "modal": { "title": "Confirm Action", "confirmButton": "Yes, proceed", "cancelButton": "Cancel" } } }, "es-ES": { "langSelect": { "component": { "label": "Idioma" } }, "showModalButton": { "component": { "children": "Mostrar Confirmación" } }, "confirmationModal": { "modal": { "title": "Confirmar Acción", "confirmButton": "Sí, proceder", "cancelButton": "Cancelar" } } } }, "languages": [ { "code": "en", "dialect": "US", "name": "English", "description": "American English", "bidi": "ltr" }, { "code": "es", "dialect": "ES", "name": "Español", "description": "Spanish", "bidi": "ltr" } ], "defaultLanguage": "en-US" }), []) const getForm = useCallback((name) => { const data = name === 'confirmation-dialog' ? confirmationDialog : form return JSON.stringify(data) }, [confirmationDialog, form]) muiView.define(myDialog.model) const [language, setLanguage] = useState<LanguageFullCode>('en-US') const actions = useMemo(() => ({ onLangChange: (e) => { setLanguage(e.args[0] ?? 'en-US') } }), []) const initialData = useMemo(() => ({langSelect: 'en-US'}), []) return <FormViewer initialData={initialData} view={muiView} getForm={getForm} actions={actions} language={language} /> }
Setting Form Language
The form language can be controlled using the language property on the FormViewer component. This property accepts a language code in
the format {code}-{dialect} (e.g., en-US, de-DE).
Example: React Component with Language Selection
import {view as muiView} from '@react-form-builder/components-material-ui'
import {FormViewer, type LanguageFullCode} from '@react-form-builder/core'
import {useCallback, useMemo, useState} from 'react'
function App() {
const form = useMemo(() => ({
"form": {
"key": "Screen",
"type": "Screen",
}
}), [])
const getForm = useCallback(() => JSON.stringify(form), [form])
const [language, setLanguage] = useState<LanguageFullCode>('en-US')
return (
<div>
<div>
<label>Select Language: </label>
<select value={language} onChange={(e) => setLanguage(e.target.value as LanguageFullCode)}>
<option value="en-US">English</option>
<option value="de-DE">German</option>
<option value="fr-FR">French</option>
<option value="es-ES">Spanish</option>
</select>
</div>
<FormViewer
getForm={getForm}
view={muiView}
language={language}
/>
</div>
)
}
Live example
function App() { const form = useMemo(() => ({ "form": { "key": "Screen", "type": "Screen", "children": [ { "key": "langSelect", "type": "MuiSelect", "props": { "disabled": { "value": true }, "items": { "value": [ { "value": "en-US", "label": "American English" }, { "value": "de-DE", "label": "Deutsch" }, { "value": "fr-FR", "label": "Français" }, { "value": "es-ES", "label": "Español" } ] }, "label": { "computeType": "localization" } } } ] }, "localization": { "en-US": { "langSelect": { "component": { "label": "Language" } } }, "de-DE": { "langSelect": { "component": { "label": "Sprache" } } }, "fr-FR": { "langSelect": { "component": { "label": "Langue" } } }, "es-ES": { "langSelect": { "component": { "label": "Idioma" } } } }, "languages": [ { "code": "en", "dialect": "US", "name": "English", "description": "American English", "bidi": "ltr" }, { "code": "de", "dialect": "DE", "name": "Deutsch", "description": "German", "bidi": "ltr" }, { "code": "fr", "dialect": "FR", "name": "Français", "description": "French", "bidi": "ltr" }, { "code": "es", "dialect": "ES", "name": "Español", "description": "Spanish", "bidi": "ltr" } ], "defaultLanguage": "en-US" }), []) const getForm = useCallback(() => JSON.stringify(form), [form]) const [language, setLanguage] = useState('en-US') return ( <div> <div> <label>Select Language: </label> <select value={language} onChange={(e) => setLanguage(e.target.value)}> <option value="en-US">English</option> <option value="de-DE">German</option> <option value="fr-FR">French</option> <option value="es-ES">Spanish</option> </select> </div> <FormViewer getForm={getForm} view={muiView} language={language} /> </div> ) }
Fallback Localization Mechanism
FormEngine provides a robust fallback mechanism to ensure users always see meaningful text even when translations are incomplete or missing.
How Fallback Works
Localized component properties (computeType: "localization") are resolved in two stages:
- Message-level lookup (requested language bundle, then default language bundle).
- Property-level fallback when no usable localized value remains.
For each message id, Fluent localization lookup works in this order:
- Explicit empty string in requested-language items (
"") - treated as a real translation value. - Requested-language Fluent bundle message.
- Explicit empty string in default-language items (
"") - also treated as a real translation value. - Default-language Fluent bundle message.
If no message is resolved (or a whitespace-only Fluent message formats to empty; see note below), component-property fallback continues in this order:
- Component property value (
props[propertyKey].value) when defined. - Component model default property (
model.defaultProps[propertyKey]) when defined. - Component type as the final fallback string.
For localized validation messages, the engine uses the same message-level lookup (requested then default). It does not apply the component property/model/type fallback chain for validation errors.
This fallback mechanism applies to component properties, validation errors, tooltips, and modal content that use the Fluent localization pipeline.
An explicit empty localized string ("") is treated as a real translation value, not as "missing". For Fluent messages that contain only
whitespace, Fluent formatting can normalize the rendered output to an empty value. For component properties, the property-level fallback
chain is then applied.
Example: Fallback Chain in Action
Consider a form with these language definitions:
{
"localization": {
"en-US": {
"emailField": {
"component": {
"label": "Email Address",
"helperText": "Enter your email"
}
}
},
"fr": {
"emailField": {
"component": {
"label": "Adresse Email"
}
}
}
},
"languages": [
{
"code": "en",
"dialect": "US",
"name": "English",
"description": "American English",
"bidi": "ltr"
},
{
"code": "fr",
"dialect": "FR",
"name": "Français",
"description": "French",
"bidi": "ltr"
}
],
"defaultLanguage": "en-US"
}
When a user requests fr-CA (Canadian French):
- FormEngine looks for
fr-CAtranslations - Not found - Falls back to
fr(French) - Finds"label": "Adresse Email"buthelperTextis missing - Falls back to form default
en-US- Uses"helperText": "Enter your email"for the missing translation - Result:
labelin French,helperTextin English
Live example
function App() { const form = useMemo(() => ({ "form": { "key": "Screen", "type": "Screen", "children": [ { "key": "langSelect", "type": "MuiSelect", "props": { "items": { "value": [ { "value": "en-US", "label": "American English" }, { "value": "fr-CA", "label": "Français (Canadian)" } ] }, "label": { "computeType": "localization" } }, "events": { "onChange": [ { "name": "onLangChange", "type": "custom" } ] } }, { "key": "email", "type": "MuiTextField", "props": { "label": { "computeType": "localization" }, "helperText": { "computeType": "localization" } } } ] }, "localization": { "en-US": { "langSelect": { "component": { "label": "Language" } }, "email": { "component": { "label": "Email Address", "helperText": "Enter your full name" } } }, "fr-FR": { "email": { "component": { "label": "Adresse Email", } }, "langSelect": { "component": { "label": "Idioma" } } } }, "languages": [ { "code": "en", "dialect": "US", "name": "English", "description": "American English", "bidi": "ltr" }, { "code": "fr", "dialect": "FR", "name": "Français", "description": "French", "bidi": "ltr" }, { "code": "fr", "dialect": "CA", "name": "Français canadien", "description": "Canadian French", "bidi": "ltr" } ], "defaultLanguage": "en-US" }), []) const getForm = useCallback(() => JSON.stringify(form), [form]) const [language, setLanguage] = useState('en-US') const actions = useMemo(() => ({ onLangChange: (e)=> { setLanguage(e.args[0] ?? 'en-US') } }), []) const initialData = useMemo(() => ({langSelect: 'en-US'}), []) return <FormViewer initialData={initialData} view={muiView} getForm={getForm} actions={actions} language={language} /> }
Best Practices for Fallback
- Always provide complete translations for your default language - This ensures a complete user experience even when other languages are incomplete
- Use base language translations for regional dialects - Provide
frtranslations that work for all French dialects (fr-FR,fr-CA,fr-BE, etc.) - Test with incomplete translations - Verify your form works correctly when some translations are missing
- Define all languages in the
languagesarray - Ensure every language used inlocalizationhas a corresponding entry in thelanguagesarray - Use consistent language codes - If you define
fr-FRinlanguages, usefr-FR(notfr) in thelocalizationobject keys
External Localization with localize Function
For more advanced scenarios where you need to load translations from external sources or integrate with existing translation systems, you can use the localize property on FormViewerProps.
The ComponentLocalizer Type
The localize property accepts a ComponentLocalizer function
with the following signature:
type ComponentLocalizer = (
componentStore: ComponentStore,
language: Language
) => Record<string, any>;
componentStore: Contains information about the component being localizedlanguage: The current language selected for the form- Returns: An object with localized property values for the component
Example: External Translation Service Integration
import {view as muiView} from '@react-form-builder/components-material-ui'
import {type ComponentLocalizer, FormViewer, type FormViewerProps, type LanguageFullCode} from '@react-form-builder/core'
import {useCallback, useMemo, useState} from 'react'
function App() {
const form = useMemo(() => ({
"form": {
"key": "Screen",
"type": "Screen",
"children": [
{
"key": "langSelect",
"type": "MuiSelect",
"props": {
"items": {
"value": [
{
"value": "en-US",
"label": "American English"
},
{
"value": "es-ES",
"label": "Español"
}
]
},
"label": {
"computeType": "localization"
}
},
"events": {
"onChange": [
{
"name": "onLangChange",
"type": "custom"
}
]
}
},
{
"key": "nameInput",
"type": "MuiTextField",
"props": {
"label": {
"computeType": "localization"
},
"helperText": {
"computeType": "localization"
}
}
}
]
},
"localization": {
"en-US": {
"langSelect": {
"component": {
"label": "Language"
}
},
},
"es-ES": {
"langSelect": {
"component": {
"label": "Idioma"
}
}
}
},
"languages": [
{
"code": "en",
"dialect": "US",
"name": "English",
"description": "American English",
"bidi": "ltr"
},
{
"code": "es",
"dialect": "ES",
"name": "Español",
"description": "Spanish",
"bidi": "ltr"
}
],
"defaultLanguage": "en-US"
}), [])
const getForm = useCallback(() => JSON.stringify(form), [form])
const [language, setLanguage] = useState<LanguageFullCode>('en-US')
const actions: FormViewerProps['actions'] = useMemo(() => ({
onLangChange: (e) => {
setLanguage(e.args[0] ?? 'en-US')
}
}), [])
const initialData = useMemo(() => ({langSelect: 'en-US'}), [])
const translate = useCallback((componentKey: string, langFullCode: LanguageFullCode) => {
if (componentKey === 'nameInput') {
if (langFullCode === 'es-ES') {
return {
label: 'Nombre Completo',
helperText: 'Introduce tu nombre completo'
// Add any other properties that need localization
}
}
return {
label: 'Full Name',
helperText: 'Enter your full name'
// Add any other properties that need localization
}
}
return {}
}, [])
const customLocalizer: ComponentLocalizer = useCallback((componentStore, language) => {
const componentKey = componentStore.key
// Use your external translation service
return translate(componentKey, language.fullCode)
}, [translate])
return <FormViewer
initialData={initialData}
view={muiView}
getForm={getForm}
actions={actions}
language={language}
localize={customLocalizer}
/>
}
Live example
function App() { const form = useMemo(() => ({ "form": { "key": "Screen", "type": "Screen", "children": [ { "key": "langSelect", "type": "MuiSelect", "props": { "items": { "value": [ { "value": "en-US", "label": "American English" }, { "value": "es-ES", "label": "Español" } ] }, "label": { "computeType": "localization" } }, "events": { "onChange": [ { "name": "onLangChange", "type": "custom" } ] } }, { "key": "nameInput", "type": "MuiTextField", "props": { "label": { "computeType": "localization" }, "helperText": { "computeType": "localization" } } } ] }, "localization": { "en-US": { "langSelect": { "component": { "label": "Language" } }, }, "es-ES": { "langSelect": { "component": { "label": "Idioma" } } } }, "languages": [ { "code": "en", "dialect": "US", "name": "English", "description": "American English", "bidi": "ltr" }, { "code": "es", "dialect": "ES", "name": "Español", "description": "Spanish", "bidi": "ltr" } ], "defaultLanguage": "en-US" }), []) const getForm = useCallback(() => JSON.stringify(form), [form]) const [language, setLanguage] = useState('en-US') const actions = useMemo(() => ({ onLangChange: (e) => { setLanguage(e.args[0] ?? 'en-US') } }), []) const initialData = useMemo(() => ({langSelect: 'en-US'}), []) const translate = useCallback((componentKey, langFullCode) => { if (componentKey === 'nameInput') { if (langFullCode === 'es-ES') { return { label: 'Nombre Completo', helperText: 'Introduce tu nombre completo' // Add any other properties that need localization } } return { label: 'Full Name', helperText: 'Enter your full name' // Add any other properties that need localization } } return {} }, []) const customLocalizer = useCallback((componentStore, language) => { const componentKey = componentStore.key // Use your external translation service return translate(componentKey, language.fullCode) }, [translate]) return <FormViewer initialData={initialData} view={muiView} getForm={getForm} actions={actions} language={language} localize={customLocalizer} /> }
In this example, you can also observe hybrid mode. The language selection component is translated into JSON, and the name input component is translated using an external function.
Best Practices
- Always provide a default language - Ensure your form has a sensible default language for users who don't have a language preference set.
- Use descriptive component keys - Component keys like
emailFieldorsubmitButtonmake localization easier to manage than generic keys likeinput1orbutton2. - Keep localization data organized - Group related translations together and consider maintaining separate translation files for large forms.
- Test bidirectional text support - For RTL languages like Arabic or Hebrew, ensure your form layout handles text direction correctly.
- Consider text expansion - Some languages require more space for the same meaning. Design your forms with flexible layouts to accommodate longer text.
- Fallback strategy - Implement a fallback strategy for missing translations, with clear defaults at least to your form default language and predictable final values.
Summary
FormEngine Core provides a robust localization system that supports:
- Inline JSON localization for self-contained forms
- Component property localization for labels, placeholders, tooltips, and more
- Array data localization for dropdown options, radio groups, checkbox lists, and other list-based components
- Validation error localization for user-friendly error messages in multiple languages
- Language switching via the
languageproperty - External localization integration through the
localizefunction
Whether you need simple multi-language support or complex translation system integration, FormEngine's localization features provide the flexibility to meet your internationalization requirements.
For more information: