Skip to content

Localization (i18n)

This document describes how localization works in the system and how to add translations for UI elements, dictionaries, and enums.

The system supports localization for:

Basic

Live Sample · GitHub

To change the interface language, you need to:

  • French — log in as a user demo_fr/demo
  • English — log in as a user demo/demo

Info

To apply the translation of all elements to the selected language, you must log out and log back in, or refresh the page. Simply switching roles does not fully change the interface language.

How does it look?

locale_fr.png

locale_en.png

Pre-setup for Working with Localization

Example

It is necessary to configure the correct language transfer.
The language is determined using:

Step 1. Add custom DynamicLocaleResolver to ApplicationConfig.java

    @Bean
    public LocaleResolver localeResolver() {
        return new DynamicLocaleResolver();
    }
Step 2. Add enum SupportedLanguages
@RequiredArgsConstructor
@Getter
public enum SupportedLanguages {

    ENGLISH(Locale.ENGLISH),
    FRENCH(Locale.FRENCH);

    private final Locale locale;

    public static @NonNull Locale getDefaultLocale() {
        return SupportedLanguages.ENGLISH.getLocale();
    }

}
Step 3. Setting DynamicLocaleResolver

Below is an example of how to configure token parsing and retrieve the locale for each user from Keycloak.

Setting Keycloak

locale_keycloak_fr.png

public class DynamicLocaleResolver extends AcceptHeaderLocaleResolver {

    @NotNull
    @Override
    public Locale resolveLocale(@NotNull HttpServletRequest request) {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null || !(authentication.getPrincipal() instanceof Jwt jwt)) {
            return SupportedLanguages.getDefaultLocale();
        }
        return resolveFromJwt(jwt);
    }

    private Locale resolveFromJwt(Jwt jwt) {
        String localeClaim = jwt.getClaimAsString("locale");

        if (localeClaim == null || localeClaim.isBlank()) {
            return SupportedLanguages.getDefaultLocale();
        }

        return SupportedLanguages.FRENCH.getLocale().getLanguage().equals(localeClaim.toLowerCase()) ?
                SupportedLanguages.FRENCH.getLocale() :
                SupportedLanguages.getDefaultLocale();
    }
}

LocaleContextHolder.getLocale().getLanguage()

or, when using the system locale:

Locale.getDefault()

If for some reason it is not possible to configure the language correctly using the standard approach, a workaround can be used — set the default locale when the application starts:

    @SpringBootApplication
    @ConfigurationPropertiesScan("org.demo.conf")
    public class Application {

        public static void main(String[] args) {
            Locale.setDefault(new Locale("ru"));
            SpringApplication.run(Application.class, args);
        }
    }            

⚠️ Using Locale.setDefault(...) changes the locale for the entire JVM process, so this approach is recommended only as a temporary solution or for single-locale systems.

To work with localization, perform pre-setup on both front-end and back-end, which is necessary for correct handling of the added language.

Front-end:

see more Global Static Text (Front-end)

Step 1

Add a translation file to ui/src/i18n/assets/fr.json containing the translations.
In ui/src/i18n/assets, we already have an en.json file with translations used for the UI. For a new language, it is sufficient to translate the values into the new language.

Use the following naming format(UTF-8): (fr.json)

fr.json
en.json
<lang>.json

Example:

"translation": { 
    "Clear": "Effacer",
    "Copy details to clipboard": "Copier les détails dans le presse-papiers",
    "Details": "Détails",
    "Error": "Erreur",
    "Errors": "Erreurs"
}
Step 2
Register the new language to allow the front-end to handle it. 1.0 ui/src/i18n/assets/local/index.ts
import { Resource } from 'i18next'
import en from './en.json'
import fr from './fr.json'
import ru from './ru.json'

export default {
    en,
    ru,
    fr
} as Resource
1.1 ui/src/i18n/assets/moment/index.ts
import 'moment/locale/ru'
import 'moment/locale/fr'

1.2 Ant supported languages: ui/src/i18n/assets/antd/index.

import { Locale } from 'antd/es/locale-provider'
import { SupportedLanguage } from '../../constants'
import enUs from 'antd/es/locale-provider/en_US'
import ruRu from 'antd/es/locale-provider/ru_RU'
import frFr from 'antd/es/locale-provider/fr_FR'

