Part of my role at ServiceNow involves creating applications that can be used across the Asia, Pacific, and Japan (APJ) region, and ideally anywhere around the world. The APJ region is probably the most diverse in the world, with many languages spoken, and thus consideration needs to be taken to ensure that the applications I build are usable in multiple languages (or at least take a small amount of time to be adapted).
ServiceNow has quite thorough support for i18n and l18n (short for internationalisation and localisation respectively), and there’s a wealth of knowledge on the internet about how to leverage these features. However there isn’t much out there specifically related to Service Portal, and best practices for doing so. I wanted to document my learnings from the last year or so, and share approaches in the hope that it will help others build better apps for an increasingly multicultural and globalised world.
Types of Translation
In Service Portal, there are a few different sources of text which can be presented to users.
Table and Column Names
Let’s say you’re building a widget which shows a list of records, and you want it to be configurable to source the records from a table set in the widget instance’s options. If you wanted to show a heading for the table chosen (e.g. “Incidents”) and column names at the top of the list (e.g. “Number” and “Short Description”), instead of hard-coding the text you would want to use the functions getLabel()
and getPlural()
for the table and field names. The good news is that these functions will already output the relevant translation for the session language, as long as there’s a corresponding record containing the translation in the sys_documentation
table.
String & HTML Fields
Say the list widget you’re building isn’t a list of Incidents, and is instead a list of services that a customer has. These are dynamic and pulled from a database. Each service has a name (string field), and a description (HTML field). In this case, simply set these fields to be of type “Translated Text” and “Translated HTML”; A different version for each language will be stored in the sys_translated_text
table, and the GlideRecord functions to get the fields values will output the relevant translation for the session language.
Choice Fields
Each choice in the sys_choice
table should have a corresponding version for each language. If you called getDisplayValue()
on a choice field like this, it would output the label for the value of that choice field. Furthermore, you can get the available choices for the language by calling the gs.getSession().getLanguage()
function, and use it’s output in a query to the sys_choice
table.
Images
You should be careful that images can contain text too. Nowadays, with the ability to use web fonts, there’s no longer really any need to have text within images, but there are various scenarios where you might need to have a different image show in a different locale. For example, when showing a picture of someone using Google Maps on their mobile device, it might be important that the map area in the picture is of a location inside the country the user is in.
For client-side JavaScript, in the $scope
of every widget there’s a user
object. This object contains a preferred_language
property which can be inspected to place a different image on the page depending on the user’s language.
If you’re placing the image on the page using CSS (e.g. a background image), Service Portal automatically places the user’s preferred language inside the lang
attribute on the html
element (e.g. lang="en"
), so you can apply different background images by using this attribute in your CSS selectors like so:
html[lang="en"] .myElement {
background-image: url('/my-english-image.jpg');
}
html[lang="ja"] .myElement {
background-image: url('/my-japanese-image.jpg');
}
Other Text
For every bit of text shown on the user interface which doesn’t come from either a table/column definition, or a record, you can use UI Messages. This might be something like welcome text, or an alert shown to a user to tell them their record has been saved.
For each bit of text you define a key, which is usually the translation in English (or the base system language). Then, for each language you want to support you define a record in the Messages table for that language, with the Value field set to the translation for that language.
Quite simple, but how do you handle something like the below?
This is a count of the current test we’re on, and the total number of tests. We’re not going to create a message for every single possible combination of numbers. Instead, we can use the substitution available in the gs.getMessage()
function to insert these dynamic numbers into a common translated string.
Using the key:
Test {0} of {1}
By calling this function:
gs.getMessage('Test {0} of {1}’, 2, 13);
In English it will output:
Test 2 of 13
And in Japanese if we had the value for that key defined as:
テスト {0}/{1}
It would output:
テスト 2/20
The goal with keys is to, where possible, make them reusable. This minimises the amount of database queries, and makes adding translations in new languages quicker. In the case above, we could have a separate key for the word “Test” so that we could build phrases like “Test 5 of 20” and “Step 3 of 6” using the same message. For example, this key:
{0} {1} of {2}
The i18n Provider
Because of Service Portal’s large use of client-side JavaScript, it’s often that you will need to work with Messages in client-side JavaScript, rather than using the server-side gs.getMessage()
function.
The i18n provider comes with Service Portal, and is a helper service for working with translations on the client-side. Being an AngularJS service, you can inject it into any of your widgets Client Scripts, Link functions, Angular Providers, or Widget Dependencies to access translations.
Being a client-side object, you’ll want to ensure that all the messages you might use are pre-loaded. You can imagine if you had 50 pieces of text on the page which needed to be translated, if the translations weren’t pre-loaded then 50 background requests would need to be made which would slow down your page loads dramatically.
To load the a message into the client-side i18n provider do this:
i18n.loadMessage(key, value)
Simply supply the below function with a key, and it will return back the translated value of that key in the current user’s language (so long as it’s been pre-loaded using the loadMessage()
function above).
i18n.getMessage(key)
If your message contains substitution placeholders, pass the message into this function as the first parameter, and the following parameters being the values you want to put into the placeholders.
i18n.format(message, parm1, parm2, ...)
There’s a shorthand way of doing this as well, by using the withValues()
function on the object returned by getMessage()
:
i18n.getMessage(key).withValues(parm1, parm2, ...)
Tips
You CAN use HTML in Messages (but please, only when necessary)
Because of its clear violation of the principle of “Separation of Concerns“, using HTML in Messages makes me feel dirty. Sometimes it’s just unavoidable though, but I try to do it sparingly, and at the very least make it semantic!
To highlight the reason why one might need to do this, compare the same UI element in different languages:
The Message concept used in ServiceNow was built with the knowledge that in different languages the information can appear in a different order.
In English, the phrase above is:
Your idea has been forwarded to Fred Luddy.
In Japanese, the same phrase is:
あなたのアイデアはMara Rineheartに送信されました。
Compared to the English phrase, the verb (in orange) along with the tense (in blue) moves to the end of the phrase in Japanese. This means that object of who the idea is being forwarded to (in green) appears in the middle in the Japanese phrase.
This is exactly what the substitution offered with Messages is designed to handle, however in the user interface the name is bold. To make the name bold we need some way of targeting just that part of the phrase with CSS, and the only way to do that is by wrapping it in a HTML element, perhaps <strong>
like so:
Your idea has been forwarded to <strong>Fred Luddy</strong>
The <strong>
tag is what makes the text bold.
An approach we could use would be to split this phrase into three parts:
- Text that comes before the name
- Name
- Text that comes after the name
Language | beforeString | nameString | afterString |
English | Your idea has been forwarded to | Fred Luddy | |
Japanese | あなたのアイデアは | Fred Luddy | に送信されました。 |
Then, when we place the “Name” component on the page, we could wrap it with the strong tag like so:
{{::beforeString}}<strong>{{::nameString}}</strong>{{::afterString}}
This is a very awkward and complicated solution though, and quickly becomes unmanageable. For example, what happens if we want to also bold the text “Idea” (アイデア)? Then we need 5 different Messages, and perhaps more if other languages need to be supported and they have things in a different order as well!
So wait, does this mean we have to put HTML inside the message string? Well, no – but it’s probably best if you do. Anyone familiar with the concept of “separation of concerns” knows that this isn’t the best, but as long as you keep the HTML semantic it is less bad.
By semantic, I mean understanding that for the text we want to make bold we are placing importance on it, but not specifying how that importance is achieved. You may want to make use of this same Message in other applications and may want to place the importance using a different colour, rather than by making it bold. In this case, using the <b>
tag to make the text bold would be non-semantic (as this tag means “bold”), as would using the <font style="font-weight: bold;”>
tag.
We should use the <strong>
tag and then leave it up to the portal’s CSS to determine how to display this tag in this context.
Create a server-side helper to make getting messages easier
Getting messages can be quite cumbersome, and lead to a lot of repetitive code:
data.messages = {
'Idea': gs.getMessage('Idea'),
'Welcome {0}!': gs.getMessage('Welcome {0}!'),
'Thank you': gs.getMessage('Thank you'),
'Concern': gs.getMessage('Concern')
};
Instead, you can create a helper script include as so:
var Myi18n = Class.create();
Myi18n.prototype = {
initialize: function() {
},
type: 'Myi18n'
};
Myi18n.getMessages = function getMessages () {
var messages = [];
for (var i = 0; i < arguments.length; ++i) {
messages.push({
key: arguments[i],
value: gs.getMessage(arguments[i])
});
}
return messages;
};
Then, getting messages in the Server Script of your widget and passing them to the client is as simple as this:
data.messages = Myi18n.getMessages('Idea', 'Welcome {0}!', 'Thank you', 'Concern');
Create a client-side helper to make loading messages easier
Much like on the server, loading the messages into the Service Portal i18n service can be quite cumbersome and repetitive. Also, it relies on you knowing the key of the messages loaded on the server:
function(i18n) {
i18n.loadMessage('Idea', c.data.messages['Idea']);
i18n.loadMessage('Welcome {0}!', c.data.messages['Welcome {0}!']);
i18n.loadMessage('Thank you', c.data.messages['Thank you']);
i18n.loadMessage('Concern', c.data.messages['Concern']);
}
Instead, you can create an Angular Service like so:
angular.module('MyCoolApp', ['sn.common.i18n'])
.service('Myi18n', function (i18n) {
return {
process: function process (messages) {
messages.forEach(function (cv) {
i18n.loadMessage(cv.key, cv.value);
});
}
};
});
Then you can load the messages into the service in your widget’s Client Script like so:
function(Myi18n) {
Myi18n.process(c.data.messages);
}
Try to avoid in-template translations
It’s possible to apply translations to your widget’s HTML template and Client Script in-line, however I’ve experienced quite a number of issues when the string contains a placeholder, or special character (e.g. an exclamation mark). Thus, I try to avoid this method.
Don’t use Google Translate
Google Translate is good for when you have some text and you want to understand what it means, but it is quite inaccurate and should not be used as a replacement of a human translator. Google Translate doesn’t take context into account, and thus can often result in nonsensical phrases at best, or offensive ones at worst!
It’s OK to start with Google Translate to begin with, but make sure you run the translations past a native speaker to allow them to fix the errors. Also, make sure you provide the native speaker with screenshots of where the text will appear, so that they have context of how the phrase will be used (as this can result in different translations).
Conclusion
Making an application support multiple languages is challenging but super rewarding when you see your work in action.
If you have any tips of your own, or have questions feel free to leave them in the comments below or reach out to me on twitter!