---
title: Localisation
slug: localisation
icon: {"faIcon":"fa-solid fa-language"}
createdAt: 2025-06-10T06:44:30.024Z
---

Servicely supports multiple locales and localisation of messages into multiple languages, dynamic translation of fields and the ability to install language packs (a kind of specialised application) that contain `LocalizedMessage` records for a specific language. All of this is configurable using application properties.

Firstly, note that Servicely refers to locales using *language\_regions*. It also refers to languages using the locale name of the primary region rather than the language name itself, so for example the English language is referred to as *en\_US,* rather than as *en*, in script environments and platform code.

It is important to note at the outset that a locale is not the same as a language. A locale is rather a set of standards for a region for items such as presentation of dates, times and numbers, names of days of the week, whether the week starts on Sunday or Monday, whether time is presented in the 12 or 24 hour clock and so on. A language (and by extension a language pack) is a set of localisations of the messages of the application into a target language. Some locales share the same language for convenience and only differ in the other standards. For example United States English (`en_US`) and Australian English (`en_AU`) are different locales that share the same language (also known as `en_US`) so there is only one localisation for each message. One can still provide specific localisations for either locale, for example the message key "`trash.bin.name`” could be localised as `Trash` in `en_US` and `Rubbish Bin` in `en_AU` but the base product doesn’t include them by default.

The default locale for Servicely is `en_US` and the default language is `en_US`. LocalizedMessage records for `en_US` are stored directly in the applications themselves. Other languages should have their LocalizedMessage records in a language pack (a kind of application) which is not installed by default.

# Locales

