DirkRedeker
Mega Sage

Hi

Today, I watched the LCHH (Live coding Happy Hour) video from Friday 2020-06-12, and there have been things that made me snoopy to try out what was shown in the video.

You can find the video at the link: https://www.youtube.com/watch?v=IyNpgpu8CyU

All credits of this article are dedicated to @Andrew Barnes - AJB , @Chuck Tomasi and @Brad Tilton.
I hope I can put some smiles on your face reading this. I could not resist when Andrew asked for comments in the video, so I started to write this one here.

Those Three did the hard work, and here is just what I learned from this video.
So, at first: "Thanks a lot guys, for all the good stuff I can learn from your publications!"

 

Scenario:

The story is about making Service Portals localized and translated.

Service Portal brings the automatic HTML translation, using ${message_key_from_message_table}, but this only translates static strings.

So what to do, if you want to have text filled up with parameters on the client-side?

The video introduces different options to use, in this kind of scenario (also featuring the "i18n" functions).

View the video upfront if you like, and also have a look at the following link (thanks a lot as well @dylan):

https://www.dylanlindgren.com/2018/11/07/building-multilingual-service-portals/

 

Example for Explanation:

Below, I will present an example of a custom Service Portal widget (very similar to the one that Chuck used in the LCHH YouTube video) and explain step-by-step what happens. You can click on the picture below and zoom-in to see all details of the widget in one place.

find_real_file.png

Review the found panes "HTML Template" (1), "Client Script" (2) an "Server Script" (3) in detail for investigation. Pane "Preview" (4) will reflect the output, that the widget will produce (when placed to Service Portal).

Let's walk through the steps to understand what's going on.....

 

Step #1 - Create example translation texts/messages

As this widget will use translated messages from the "Messages" [sys_ui_message] table, the first thing to prepare the example case is, to capture some message texts to be used as translated texts in the widget.

Navigate to: > System UI > Messages and capture the records in your instance to replay my example.

The screenshot below shows the Message records that I will use for my example:

find_real_file.png

Review the column "Message" for the records above, to find the placeholders ({0} and {1}) to be filled in the widget later.

 

 

Step #2 - Create a new Service Portal Widget

Create a new Service Portal Widget, by navigating to > Service Portal > Widgets.

Click on "New" to create a new Widget Record

find_real_file.png

For testing, just fill in the "Name" (1) and "ID" (2) of the Widget form - similar to shown in the screenshot above. Right-click the header and select "Save" to create the record.

 

 

Step #3 - Open the new Widget in the "Widget Editor"

To have a nice development environment for your newly created widget, scroll down on the Widget Form and click the Related Link "Open in Widget Editor" - see screenshot below.

find_real_file.png

The Widget Editor will open in a new tab of your Web Browser.

Make sure to select the code panes of the Widget Editor as shown in the screenshot below.

find_real_file.png

We will need to work on the "HTML Template", "Client Script" and "Server Script" columns.

 

 

Step #4 - Enable the Preview of the Widget

Before starting to develop the Widget, activate the Preview, so that all changes to the widget will be visible immediately after every SAVE of the code.

To activate the Preview, select the hamburger menu (1) in the top-right corner of the Widget Editor and select "Enable Preview" (2). 

find_real_file.png

Note: You can always switch on/off the Preview pane by clicking the "eye-icon" button (3).

 

 

Step #5 - Fill the Widget with code

Below, you can find the copy/paste versions of the code, found in the widget. I will explain, what each code does, in detail below. 

a) Put the following code to the "HTML" pane

<div>
<!-- your widget template -->
  <div>
    <button class="btn" ng-click="clientActionButton();">
    	Refresh                                                
    </button>
    <br />
    <b>txt_dirk_1: </b> {{c.data.clienttext_1}}<br />
    <b>txt_dirk_2: </b> {{c.data.clienttext_2a}}<br />
    <b>txt_dirk_3: </b> {{c.data.clienttext_3}}<br />
  </div>

  <hr/>
  
  <div>
    <b>A FIXED TEXT IS A FIXED TEXT IS A FIXED TEXT</b>
  </div>

  <hr/>
  
  <div>
    <b>OOB STATIC HTML Translation: </b>${txt_dirk_1}
  </div>
  
  <hr/>
  
  <div>
    <b>Variable 'c.data.check' set in Client Script: </b>{{c.data.check}}<br />
  </div>
  
  <hr/>

  <div>
    <b>Translated Text on Server-Side: </b>{{c.data.messages.txt_dirk_1}}<br />
	</div>
  
  <hr/>
  
  <div>
    <b>${txt_dirk_2}:</b> {{c.data.messages.txt_dirk_2}}<br />
    <b>${txt_dirk_2}:</b> {{c.data.clienttext_2}}<br />
    <b>${txt_dirk_2}:</b> {{c.data.clienttext_2a}}<br />
    <b>${txt_dirk_2}:</b> {{c.data.clienttext_3}}<br />
  </div>

  <hr/>

  <pre>{{c.data | json}}</pre>