export default { en: enUs, ru: ruRu, fr: frFr } as { [key in SupportedLanguage]: Locale }

Info

Since release 2.0.18: The frontend automatically detects the user's language: /login parameter language

Before release 2.0.18: Additional frontend development was required to retrieve the user's language. Without this customization, the frontend used a predefined constant.

Back-end:

see more Static Text: Widget / View / Screen

Step 1
Create translation files for Static Text: Widget / View / Screen in: src/main/resources/ui/

Use the following naming format(UTF-8): (messages_.properties)

messages.properties (default)
messages_fr.properties

Step 2 Add supported languages. Add to application.yml :

cxbox:
  localization:
    supported-languages: [ en, fr ]

Static Text

Static localization is used for interface labels, titles, buttons, messages, and other UI text that does not come from business data.

Static Text: Global(Front-end)

This includes common UI labels shared across the entire interface(standard Cxbox buttons, operations, and validation errors handled on the UI side).

Stored on the front-end: translation file to ui/src/i18n/assets/.json containing the translations.

Examples

How does it look?

standartbutton.png

standartbutton_en.png

standartmessage.png

standartmessage_en.png

How to add?

Example

Add a translation file to ui/src/i18n/assets/fr.json containing the translations.

"translation": { 
    "Clear": "Effacer",
    "Copy details to clipboard": "Copier les détails dans le presse-papiers",
    "Details": "Détails",
    "Error": "Erreur",
    "Errors": "Erreurs"
}

Static Text: Widget / View / Screen

This includes text defined directly in configuration files:

  • *.widget.json
  • *.view.json
  • *.screen.json

Such text may include: Titles, Labels, Any custom text specified directly in JSON

Localization is applied by using translation keys instead of hardcoded text.

Use {{ui.client.name}}

Examples Localization

Field Labels

Field labels define how fields are displayed on screens.

How does it look?

field_fr.png

field_en.png

How to add?

Example

Step 1
Use a translation key in screen JSON: ui.client.name

{
  "name": "clientList",
  "title": "",
  "type": "List",
  "bc": "myexample",
  "fields": [
    {
      "title": "{{ui.client.name}}",
      "key": "fullName",
      "type": "input",
      "width": 300
    },
    {
      "title": "{{ui.address}}",
      "key": "address",
      "type": "input",
      "width": 300
    },
    {
      "title": "{{ui.importance}}",
      "key": "importance",
      "type": "dictionary",
      "width": 300
    },
    {
      "title": "{{ui.status}}",
      "key": "status",
      "type": "dictionary",
      "width": 300
    },
    {
      "title": "{{ui.client.date.start}}",
      "key": "dateStart",
      "type": "date"
    }
  ],
  "options": {
    "export": {
      "enabled": true,
      "title": "{{ui.client}}"
    },
    "fullTextSearch": {
      "enabled": true,
      "placeholder": "{{ui.client.find.placeholder}}"
    },
    "filterSetting": {
      "enabled": true
    },
    "actionGroups": {
      "include": [
        "delete",
        "create",
        "save"
      ]
    }
  }

}

Step 2
Add translation to src/main/resources/ui/messages_fr.properties:

ui.screen.screenname=Nom du client

Use recommended key prefixes:

  • ui.* — UI texts
View Titles

Screen titles define the name of a view in the UI.

How does it look?

view_fr.png

view_en.png

How to add?

Example

Step 1
Define title in screen JSON: ui.view.clients

{
  "name": "clientlist",
  "title": "Client List",
  "template": "DashboardView",
  "url": "/screen/client/view/clientlist",
  "widgets": [
    {
      "widgetName": "SecondLevelMenu",
      "position": 0,
      "gridWidth": 24
    },
    {
      "widgetName": "clientListHeader",
      "position": 2,
      "gridWidth": 24
    },
    {
      "widgetName": "clientList",
      "position": 20,
      "gridWidth": 24
    }
  ],
  "rolesAllowed": [
   "CXBOX_FR_USER",
    "ADMIN"
  ]
}

Step 2
Add translation to src/main/resources/ui/messages_fr.properties:

ui.view.clients=Clientes

Use recommended key prefixes:

  • ui.* — UI texts
Screen Titles

Screen titles define the name of a screen in the UI.

How does it look?

screen_fr.png

screen_en.png

How to add?

Example

Step 1
Define title in screen JSON: ui.screen.clients