Servicely now supports the Unicode CLDR standard [https://cldr.unicode.org/](https://cldr.unicode.org/) for all locale information and use of the `LocaleSettings` table has been discontinued. A new `LocaleCustomization` table has been added to allow custom definitions for date and time formats in CLDR format if the default ones are not suitable.

## Supported Locales

With the introduction of Release 1.8 Servicely support a number of locales in the base product. A number of them share the same language as detailed in the following table:

| **Locale** | **Name**                 | **Default Language** |
| ---------- | ------------------------ | -------------------- |
| af\_ZA     | Afrikaans                | af\_ZA               |
| de\_DE     | German                   | de\_DE               |
| en\_AU     | English (Australia)      | en\_US               |
| en\_GB     | English (United Kingdom) | en\_US               |
| en\_US     | English (United States)  | en\_US               |
| en\_ZA     | English (South Africa)   | en\_US               |
| es\_CL     | Spanish (Chile)          | es\_ES               |
| es\_ES     | Spanish (Spain)          | es\_ES               |
| fr\_FR     | French (France)          | fr\_FR               |
| hy\_AM     | Armenian (Armenia)       | hy\_AM               |
| id\_ID     | Indonesian (Indonesia)   | id\_ID               |
| lo\_LA     | Laotian (Laos)           | lo\_LA               |
| nl\_NL     | Dutch (Netherlands)      | nl\_NL               |
| pt\_BR     | Portuguese (Brazil)      | pt\_BR               |
| pt\_PT     | Portuguese (Portugal)    | pt\_PT               |
| ru\_RU     | Russian (Russia)         | ru\_RU               |
| th\_TH     | Thai (Thailand)          | th\_TH               |
| tr\_TR     | Turkish (Türkiye)        | tr\_TR               |
| uk\_UK     | Ukrainian (Ukraine)      | uk\_UK               |
| zh\_CN     | Chinese (China)          | zh\_CN               |

CLDR information for each locale is included in the UI, along with localisations for many third party UI components. New locales can only be added as part of a new release of the base platform.

An installation of Servicely can determine whether that want to support multilingual capabilities and for which languages and locales by setting application properties appropriately. A list of properties and their meaning is in the following table:

| **Property**               | **Default value**                                                                                                                                              | **Meaning**                                                                                                                       |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| system.multilingual        | false                                                                                                                                                          | Whether to enable multiple language support in the UI including the language switcher and localised message fields certain tables |
| system.supported.locales   | en\_US, en\_AU, en\_GB, en\_ZA, fr\_FR, de\_DE, nl\_NL, es\_ES, es\_CL, pt\_BR, pt\_PT, af\_ZA, id\_ID, ru\_RU, hy\_AM, uk\_UA, th\_TH, lo\_LA, tr\_TR, zh\_CN | Locales to display in the language switcher and the PreferredLanguage field on a user record                                      |
| system.supported.languages | en\_US, fr\_FR, de\_DE, nl\_NL, es\_ES, pt\_BR, pt\_PT, af\_ZA, id\_ID, ru\_RU, hy\_AM, uk\_UA, th\_TH, lo\_LA, tr\_TR, zh\_CN                                 | Languages that are displayed in UI tools for localised message generation.                                                        |

Note that a multilingual installation is still possible even if `system.multilingual` is set to false. This is because a role of `language_editor` is available which enables it for users with that role. They can generate localisations, edit localised fields and switch languages but for users without that role they will see a monolingual installation but with the localisations determined by their `PreferredLangage` setting (which will be read only).

A installation should determine what locales and languages it wants to support by editing the application properties. The default locale and language `en_US` is supported even if the properties are empty or missing.

## Locale Switching

If `system.multilingual` is `true` then a locale switcher component is available in the menu bar. It displays the list of locales that are configured in the installation, in the language of the current locale. By choosing a locale off the list the current users PreferredLanguage is changed to the selection and the page reloaded. A flag is displayed in the menu bar to indicate the current locale. In this sequence of images the locale is changed from United States English to French:



![](https://api.archbee.com/api/optimize/RDSPJwtKQ5B5sE-oMCN16/UGdxio8l7ifjZ9z-7zVvU_2300444708.png)

![](https://api.archbee.com/api/optimize/RDSPJwtKQ5B5sE-oMCN16/5pPEeo8MExNPtrGMl5xOh_2300444702.png)

![](https://api.archbee.com/api/optimize/RDSPJwtKQ5B5sE-oMCN16/FGeFlyi7AHp7YMtSQ5Bh1_2300477442.png)

## Localisation fields

Some tables were developed before their localisation needs were apparent. These tables have been enhanced with additional localised message fields as a complement to the existing monolingual fields, and all display templates have been modified to use the localised message fields (if they have a value) before using the monolingual fields. The localised message fields are hidden if `system.multilingual` is `false` so as to not cause confusion. The localised message field names are based on the monolingual field names with the word Localized as a prefix, so the complementary field for `Description` is `LocalizedDescription`. On a form they have the word Display as a prefix in the title and immediately follow the monolingual field as in this example of a CatalogItem:

![](https://api.archbee.com/api/optimize/RDSPJwtKQ5B5sE-oMCN16/AKQwS0WnzorO-OJ6U751Z_2300575838.png)

The localised message fields visible are `Display name`, `Display summary` and `Display button text`.

The tables affected by this are:

| **Table**       | **Localised Message Fields**                                         | **Child Tables**              |
| --------------- | -------------------------------------------------------------------- | ----------------------------- |
| TemplateReport  | LocalizedNameLocalizedDescription                                    |                               |
| QuestionItem    | LocalizedNameLocalizedDescriptionLocalizedSummaryLocalizedButtonText | CatalogItemRiskAssessmentItem |
| Report          | LocalizedNameLocalizedDescriptionLocalizedTitle                      |                               |
| Dashboard       | LocalizedNameLocalizedDescription                                    | ReportDashboardPortalPage     |
| Question        | LocalizedSectionTitle                                                |                               |
| CatalogCategory | LocalizedNameLocalizedSummary                                        |                               |

## Locale Customisations

Prior to Release 1.8 Servicely used the `LocaleSettings` table to store the definitions of locale properties, such as date and time formats and some number formats. With the introduction of the CLDR this table is no longer necessary and is no longer used. Some existing installations did use it though for customising the date and time formats from the Servicely defaults. To support those customers a new table `LocaleCustomization` has been introduced that only contains date and time customisation fields in CLDR format.

The CLDR has the concept of *skeleton* and *custom* formats. A *skeleton* format is a name like `short` or `long` that is an abstraction that can be applied in any locale and reduces the need to explicitly specify in detail what the format contains. A *custom* format contains the full detail including placement of the various elements of a date or time and other indictors such as AM/PM.

The Servicely default formats are

| **Type**        | **Format** | **Is Skeleton** | **Notes**                                          |
| --------------- | ---------- | --------------- | -------------------------------------------------- |
| Short Date      | short      | Y               |                                                    |
| Long Date       | yMMMEd     | Y               |                                                    |
| Short Time      | short      | Y               |                                                    |
| Long Time       | medium     | Y               |                                                    |
| Short Date Time | short      | Y               | Commonly replaced by Short Date + “ “ + Short Time |
| Long Date Time  | medium     | Y               | Commonly replaced by Long Date + “ “ + Long Time   |

The `LocaleCustomization` table allows the override of any of these values for a locale and allows either skeleton or custom formats:

![](https://api.archbee.com/api/optimize/RDSPJwtKQ5B5sE-oMCN16/v7468L_O-8ksbd2U6MM0S_2300641281.png)

If you want to use a 24 hour format, you can update the short and long time format to be HH\:mm\:ss , which will be 23:50:30 for example, and if you add an a, such as HH\:mm\:ss a, it will also include PM/AM.

## Localisation Packs

Localisation packs for all supported languages are available as special applications listed in the `Application languages` section. They only contain `LocalizedMessage` records. If they are installed using the standard procedure then those messages are available when a locale using that language is selected using the locale switcher.

Currently only the French (`translations-fr_fr`) and German (`translations-de_de`) localisation packs are populated with messages by Servicely for out of the box use. We will be continually improving the translations in the packs to make them more accurate. The `translations` language pack is empty but deployed and is used when experimenting with localisation generation in order to not modify the default ones. There is no English localisation pack because those localisations are directly included in the default installation.

If a customer is generating localisations for a specific language in bulk then it is recommended that the appropriate language pack be installed and the messages generated into there. This allows management of the localisations to be easier.

![](https://api.archbee.com/api/optimize/RDSPJwtKQ5B5sE-oMCN16/IYfa3O8YTlp716nWy3rfz_2300477448.png)

Installation of a localisation pack is a short procedure and usually takes less than a minute. They take effect immediately.

# Localisation

Servicely provides a number of mechanisms for localisation entry points, i.e. places where a localised message can be substituted for a base message, or for a localisation message key. Some of the mechanisms are based on the internationalisation tools that are part of the Angular web framework - because the majority of the Servicely UI is Angular, including Catalog, Portal and Modal components.

## Localisation in Angular

Angular provides the means to localise messages in the HTML template and the TypeScript code of a component. Servicely uses the HTML template approach as-is, but provides a variation on the standard TypeScript approach that provides more flexibility and allows for development of components without needing to initially create LocalizedMessage records. The official documentation can be found at [https://angular.io/guide/i18n-overview](https://angular.io/guide/i18n-overview)

### Localisation in an Angular Template

An Angular template can indicate localisation points by using custom HTML tags. The `i18n` tag indicates that the element contents will be replaced with the localised message, while the `i18n-XXX` tag indicates that the `XXX` attribute will be replaced with the localised message. This is useful for replacing text such as the `href` of an anchor tag. Angular message keys have a marker prefix of “@@” but those characters are not used in the LocalizedMessage table, for example the message key "@@ng.AAA.BBB" is what would be used in the template while "ng.AAA.BBB" would be the message key for the record.

Here is an example of markup from a template:

```text
<a i18n-href="@@ng.0a00000a72811a1181728242dbb22f81.PortalComponent.Template.demo.href"
   href="http://default.place.com">
  <span i18n="@@ng.0a00000a72811a1181728242dbb22f81.PortalComponent.Template.demo.label">
    The default place is {{place}}
  </span>
</a>
```

The body of the span and the href of the anchor will both be replaced by a localised message if one exists. Note that it is necessary to supply a default value for anything that is to be localised otherwise the substitution will not be made.

The default value is also used to derive the structure of the message format itself in the LocalizedMessage table. Note that the default message is `The default place is {{place}}` which has an embedded Angular expression `{{place}}` This indicates that a variable is to be substituted in that part of the string - usually a member variable of the component itself. Because it has a substitution point the messages in the LocalizedMessage table should also have a substitution point, and they will look like `The default place is {0}`, or `Le lieu par défaut est {0}` This allows messages to be built up using the natural order of the localisation, for example one could imagine a language where the natural order would be XXX &#x7B;0} YYY ZZZ, and the substitution of ‘place’ will still be at the &#x7B;0} position. Messages can have an arbitrary number of substitution points indicated by &#x7B;0}, &#x7B;1} etc. Message formats in general also support named substitution points and number and plural substitution points but Servicely does not at the moment.

The message key is designed to allow support for automated tooling to extract messages, which will be developed in a future release. The format of the key is as follows, where name-suffix can be anything you like but without spaces:

```text
ng.ID.TableName.FieldName.name-suffix
```

These can be generated easily inside a Servicely code editor using the context menu or the key sequence Central-F12 (or Cmd-F12 on macOS):

![](https://api.archbee.com/api/optimize/RDSPJwtKQ5B5sE-oMCN16/EWjrG5BDyReCisfpz8arS_2300510211.png)

If there is no localisation for the current locale (i.e. the users PreferredLanguage) then the default message value is used instead. This allows components to be built without having to add the `en_US` message immediately.

### Localisation in an Angular Component

In component code we can use the `$translate` function to perform localisation on template literals. A template literal is a string wrapped in backticks (`` ` ``) that can have substitution points in a similar fashion to a default message in an Angular template. `$translate` is a function that operates on template literals and knows how to localise them, or how to construct the default message if there is no localisation available. An example of such a function call would be:

```text
let msg = $translate('ng.0a00000a719518168171966aa7a50a19.PortalComponent.Template.approve-error')`Error approving: ${response.responseText}`;
```

`$translate` takes a parameter which is the message key and returns a function that can operate on the string `` `Error approving: ${response.responseText}` `` which is a template literal with one substitution point. An equivalent LocalizedMessage would be `Erreur d'approbation : {0}` The return value of the `$translate` function call will be the localised message. The default message is constructed from the parts of the template literal and the substituted strings if it is necessary to use that instead.

It is also possible to just supply a key in which case the call would be:

```text
let msg = $translateKey('ng.0a00000a719518168171966aa7a50a19.PortalComponent.Template.approve-error');
```

## Localisation in Server Scripts

Server scripts (and Script Libraries) can use `$translate` and `$translateKey` in a similar fashion to an Angular component.

It is also possible to use an older form like this:

```text
let msg = i18n.message("message.key");
```

## Generating Localised Messages using a Translation Service

It is possible to generate localised messages using a cloud translation service if the appropriate system properties are set and an access key is supplied as a Bearer token. Servicely supports two translation services for release 1.8: Google Translate or OpenAI.

### Configuration

The relevant application properties are

| **Property**                     | **Default value**               | **Meaning**                                                                                |
| -------------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------ |
| system.translation.service       | google                          | Whether to use Google Translate ('google') or OpenAI ('openai') for generated translations |
| ai.openai.token.name             | OpenAI                          | Name of the Bearer Token used to access OpenAI                                             |
| ai.openai.organization.id        |                                 | Id of the organisation that supplies the Bearer Token                                      |
| ai.openai.model.id               | gpt-4                           | OpenAI model used to generate translations. Release 1.8 uses gpt-4                         |
| system.translation.openai.prompt | *Varies depending on customer…* | Prompt sent to OpenAI to fine-tune translations for the Servicely application.             |

There should be at least one `SystemAPIOutboundToken` record containing the Bearer token for the translation service that is being used.

| **Name**                   | **Type** | **Meaning**                       |
| -------------------------- | -------- | --------------------------------- |
| google.api.translation.key | Bearer   | Bearer token for Google Translate |
| OpenAI                     | Bearer   | Bearer token for OpenAI           |

### Generation

Localised messages can be generated using a Server Script. They are currently done manually on the standard Server Script page, e.g. https\://.servicely.ai/#/View/ServerScript

:::Iframe{code="<instance>"}

:::

The common approach is to generate missing localised messages for a target locale relative to a source locale, usually `en_US`. The messages are generated into the localisation packs mentioned above so that they are all contained in one place for ease of management. In this way the entire application can be localised in a short period of time. Note that if more components are developed then their messages can be generated in the same way (i.e. only doing the missing ones) without affecting already generated ones.

All the commands are part of the Translation server script library.

### Translate a message

Note that this does not create a LocalizedMessage record, it is for testing the integration with a translation service. If a `fromLocale` is not supplied the translation service will assume `en_US`.

```text
Translation.translate(toLocale: String, text: String): String?
Translation.translate(fromLocale: String, toLocale: String, text: String): String?
Translation.translate(fromLocale: String, toLocale: String, text: List<String>): List<String?>
```

```text
Translation.translate("fr_FR", "I am an elk");
// will return
"je suis un élan"
```

### List Missing Translations

This will list all the messages that are in `fromLocale` but not in `toLocale` for the application identified by `applicationNameOrId`. If `fromLocale` is not supplied then `en_US` is assumed. If `applicationNameOrId` is `"*"` then all messages for all applications are listed.

```text
Translation.listMissingTranslations(toLocale: String, applicationNameOrId: String): List<LocalizedMessageRec>
Translation.listMissingTranslations(fromLocale: String, toLocale: String, applicationNameOrId: String): List<LocalizedMessageRec>
```

```text
let missing = Translation.listMissingTranslations("fr_FR", "*");
// will return about 7500 records for a locale that has not had any messages translated
```

### Generate Missing Translations

This will generate all the missing messages using the translation service. Generally takes about two minutes to do a complete set of 7500 messages. If `fromLocale` is not supplied then `en_US` is assumed. If `fromApplicationNameOrId` is `"*"` then all messages for all applications are checked. The target application is `toApplicationNameOrId` and should be one of the localisation packs.

```text
Translation.generateMissingTranslations(toLocale: String, fromApplicationNameOrId: String, toApplicationNameOrId: String): List<LocalizedMessageRec?>
Translation.generateMissingTranslations(fromLocale: String, toLocale: String, fromApplicationNameOrId: String, toApplicationNameOrId: String): List<LocalizedMessageRec?>
```

```text
Translation.generateMissingTranslations("th_TH", "*", "translations-th_th");
// will generate about 7500 records into the Thai localisation pack.
```

### Generate Localised Message

Generate localised messages for the supplied keys. If `fromLocale` is not supplied then `en_US` is assumed.

```text
Translation.generateLocalizedMessage(toLocale: String, fromApplicationNameOrId: String, toApplicationNameOrId: String, key: String): LocalizedMessageRec?
Translation.generateLocalizedMessage(fromLocale: String, toLocale: String, fromApplicationNameOrId: String, toApplicationNameOrId: String, key: String): LocalizedMessageRec?
Translation.generateLocalizedMessage(fromLocale: String, toLocale: String, fromApplicationNameOrId: String, toApplicationNameOrId: String, key: List<String>): List<LocalizedMessageRec?>
```

```text
Translation.generateLocalizedMessage("nl_NL", "platform", "translations-nl_nl", "common.button.create");
// Makes a single Dutch localised message in the correct localisation pack
```

### Replace Localised Message

Replaces an existing message with a better translation.

```text
Translation.replaceLocalizedMessage(locale: String, applicationNameOrId: String, text: String, replacement: String): List<LocalizedMessageRec>
```

```text
Translation.replaceLocalizedMessage("fr_FR", "translations-fr_fr", "NON", "Non");
// Sometimes Google Translate make words all uppercase.
```

### Patch Localised Message

Replace a substring of a message with a better substring.

```text
Translation.patchLocalizedMessage(locale: String, applicationNameOrId: String, text: String, replacement: String): List<LocalizedMessageRec> 
```

```text
Translation.patchLocalizedMessage("fr_FR", "translations-fr_fr", "Jacques", "Pierre");
// Renames all the people
```

### Copy Localised Message

Copy messages from one locale to another. This is used when the translations have be the same in both locales and you don’t want them picked up by the missing translation detector. If `fromLocale` is not supplied then `en_US` is assumed.

```text
Translation.copyLocalizedMessage(fromLocale: String, toLocale: String, fromApplicationNameOrId: String, toApplicationNameOrId: String, key: String): LocalizedMessageRec?
Translation.copyLocalizedMessage(locale: String, fromApplicationNameOrId: String, toApplicationNameOrId: String, key: String): LocalizedMessageRec? 
Translation.copyLocalizedMessage(fromLocale: String, toLocale: String, fromApplicationNameOrId: String, toApplicationNameOrId: String, key: List<String>): List<LocalizedMessageRec?>
Translation.copyLocalizedMessage(toLocale: String, fromApplicationNameOrId: String, toApplicationNameOrId: String, key: List<String>): List<LocalizedMessageRec?> 
```

```text
Translation.copyLocalizedMessage("es_ES", "itsm", "translations-es_es", "table.incident.name");
// We want the Incident table in Spanish to also be called Incident.
```

### Delete Translation

Deletion one or more translations.

```text
Translation.deleteTranslation(locale: String, applicationNameOrId: String, key: String): List<String?>
Translation.deleteTranslation(locale: String, applicationNameOrId: String, key: List<String>): List<String?> 
Translation.deleteTranslation(locale: String, applicationNameOrId: String): List<String?>
Translation.deleteTranslation(locale: String): List<String?>
```

```text
Translation.deleteTranslation("th_TH");
// Delete all Thai localised messages
```

# Dynamic Translation

If you have a translation service set up with an access key it is possible to mark specific fields as supporting dynamic translation.

An application property needs to be enabled first:

| **Property**               | **Default value** | **Meaning**                           |
| -------------------------- | ----------------- | ------------------------------------- |
| system.dynamic.translation | false             | Whether to enable dynamic translation |

By setting the field level renderer property “`dynamic.translation`" to `true` a translate button will appear against that field. When clicked it will place the translation alongside or below the field itself. Translations are also possible for standard journal entries (but not for audit history entries).

Note that The renderer property is at the field level not the view definition level and is accessed from the context menu on the field:



The translate button is a small multilingual icon:

When clicked a translation into the current locale is performed using the cloud translation service

The same applies for journal entries:

Refreshing the form will clear the translations being displayed.
