Alex Coope - SN
ServiceNow Employee
ServiceNow Employee

Last week I had a super interesting call with an international customer, where they needed to be able to allow their agents the ability to read submitted variables on their requests to aid fulfilment. Which makes perfect sense, because we have the ability ootb for fields (such as string and html type fields). However, we don't (yet) have the ability on variables. Let's change that right now! Grab your favourite beverage of choice (I have my coffee in hand) because we're going to delve into the world of GlideAJAX, API's, lesser known methods and let's do this!

 

 

The ideal

Imagine, you have a RITM, which has a few variables and they are in a language you can't read. This is a genuine issue in some teams. Now, imagine if we could show that user the text in their language like this:

AlexCoopeSN_1-1709627501468.png

Above you can see that we can apply Dyanmic Translation specifically to the "string" type (free text) variables on the form, however there's a bit of a but. It's not ootb so we need to think how we do it.

 

To do this, there are a few pre-reqs:

  1. You need to have Dynamic Translation installed and set (because we need it's API's)
  2. You need to have the Localization Framework installed because we need one of it's Script Includes (SI's).

 

 

The Design

As you can see in the screenshot, it's clearly taken from the "CoreUI" / "UI16", this is because for this specific use-case for this Customer, this is the UI I was focussing on first with this solution. We will look to try and make it work for Workspaces as well in the future, unless it becomes part of the ootb offering (yes there are a few IDEAs already logged on this topic) but as that day is not today we have this.

 

So, what-ever we were to do, we have to ensure we are not causing any customizations to anything ootb (just incase it does become part of the ootb offering), and we also need to ensure we're not doing any DOM manipulation. This therefore meant that I couldn't go hacking up the "Variable Editor" formatter and stretch my knowledge of "Jelly" that little bit more. Instead we need to think how we can "trigger" the behaviour we want coupled with how we "show" the output we want.

 

I started to think about a flag field and potentially a Display Business Rule potentially sending a big scratchpad, but then I thought about something even simpler. I could use an onLoad Client Script to read the values I want because this allows me access to "g_form" and "g_sc_form" to give me access to decorations and super importantly "showFieldMsg", this also allows me the ability to keep the heavy lifting part on the server by leverage a GlideAJAX call.

Important note > You do not need to use a Client Script for the client side, you could just as easily convert it into a UI action so 
it's only callable to whom you want and when you want. I'm using a Client Script in this example just to show the concept. And this
is important because in this design it will trigger DT upon each form load.

 

 

The Solution

Amazingly, all we need is 1 Client-Script and 1 Script-Include to do this. I'll break down each one and what it does;

The Client-Script