</div>

 

b) Put the following code to the "Client Script" pane

function($scope, i18n) {
  /* widget controller */
  var c = this;
	
	// data does not need to come from Server, may be set here as well
	c.data.check = 123;

	// NOTE: This function cannot be called from here, as it is not "yet"
	//       known. It can be called AFTER the function() declaration in script
	// takeSubFunction();

	
	// i18n is a kind of "Local Store" for texts you can grab with
	// i18n.getMessage() on the Client side, similarily like the
	// gs.getMessage() function on the Server side
	
	// The usage is TWO steps:
	// 1) propulate Texts to your "i18n" store for later usage
	// 2) pull them to use (e.g. in your widgets)

	
	// NOTE: The name of the KEY used here, does NOT need to be the same
	//       like the Key used in the Messages Table on the Server.
	//       You can utilize ANY (textual) KEY - as long as it is UNIQUE
	i18n.loadMessage('txt_dirk_2', c.data.messages.txt_dirk_2);


	// ----------- WORKING EXAMPLE -----------

	// i18n.loadMessage(key, value)
	//   will "load" any available Text into the "i18n" store
	// - key   : the key to later access the Text (I suggest readable names)
	// - value : the Text (with placeholders if desired)
	
	// loads a String (already available in the c.data object from Server side)
	// into the 'key' named "txt_client_2"
	i18n.loadMessage('txt_client_1', c.data.messages.txt_dirk_1);
	i18n.loadMessage('txt_client_2', c.data.messages.txt_dirk_2);
	i18n.loadMessage('txt_client_3', c.data.messages.txt_dirk_3);

	
	// the following line of code will NOT work, that means, that
	// the i18n.loadMessage() function does not go back to the Server and
	// read the value from the database. 
	// This avoids extra Server-Roundtrips and this is EXACTLY what you want
	// i18n.loadMessage('txt_client_2', 'txt_dirk_2');
	
	// i18n.getMessage(key)
	//   will load the existing key from the "i18n" store and return it
	// - key  : the key of a previously loaded text (in the "i18n" store)
	// this previously stored key can be grabbed later like this
	c.data.clienttext_1 = i18n.getMessage('txt_client_1');
	c.data.clienttext_2 = i18n.getMessage('txt_client_2');

	
	// i18n.format(text, parm1. parm2, ...)
	//   if the translated text is not static, and must be filled with variables
	// - text   : ANY text that needs to be filled with parameters
	// - parm1  : first parameter to be replaced in "text", addressed with "{0}"
	// - parm2  : second parameter to be replaced in "text", addressed with "{1}"
	// - ...    : any number of further parameters to be replaced in "text"
	// the i18n.format() function now can fill in Placeholders
	c.data.clienttext_2a = i18n.format(c.data.clienttext_2, c.data.age);

	
	// i18n.getMessage().withValues([parm1, parm2, ...])
	// both functions "i18n.getMessage()" and "i18n.format()" can be used
	// in one function (that's what the docs say, BUT....)
	// NOTE: The n parameters must be passed in as an ARRAY of Strings !!!
	// NOTE: The "withValues()" function only accepts strings, and will fail on
	// numeric values - see examples below
  // c.data.clienttext_3 = i18n.getMessage('txt_client_3').withValues(['parm1', 'parm2']);
  c.data.clienttext_3 = i18n.getMessage('txt_client_3').withValues([c.data.videosseen, c.data.videosleft]);

	$scope.clientActionButton = function() {
		c.data.age++;              // getting older with each click
		
		c.data.videosseen++;
		c.data.videosleft--;
		
		// this previously stored key can be grabbed later like this
		c.data.clienttext_2 = i18n.getMessage('txt_client_2');

		// the i18n.format() function now can fill in Placeholders
		c.data.clienttext_2a = i18n.format(c.data.clienttext_2, c.data.age);

		c.data.clienttext_3 = i18n.getMessage('txt_client_3').withValues([c.data.videosseen, c.data.videosleft]);
	} // end function

	takeSubFunction = function() {
		c.data.check++;
	}
	
	// NOW, the "takeSubFunction" function is available to be used in script!
	takeSubFunction();
}

 

c) Put the following code to the "Server Script" pane