{
  "name": "client",
  "icon": "team",
  "order": 0,
  "title": "{{ui.screen.screenname}}",
  "navigation": {
    "type": "standard",
    "menu": [
      {
        "title": "{{ui.view.clients}}",
        "child": [
          {
            "viewName": "clientlist"
          }
        ]
      }
    ]
  }
}

Step 2
Add translation to src/main/resources/ui/messages_fr.properties:

  ui.screen.screenname

Use recommended key prefixes:

  • ui.* — UI texts
FullTextSearch placeholder

How does it look?

placeholder_fr.png

placeholder_en.png

How to add?

Example

Step 1
Use a translation key in screen JSON: ui.client.find.placeholder

{
  "name": "clientList",
  "title": "",
  "type": "List",
  "bc": "myexample",
  "fields": [
    {
      "title": "{{ui.client.name}}",
      "key": "fullName",
      "type": "input",
      "width": 300
    },
    {
      "title": "{{ui.address}}",
      "key": "address",
      "type": "input",
      "width": 300
    },
    {
      "title": "{{ui.importance}}",
      "key": "importance",
      "type": "dictionary",
      "width": 300
    },
    {
      "title": "{{ui.status}}",
      "key": "status",
      "type": "dictionary",
      "width": 300
    },
    {
      "title": "{{ui.client.date.start}}",
      "key": "dateStart",
      "type": "date"
    }
  ],
  "options": {
    "export": {
      "enabled": true,
      "title": "{{ui.client}}"
    },
    "fullTextSearch": {
      "enabled": true,
      "placeholder": "{{ui.client.find.placeholder}}"
    },
    "filterSetting": {
      "enabled": true
    },
    "actionGroups": {
      "include": [
        "delete",
        "create",
        "save"
      ]
    }
  }

}

Step 2
Add translation to src/main/resources/ui/messages_fr.properties:

ui.client.find.placeholder=Recherche par client ou adresse

Use recommended key prefixes:

  • ui.* — UI texts

Static Text: Defined in Java

This includes UI text created on the backend, such as:

  • Button captions
  • Popup messages
  • Validation messages
  • etc

Warning

It is important to distinguish statistics from data that are also passed from Java. By data, we mean values that can change (by a user, via the admin UI, etc.) and/or for which additional features are available—such as search, sorting, full-text search, and so on. As a result, Static Text: Defined in Java only require translating the value immediately before it is sent to the front end, anywhere in Java, using the corresponding expression. Data, on the other hand, must support editing, searching, and sorting. Therefore, a task of reverse translation is added—converting the localized value received from the UI back to its internal representation. This is a more complex problem and will be discussed in section Data Localization.

The translation can be performed at any place in Java code where the value is prepared for the UI. Use method LocalizationFormatter.uiMessage("action.add")

Examples Localization

Actions

How does it look?

action_fr.png

action_en.png

How to add?

Example

Step 1
Add translation LocalizationFormatter.uiMessage() to button

    @Override
    public Actions<MyexampleDTO> getActions() {
        return Actions.<MyexampleDTO>builder()
                .save(sv -> sv.text(LocalizationFormatter.uiMessage("action.save")))
                .cancelCreate(ccr -> ccr.text(LocalizationFormatter.uiMessage("action.cancel")).available(bc -> true))
                .create(crt -> crt.text(LocalizationFormatter.uiMessage("action.add")))
                .delete(dlt -> dlt.text(LocalizationFormatter.uiMessage("action.delete")))
                .build();
    }

Step 2
Add translation to src/main/resources/ui/messages_fr.properties:

action.add=Ajouter 

Use recommended key prefixes:

  • action.* — buttons and actions
Business Exception messages

How does it look?

message_business_exception.png

message_business_exception_en.png

How to add?

Example