We need to declare a Client-Script on the [sc_req_item] table (this is the task type table that holds RITM's and typically has variables.

Side note - the variable's values for RITM's are stored in the [sc_item_option_new] table, for everything else (e.g. if you wanted 
this behaviour for a Record Producer for HR cases, or Incidents) you would need to change the logic in the Script Include to point
to the [question_answer] table. In principle it works exactly the same but double check your query to this table to make sure you're
point to the right fields.

AlexCoopeSN_2-1709628347490.png

Make sure that your client-script is in the "global" scope.

 

Code:

 

 

function onLoad() {
    try {
        // decorate the appropriate variables
        var getVars = g_sc_form.elements;

        var varJSON = JSON.stringify(getVars);
        // now we need to find the variables we care about
        var varName = '';
        var getVarStr = JSON.parse(varJSON, function(key, value) {

            if (value.toString().includes('ni.')) {
                varName = value.toString();
            }
            if (key == 'type' && value == 'string' && varName != '') {
                // we need to parse the field label of the variable
                g_sc_form.addDecoration(varName, "icon-global");
            }
        });

        // now we need to get the text for translation
        var theVars = g_sc_form.getEditableFields();
        var theVarsArr = theVars.toString().split(',');
        for (var v1 = 0; v1 < theVarsArr.length; v1++) {
            var varVal = theVarsArr[v1].toString();
            var varValue = g_sc_form.getValue(varVal);
            if (varValue.toString() != 'true' && varValue.toString() != 'false') {
                // now we need to call a custom GlideAJAX to fetch the translations
                var callTrans = new GlideAjax('poc_var_trans');
                callTrans.addParam('sysparm_name', 'getTranslation');
                callTrans.addParam('sysparm_string', varValue.toString());
                callTrans.addParam('sysparm_variable', theVarsArr[v1].toString());
                callTrans.addParam('sysparm_task', g_form.getUniqueValue().toString());
                callTrans.getXML(translationParse);
            }
        }
    } catch (err) {
        alert('Error in poc_DT_variables_onLoad - ' + err + ' - ' + err.message);
    }
}

function translationParse(response) {
    try {
		var output = '';
        var valResponse = response.responseXML.getElementsByTagName("result");
        for (var i = 0; i < valResponse.length; i++) {
            var name = valResponse[i].getAttribute("variable");
            var value = valResponse[i].getAttribute("value");
            //output += name + " = " + value + "\n ";
			if(name != 'null' || name != null){
				g_sc_form.showFieldMsg(name, value);
			}
        }
    } catch (err) {
        alert('Error in translationParse - ' + err.name + ' - ' + err.message);
    }
}

 

 

It's not very widely known, but whilst everyone knows about "g_form", there's another call you can use called "g_sc_form" (Glide Service-Catalog Form), which has it's own methods. In this case, we're going to be leveraging "getEditableFields()" which you'll find give us only the variable ID's which is exactly what we need because we can then send those ID's much like field names into "getValue()". We can even use those same values with "showFieldMsg()" which are treated exactly the same as any other field.

 

 

The Script-Include

With the Script-Include, we need to make sure it's available to all scopes and that it's "Client Callable" because we're calling it from a GlideAJAX call in our client-script:

AlexCoopeSN_3-1709628613664.png

 

Code:

 

 

var poc_var_trans = Class.create();
poc_var_trans.prototype = Object.extendsObject(AbstractAjaxProcessor, {

    getTranslation: function() {
        try {
			var result = this.newItem("result");
            var theVar = this.getParameter('sysparm_variable'); // variable ID
            var string = this.getParameter('sysparm_string'); // the string to translate
            var taskSys = this.getParameter('sysparm_task'); // task record for gr'ing to get the text
            string = string.toString();
            // we need to include a few other scripts
            gs.include('LFTranslateUtils'); // this is mandatory

            // we need to know which translator is used for DT
            var usedTranslator = '';
            var getTranslator = new GlideRecord('sn_dt_translator_configuration');
            getTranslator.addEncodedQuery('default_provider=true');
            getTranslator.query();
            if (getTranslator.next()) {
                usedTranslator = getTranslator.name.toString();
            }

            var getString = new GlideRecord('sc_item_option_mtom');
            getString.addEncodedQuery('request_item.sys_id=' + taskSys + '^sc_item_option.value=' + string);
            getString.query();
            if (getString.next()) {
                var detectedResponse = sn_dt_api.DynamicTranslation.getDetectedLanguage(getString.sc_item_option.value.toString());
                var respStr = JSON.stringify(detectedResponse);
                var respE = new JSON().decode(respStr);
                var strLang = respE.detectedLanguage.name.toString();
                if (strLang != gs.getSession().getLanguage()) {
                    var getTranslation = JSON.stringify(sn_dt_api.DynamicTranslation.getTranslation(getString.sc_item_option.value.toString()), {
                        "targetLanguages": gs.getSession().getLanguage(),
                        "additionalParameters": [{
                            "parameterName": "texttype",
                            "parameterValue": "plain"
                        }],
                        "translator": usedTranslator
                    });
                    var gTransObj = JSON.parse(getTranslation, function(key, value) {
                        if (key == 'translatedText') {
							result.setAttribute("value", value.toString()+ gs.getMessage(" - Translated by ")+usedTranslator.toString());
							result.setAttribute("variable", theVar.toString());
                            return result;
                        }
                    });
                }
            }
        } catch (err) {
            gs.log('Error in poc_var_trans.getTranslation - ' + err.name + ' - ' + err.message);
        }
    },
    type: 'poc_var_trans'
});

 

 

So in our query, we need to know what the string we want to translate is, the variable ID so when we send it back we can super easily mapping the string to the variable in our response function, and the task's sys_id so that we can easily query the variable table. We also need to ensure that we check that Dynamic Translation is configured so we know which MT to use, and we also need to ensure we are translating to the user's language. This therefore means that our query is entirely dynamic, because we want to avoid any hard-coded elements to ensure the most amount of users can benefit from it.

 

 

Summary

What have we learned? Well, If we think a little bit out of the box, we can absolutely (and fairly easily) make a solution that can scale (for multiple scenarios) leverage various building blocks provided to use. In this case we leveraged Client-Scripts, Script-Includes, Decorators, DynamicTranslation, LocalizationFramework, multiple API's, and multiple Methods in such a way that if this becomes a feature ootb in the future we can easily deactivate these two scripts without impacting possible future capabilities as we're not changing anything ootb.

 

 

If you found this helpful, please like, share and subscribe as it always helps.

 

 

 

 

Comments
surajwipro
Tera Contributor

Hi @Alex Coope - SN : I tried this in my PDI but not working.

I have enabled plugins and used your SI & CS

Alex Coope - SN
ServiceNow Employee
ServiceNow Employee

@surajwipro, you will need a working subscription to an CloudMT service like Microsoft / Google / DeepL etc,

Many thanks,
kind regards

surajwipro
Tera Contributor

Hi @Alex,

Thanks much for your response,

I have Microsoft translator, pfb ss.

surajwipro_0-1726582306016.png

 

Alex Coope - SN
ServiceNow Employee
ServiceNow Employee

@surajwipro, do you have a working and active Microsoft Translation Azure subscription? Having the spoke or having it turned on I'm afraid isn't enough, you will need an actual MS subscription to be able to use their translation service,


To avoid confusion for others, as troubleshooting Dynamic Translation is out of scope of this thread, please review all the documentation on what is required for Dynamic Translation, ensure you have an active MS subscription and then if it's still not working please open a new thread,

Many thanks,
kind regards

shivani_verma
Tera Contributor

Hi @Alex Coope - SN 

Thank you for sharing this wonderful post! Have you implemented a similar requirement for Notifications as well? Where we can see the attached variables in translated language, currently OOB we can achieve static translation of notification, but we want to dynamically translate our notifications along with its variables. Please let me know if you have implemented anything similar for notifications and if you have posted that somewhere on community.

 

Thanks again in advance!

shivani_verma
Tera Contributor

Hi  @Alex Coope - SN 

Thank you for sharing this wonderful post! Have you implemented a similar requirement for Notifications as well? Where we can see the attached variables in translated language, currently OOB we can achieve static translation of notification, but we want to dynamically translate our notifications along with its variables. Please let me know if you have implemented anything similar for notifications and if you have posted that somewhere on community.

 

Thanks again in advance!

Alex Coope - SN
ServiceNow Employee
ServiceNow Employee

Hi @shivani_verma,

There is an ootb feature for that, information available here. However, if you mean when the email is shown in the activity stream - that is something coming in a future release but I can't yet confirm when yet,

Many thanks,

kind regards

 

shivani_verma
Tera Contributor

Hi @Alex Coope - SN 

Yes, I am talking about the emails which we receive in email logs and test it through "Preview email", so the link which you have attached in your response, we are already checking that field "enable dynamic translation" as true, but the variables in our notification like ${short_description} is not getting translated in other language (e.g. french here), Rest of the email body is getting translated. FYI we have also created a "Translated notification" in french version since without this in email log, we didn't get "Translated email contents". PFA the screenshot of both records. But somehow we are only getting translated hardcoded body and not the variables (short_description)Preview email on French User.pngTranslated Email Content.pngTranslated notification.png
We have to do this for all the notification existing in the system, so gs.getMessage won't work for our case.
Please give your thoughts if you have worked on anything similar like this.

Alex Coope - SN
ServiceNow Employee
ServiceNow Employee

@shivani_verma,

Ok, this is now out of the scope of this particular thread, but what you're looking for is in this blog post, 

If you have any further questions (for the purpose of topic consistency) either open a new thread or ask them in that one if still related (just because this thread isn't about emails),

Many thanks,
Kind regards

Tobias Vogt
Tera Explorer

Hi @Alex Coope - SN thanks for this article as well as for your other articles around localization, they are a tremendous help!

Just as you said with just a few modifications this idea works also as an UI action and even in the Service Operations Workspace 🙂

 

One question: As you are a ServiceNow employee, when will your great ideas find their way into the Dynamic translation/Localization Framework product(s)? Or are there some ideas in the idea portal that need to be upvoted for that to happen?

Alex Coope - SN
ServiceNow Employee
ServiceNow Employee

Hi @Tobias Vogt,

I am indeed an employee, I head up our Customer and Partner enablement aspect to do with anything non-English across the entire platform, it's various product lines, UI's and use-cases. The team covers from Japan, Europe and the US. But thank you for the kind words, it's greatly appreciated.

Good to know it works in SOW, I assume that's in a modal pop-up because I didn't yet think the annotations in the "Variables" form section worked yet?

In terms of this being an IDEA, there was one, but it might have expired now. If you can find it, please feel free to re-open it or create a new one because it's always worth while as it helps us prioritise which features are the most meaningful to include before others, if that makes sense?

Many thanks,
Kind regards

Tobias Vogt
Tera Explorer

Hi @Alex Coope - SN ,

actually the variable field messages work just fine in the SOW (variables section of a RITM after pressing the translation UI action):

TobiasVogt_0-1745389331022.png

There are just two things which unfortunately do not work as in the core UI:

  1. The "g_sc_form" object seems to be unknown in the workspace UI action script, so "g_form" has to be used instead
  2. The nice little globe icon decorator you used in your code to highlight translated variables does not appear to do anything as the "addDecoration" function itself apparently is not supported in SOW

And thanks for the information with the idea, I will search an upvote it if there is something!

Version history
Last update:
‎03-05-2024 12:58 AM
Updated by:
Contributors