(function() {
  /* populate the 'data' object */
  /* e.g., data.table = $sp.getValue('table'); */

	data.age        = 16;
	data.videosseen = 87;
	data.videosleft = 2012;
	
	data.messages = {
		"txt_dirk_1" : gs.getMessage('txt_dirk_1'),
		"txt_dirk_2" : gs.getMessage('txt_dirk_2'),
		"txt_dirk_3" : gs.getMessage('txt_dirk_3')
	};
	
})();

 

Click on the "Save" button on the top-right edge of the Widget editor. This will save and run the preview of the Widget. It now should look something like shown in the screenshot below:

find_real_file.png

Note: The yellow right-hand pane is the preview of the widget, which is the "resulting output" of the three other code panes.

 

 

Step #6 - Review the code

Of course, we are doing all the coding stuff, to have a resulting Widget. But it all starts at the server. So let me just explain in a short way, what the Widget is supposed to be and expected to do. Then let's walk the way down what happens in detail.

a) Outcome / Process

The Widget will display the message texts, we created in Step #1 (see above). Two of them do have placeholders, that will be filled up with values on the client to have dynamic texts appear on the screen.

- On Server Side, data will be populated to be sent to the Client. This is done using the "data" object variable

- On Client Side, the "data" is available in the "c.data" object (if the "$scope" is injected to the Client Script)
   In the Client Script, "c.data" can be changed as needed for the presentation in the resulting HTML.   

- In HTML Template, "c.data" Object variables can be used in HTML, by referencing in double braces {{}}

This way, the messages and the data flow move from the Server-Side down to the Client (Web Browser)

 

b) Creating the "data" Object on the Server-Side

First, let's review the Server Side code.

find_real_file.png

Line 5 to 7 will just add some numeric variables to the data object (data is already in place for you).

Line 10 to 12 will add three object variables to the "data.messages" object. This way, you can collect all messages in one object variable below the general "data" object.

Note:
As this script runs on the server, all "gs.xyz()" functions (like "gs.getMessage()") will be available (of course depending on which scope your widget is developed in). All three lines are reading the message-texts from the "Messages" table and store the in object variables respectively.

Line 10, for example, creates the object variable "txt_dirk_1" (1) and fills this variable with the message read by the server-side function "gs.getMessage()" (2). This results in having "data.messages.txt_dirk_1" available

find_real_file.png

This call of the "gs.getMessage()" function will read the Message text "I am just static test" form the record marked in the screenshot below. 

find_real_file.png

Having this Server Script code run, the Server sends the data (with the HTML) to the client, where it will be processed to be displayed in the Web Browser.

 

c) Consuming the data on the Client-Side

To access data send from the Server, you NEED TO inject "$scope" to your Client Script. Do not forget that! This is not done automatically for you when creating a new widget.

Just add "$scope" into the parenthesis of the Client Script" - see screenshot below:

find_real_file.png

 

Afterward, you can easily reference the "data" object created on the server from within the Client Script (as "c.data") using object notation. The  screenshot below shows Line 25 of the "Client Script" pane code:

find_real_file.png 

Line 25, in the example above, is found in the Client Script and is used to store the content of the variable "c.data.messages.txt_dirk_2" (the string we loaded on server-side) in the i18n Translation store (see more below).

 

Without any modification, the same value can already be used to show in the HTML of the Widget. This is done using the AngularJS placeholder notation, putting the variable in double curly braces.

The screenshot below shows the "HTML Template" code used to display the values from client variables (found in the "HTML Template" pane of the Widget Editor):

find_real_file.png

This will bring up the text/message inline to the HTML code during run-time.

Note:
Do NOT get mixed up with the "${message_key}" and the "{{angularjs_client_variable}}" notation.

The first one automatically reads a Message from the Server (one-by-one) and replaces the text found in HTML. This is NOT showing any client-side variable. The result is obtained directly from the Server (at cost).

The second one is using client-side variables to be shown by AngularJS. The values of the variables must be available or be created on the client-side. 

So far, this is all just "Standard Widget functionality". If you learned something - YEEH 🙂

  

d) Reviewing the Translation functions of "i18n"

Now coming to what happened in the LCHH... 😉

There is a "library" for supporting localization, that can be used on the Client-Side, called "i18n".

To use this "library, you need to inject this as well to your Client Script, as you can see in the screenshot below:

find_real_file.png

Having this done, there are some functions of this library, I will show you below in detail:

- i18n.loadMessage(key, value)
- i18n.getMessage(key)
- i18n.format(value, parm1, parm2, ...)

and the function "withValues()" to run on the resulting object of "i18n.getMessage()"
- i18n.getMessahe(key).withVales([array of param])