Step 1
Add translation LocalizationFormatter.uiMessage() to button

    @Override
    protected ActionResultDTO<MyexampleDTO> doUpdateEntity(Myexample entity, MyexampleDTO data, BusinessComponent bc) {

        if (data.isFieldChanged(MyexampleDTO_.dateStart)) {
            LocalDateTime sysdate = LocalDateTime.now();
            if (data.getDateStart() != null && sysdate.compareTo(data.getDateStart()) > 0) {
                throw new BusinessException().addPopup(LocalizationFormatter.uiMessage("business.exception.less.current.date"));
            }
            entity.setDateStart(data.getDateStart());
        }
        setIfChanged(data, MyexampleDTO_.status, entity::setStatus);
        setIfChanged(data, MyexampleDTO_.importance, entity::setImportance);
        setIfChanged(data, MyexampleDTO_.address, entity::setAddress);
        setIfChanged(data, MyexampleDTO_.fullName, entity::setFullName);
        return new ActionResultDTO<>(entityToDto(bc, myexampleRepository.save(entity)))
                .setAction(PostAction.refreshBc(bc));
    }

Step 2
Add translation to src/main/resources/ui/messages_fr.properties:

business.exception.less.current.date=La valeur de ce champ ne peut pas être antérieure à la date actuelle

Data Localization

Warning

By 'data' mean information that can be modified (by users, via the admin panel, etc.) and/or supports additional functions such as search, sorting, full-text search, etc. The LocalizationFormatter.uiMessage() function performs translation in one direction only, while data requires editing, search, full-text search, and sorting capabilities. Since this function does not support reverse translation, we do not recommend using LocalizationFormatter.uiMessage() for data

Enum

How does it look?

enum_fr.png

enum_en.png

How to add?

Example

Step 1 Add PlatformLocaleEnum.java to /conf/cxbox/extension/locale

package org.demo.conf.cxbox.extension.locale;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.NonNull;
import org.springframework.context.i18n.LocaleContextHolder;

import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.function.Supplier;

/**
 * <b>Dont change this class</b>
 * Candidate to move cxbox core
 * Interface for enums
 */
public interface PlatformLocaleEnum<E extends Enum<E> & PlatformLocaleEnum<E>> {

    Map<Locale, Supplier<@NonNull String>> translations();

    /**
     * Converts this enum constant to its string representation based on the current locale.
     */
    @JsonValue
    default String toValue() {
        return toValue(this);
    }

    /**
     * Creates an enum constant from its string representation.
     */
    @JsonCreator
    @SuppressWarnings("unchecked")
    default E fromValue(@NonNull String value) {
        return fromValue((Class<E>) this.getClass(), value);
    }

    /**
     * Converts this enum constant to its string representation based on the current locale.
     * <p>
     * Serialization logic. The current locale is obtained
     * from {@link LocaleContextHolder}. If no translation exists for the current locale,
     * the first available translation is used as a fallback.
     * </p>
     *
     */
    static <E extends Enum<E> & PlatformLocaleEnum<E>> String toValue(
            @NonNull PlatformLocaleEnum<E> e
    ) {
        Locale locale = LocaleContextHolder.getLocale();
        return e.translations()
                .getOrDefault(
                        locale,
                        e.translations().values().stream().findFirst().orElseThrow()
                )
                .get();
    }

    /**
     * Creates an enum constant of the specified type from its string representation.
     * <p>
     * Deserialization logic. It builds
     * a reverse lookup map from all translated values to their corresponding enum constants
     * and uses it to find the matching constant.
     * </p>
     */
    static <E extends Enum<E> & PlatformLocaleEnum<E>> E fromValue(
            @NonNull Class<E> enumClass,
            @NonNull String value
    ) {
        Map<String, E> map = new HashMap<>();
        for (E e : enumClass.getEnumConstants()) {
            for (var entry : e.translations().entrySet()) {
                if (entry != null && entry.getValue() != null) {
                    map.put(entry.getValue().get(), e);
                }
            }
        }
        return map.get(value);
    }

}
Step 2 Add SupportedLanguages.java

package org.demo.conf.cxbox.extension.locale;

import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;

import java.util.Locale;