Let's review what these functions are doing in detail below:

 

e) Loading messages / texts into "i18n"

One thing upfront from my point of view: "i18n" does no real magic in terms of localization. At least, I did not find the magic. But hey, if you know it, please let me know and write some comments to this article below.

The first thing to do, is to populate all the Strings needed on Client-Side to the "i18n" library store. This is done using the "loadMessage()" function. The screenshot below shows the snippet of the Client Script, that loads the texts to the "i18n" store (I just call it "store" - I don't know if this is wrong, so shame on me if I am calling it wrong :-/)

find_real_file.png

The first call (in line 25) loads the string found in the variable that previously was populated in the Server Script and then transferred to the Client into the "c.data" object. The string to be loaded to the "i18n" store is the second parameter ("value") of the function call. This even can be a fixed string (literal).

The first parameter of the "loadMessage()" function ("key") is just SOME unique name for the string used in the "i18n" store, to address it later. This MAY be exactly the same name, that you have used in your Messages table as the key (which makes code review more simple), but it DOES NOT NEED to be the same one. In fact, there is NO technical link between the "key" used here, in the "i18n" store, and the key used in the "Messages" table in ServiceNow.

As you can see in lines 37 to 39, I intentionally used different "keys" for the messages loaded, just to show, that there is no need to use the same keys. In fact the three keys "txt_client_1", "txt_client_2", "txt_client_3" are populated with the "c.data" variables "c.data.messages.txt_dirk1", "c.data.messages.txt_dirk2" and  "c.data.messages.txt_dirk3" respectively (which in turn are populated on the server using "gs.getMessage()" - see above.

Note:
The "i18n.loadMessage()" function will not go back to the server automatically and load message texts from the "Messages" table using a given KEY. If you use the function as shown in the screenshot below, it will just create a new "key" named "txt_client_2" in the "i18n" store and fill it with the literal "txt_dirk_2". There will be no lookup for which text may be behind the key "txt_dirk_2" on the Server in the "Messages" table.

find_real_file.png

 

 

f) Using messages / texts from "i18n" store

Now, having the messages/texts loaded to the "i18n" store, they can be consumed easily by using "i18n.getMessage()" on the Client-Side, similar to the Server-Side call "gs.getMessage()".

And I guess this was the main idea when implementing those "i18n" functions, to give a similar "development experience" on Client-Side, like having on Server-Side.

"i18n.getMessage()" returns the message found in the store, so that you can just assign it to any variable in your Client Script.

The screenshot below shows the snippet of the Client Script, which gets back a previously stored message from the "i18n" store, using the "key" you set when loading to the store. 

find_real_file.png

You can see, that the "key" used, has NO relation to any "key" in the "Messages" table on the Server. Just use the "key" you gave the message when inserting the message to the "i18n" store, using the "loadMessage()" function.

Note:
"i18n.getMessage()" will NOT do the job for you to grab the text from the Server and return this. This call only returns messages, that you did load to the "i18n" store before, by using the "loadMessage" function (see above).

By intention, I used a different variable ("c.data.clienttext_1") to get back the message from the store. This way, I do NOT overwrite the original content of the variable received from the Server (which was "c.data.messages.txt_dirk_1").

Anyhow, you COULD (if you like), just use the same variable and overwrite it by using

c.data.messages.txt_dirk_1 = i18n.getMessage('txt_client_1");

As the message was loaded to the "i18n" store, you can always get it back again and again and again by using "i18n.getMessage()".

 

g) Filling in the placeholders of messages

After retrieving the message, some of them may have placeholders, which need to be filled in. This can be done with the "i18n.format()" function.

find_real_file.png

The message "txt_dirk_2" has one placeholder, which is just put "inline" (as {0}) to the "Message" in the "Messages" table in the ServiceNow database (see screenshot below):

find_real_file.png

The "i18n.format()" function just takes any string as the first parameter, which in the screenshot of the Client Script above is the string we retrieved before with the "i18n.getMessage()" function. But this first parameter can also be any other string variable or literal, that may hold placeholders.

Following the first parameter, there can be any number of optional subsequent parameters for the "format()" function call, which will be the values to be substituted in the placeholders of the string (given as the first parameter).

So, the call

c.data.clienttext_2a = i18n.format(c.data.clienttext_2, c.data.age);

will replace the value of the "c.data.age" variable (which is an integer) to the placeholder "{0}" in the string.

The next screenshot shows the result in the Widget Preview on the left side, and the HTML used in the "HTML Template" to display it. So, the string substituted is in the variable "c.data.clienttext_2a":

find_real_file.png

 

h) Filling in the placeholders of messages (alternatively)