/**
 * Enumeration of locales supported by the application.
 * <p>
 * For correct operation of a locale, ensure the following steps are properly configured:
 *
 * <p><b>Backend:</b></p>
 * <ul>
 *     <li>Register the locale in this enum by adding a new constant.</li>
 *     <li>Add the locale code to {@code cxbox.localization.supported-languages} in {@code application.yaml}:
 *     </li>
 *     <li>Add resource bundles {@code messages_<code>.properties} in {@code src/main/resources/ui/messages/} for static texts.</li>
 *     <li>To translate dictionaries and enums:
 *         <ul>
 *           <li>For enums: implement a project-level interface (example {@link LocaleEnum},
 *           that extends the core interface {@link PlatformLocaleEnum}) and provide translations in enum constants that inherit that interface.</li>
 *             <li>For dictionaries: add new columns in {@code DICTIONARY_ITEM} for the new language,
 *             and populate {@code dictionary_item_tr} with translations.</li>
 *         </ul>
 *     </li>
 * </ul>
 *
 * <p>
 *    Important: instruction written for cxbox version >= 2.0.18.
 *    For older versions, see <a href="https://github.com/CX-Box/cxbox-demo/pull/606">pull request</a> and change/add required file
 *   <b>Frontend:</b>
 * </p>
 * <ul>
 *     <li>Add a translation file {@code <code>.json} in {@code ui/src/i18n/assets/local/}.</li>
 *     <li>Register the locale in the following files:
 *         <ul>
 *             <li>{@code ui/src/i18n/assets/local/index.ts}</li>
 *             <li>{@code moment/index.ts} (for date/time formats)</li>
 *             <li>{@code antd/index.ts} (for Ant Design UI components)</li>
 *         </ul>
 *     </li>
 * </ul>
 *
 * <p>See the <a href="https://doc.cxbox.org/features/locale/locale/">official documentation</a>
 * for more details.</p>
 *
 * @see Locale
 */
@RequiredArgsConstructor
@Getter
public enum SupportedLanguages {

    ENGLISH(Locale.ENGLISH),
    FRENCH(Locale.FRENCH);

    private final Locale locale;

    public static @NonNull Locale getDefaultLocale() {
        return SupportedLanguages.ENGLISH.getLocale();
    }

}
Step 3 Add LocaleEnum.java Each enum constant must define a value for every supported Locale. Localization is configured via the #translations() map.

package org.demo.conf.cxbox.extension.locale;

import java.util.Locale;
import java.util.Map;
import java.util.function.Supplier;

/**
 * Adding support for a new language
 * Example
 * <pre>{@code
 * public interface LocaleEnum<E extends Enum<E> & PlatformLocaleEnum<E>>
 *         extends PlatformLocaleEnum<E> {
 *
 *     String getValue();
 *     String getValueFr();
 *     String getValueDe();
 *
 *     @Override
 *     default Map<Locale, Supplier<String>> translations() {
 *         return Map.of(
 *             SupportedLanguages.ENGLISH.getLocale(), this::getValue,
 *             SupportedLanguages.FRENCH.getLocale(), this::getValueFr,
 *             SupportedLanguages.GERNANY.getLocale(), this::getValueDe
 *         );
 *     }
 * }
 * }</pre>
 *
 */

public interface LocaleEnum<E extends Enum<E> & PlatformLocaleEnum<E>>
        extends PlatformLocaleEnum<E> {

    String getValue();

    String getValueFr();

    @Override
    default Map<Locale, Supplier<String>> translations() {
        return Map.of(
                SupportedLanguages.getDefaultLocale(), this::getValue,
                SupportedLanguages.FRENCH.getLocale(), this::getValueFr
        );
    }

}
Step 3 implements LocaleEnum.java

@Getter
@AllArgsConstructor
public enum StatusEnum implements LocaleEnum {

    NEW("New", "Nouvelle"),
    INACTIVE("Inactive", "Inactive"),
    IN_PROGRESS("In progress", "En cours");

    @JsonValue
    private final String value;

    private final String valueFr;
}

Dictionary

How does it look?

dict_fr.png

dict_en.png

How to add?

Example

It is necessary to populate the dictionary_item_tr table with translated values for each dictionary, adding the value of the newly introduced language in the language column.

Step 1 Add new column VALUE_FR

    <column name="VALUE_FR" remarks="French language" type="VARCHAR2(255)"/>
Step 2 Add new column VALUE_FR in DICTIONARY.csv

TYPE;KEY;VALUE;VALUE_FR;DISPLAY_ORDER;DESCRIPTION;ACTIVE;ID
BRIEFINGS;PROJECT;Project;Projet;1;;;
BRIEFINGS;SECURITY;Security;Sécurité;2;;;
BRIEFINGS;MARKET;Market;Marché;3;;;

Step 3 Add value_fr in insert for dictionary_item_tr

<sql>
  insert into dictionary_item_tr (id, language, value)
  select id, 'en', value from dictionary_item
  union all
  select id, 'fr', value_fr from dictionary_item;
</sql>