Instead of using the both functions "i18n.getMessage()" and "i18n.format()" separately, you can do the "get" and "format" within one call. This is done using the "withValues()" function on the "i18n.getMessage()" function directly.

The "withValues()" function will not accept a series of parameters. Instead, the function just accepts ONE parameter, which is an array of values to be used for the placeholder substitution. 

As an example, the message in the "Messages" table with the key "txt_dirk_3" has TWO placeholders (see screenshot below):

find_real_file.png

Using this message to substitute the placeholders with the "withValues()" function will look something like shown in the code snippet below:

find_real_file.png

Note:
The "withValues()" function is called directly on the "getMessage()" function, using just ONE Parameter, which is an array of substitution values.

 

 

 

Step #7 - Making the Widget act dynamically

The last thing to mention in this article is, to make the widget work dynamically so that you can review the Client Script interacting with the data and the messages loaded before.

The HTML adds one button to the widget, which calls a function in the Client Script, whenever clicked. You can find the HTML for the Button in the "HTML Template" pane, just as shown in the screenshot below:

find_real_file.png

The "ng-click" attribute of the button states "AngularJS" to keep track of the button to be clicked, and call the function "clientActionButton()" found in the Client Script (see screenshot below):

find_real_file.png

Note:
The Client Script to be called by the button must be "prefixed" with "$scope", to define it "reachable" for Angular. If you omit this, the Button Click will not work as expected.

The function just changes some values of variables in the "c.data" object, retrieves the Messages from the "i18n" store again, and fills in placeholders with "format" or "withValues()" functions.

The changes in the values will be reflected in the HTML / in the Widget in the Web Browser immediately. This is done by AngularJS in the background. You do not need to care about any new rendering. Great!

So, clicking the button will also change values and reflect them in the Widget output:

Initial layout/content in the Preview:

find_real_file.png

Layout/content of the Widget after clicking "Refresh" two times:

find_real_file.png

 

 

Bonus #1 - Showing the "c.data" object as json

One nice takeaway that I got from this LCHH was, to show the "c.data" object printed out to the widget itself. This is a very nice and easy but very effective and efficient way to see the contents of the variables used.

This is just done with the single line of code in the "HTML Template" (see screenshot below):

find_real_file.png

In fact, it uses the AngularJS notation (in double curly braces) to show the variable in the HTML, but before inserting it to HTML, it is sent through the "json" pipe (AngularJS stuff) to format it nicely. Therefore, this one line of HTML code will result in the output shown below:

find_real_file.png

Great stuff! Thanks for every time having those small extras in your videos. That's also another reason, why I just can advise everyone, to watch those videos on YouTube.

  

 

Bonus #2 - 😉

Calling local functions within your Client Script can only be done in the CODE, AFTER the declaration of the function - never before. Before in code, the function is UNKNOWN, and trying to call it, will throw the following error in the Development Console of your Web Browser.

find_real_file.png

You can review my Client Script, to see this example. Look at line 10, where the call is throwing the error (when undocumented). Also look at line 90, where the Client Script "knows" the function (after declaration) and properly can call and execute it.

 

Conclusion

I hope, this article was entertaining and helpful, as it was for myself while investigating and testing the things teached in the LCHH video. Building Widgets for Service Portal on ServiceNow is made easy for the Developers. And the learning curve, in fact, is not stat steep as I just thought from the beginning.

But as always... "The devil is in the detail" 🙂
Never tell your boss "it's done in five minutes ...."

Again, I want to send my honor to Andrew, Chuck and Brad for the show.

Keep on going & have fun!

 

BTW: You find the XML for the Widget Attached to this Article, so you do not need to type or copy/paste.....

 

Just let me know what you think about this article.

Thanks a lot in advance for your feedback and comments on this article, you can leave here below!

 

Thanks for marking as helpful and bookmarking - that helps me determine your interest in further articles.

Enjoy ServiceNow & BR

Dirk

---------------------------------------------------------------------

If you like to also review my other articles on the ServiceNow Community, please have a look at the overview here:

Overview of my articles

NOTE: The content I provide here is based on my own experiences and does not necessarily represent my employer's views.

 

Comments
Andrew Barnes -
ServiceNow Employee
ServiceNow Employee

Greetings Dirk! 

 Awesome write-up - any objections to me including it in the notes for the episode?

-Andrew Barnes
Join me at Developer Blog

DirkRedeker
Mega Sage

Hi Andrew.

You are very welcome to include it in the notes. It will be an honor on my side.

Thanks a lot 

BR

Dirk

Version history
Last update:
‎06-13-2020 07:45 AM
Updated by: