Alex Coope - SN
ServiceNow Employee
Update:
Due to multiple requests, I've added checks for the various default strings coming from VA not just NAVA.

Thank you for providing the feedback - keep it coming.

 

 

So, I was in a call with a customer the other day who are going live with NowAssist in Virtual Agent in a bunch of languages and so they had an ask of "how can we translate some of that static strings that aren't coming from the LLM's?".

 

As usual it got me thinking - let's make another LF artifact 🙂

 

Here goes - as usual we need 3 key parts (all made in the global scope). Also, please remember this is a prototype so I can't guarantee it will work for all scenarios or pick up all strings. It will depend on how you've made your various topics, so you may need to adjust the query of the processor script if necessary for your specific use-case.

 

The UI action:
We need to define it on the [sys_cs_context_profile] table

 

AlexCoopeSN_1-1763635827315.png

^ remember to pay attention to the "internal name" of the artifact that is called in the condition of the UI action. We will be calling this one "proto_gen_ai_skill" in this example.

 

The Artifact configuration:

AlexCoopeSN_2-1763635924844.png

 

The processor script (remember to set it for all application scopes):

var LF_gen_ai_skill = Class.create();
LF_gen_ai_skill.prototype = Object.extendsObject(global.LFArtifactProcessorSNC, {
    category: 'localization_framework', // DO NOT REMOVE THIS LINE!

    /**********
     * Extracts the translatable content for the artifact record
     * 
     * @Param params.tableName The table name of the artifact record
     * @Param params.sysId The sys_id of the artifact record 
     * @Param params.language Language into which the artifact has to be translated (Target language)
     * @return LFDocumentContent object
     **********/
    getTranslatableContent: function(params) {
        /**********
         * Use LFDocumentContentBuilder to build the LFDocumentContent object
         * Use the build() to return the LFDocumentContent object
         **********/

        var tableName = params.tableName;
        var sysId = params.sysId;
        var language = params.language;
        var groupName = "";
        var lfDocumentContentBuilder = new global.LFDocumentContentBuilder("v1", language, sysId, tableName);

        // what context profile are we looking at
        var contextProfile = new GlideRecord('sys_cs_context_profile');
        contextProfile.addQuery('sys_id', sysId);
        contextProfile.query();
        if (contextProfile.next()) {
            lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(contextProfile, "ContextProfile", "Name");

            // from the profile we need to find the topics
            var getTopics = new GlideRecord('sys_cs_context_profile_topic');
            getTopics.addQuery('context_profile', contextProfile.sys_id);
            getTopics.query();
            while (getTopics.next()) {
                // we need to process the topics and the ai-skills
                // topics first
                var getCSTopic = new GlideRecord('sys_cs_topic');
                getCSTopic.addQuery('sys_id', getTopics.topic.sys_id);
                getCSTopic.query();
                if (getCSTopic.next()) {
                    lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getCSTopic, "Topic", "Name");
                    // now we need to get it's corresponding AI Skill
                    var getAISkill = new GlideRecord('sys_gen_ai_skill');
                    getAISkill.addQuery('skill_document', getCSTopic.sys_id);
                    getAISkill.query();
                    if (getAISkill.next()) {
                        lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getAISkill, "Skill", "Name");
                    }
                }
            }

            // we need to process a bunch of static entries from NowAssist, these come from a specific lib component
            // first we need to see if NowAssist is enabled
            var NAcheck = GlidePluginManager.isActive("sn_assist_fullwrap");
            if (NAcheck) {
                // now we need to process a specific lib component called "now-assist-full-page-wrapper-app";
                _getLib("311ac228a69e10be24deadc854d0661b"); // this is the sys_id of the "now-assist-full-page-wrapper-app" component used in portals.
            }

            // more default messages
            var getOtDefMsgs = new GlideRecord('sys_script_include');
            getOtDefMsgs.addQuery('sys_id', '4bd02c56eb102110116a378ab55228ef');
            getOtDefMsgs.query();
            if (getOtDefMsgs.next()) {
                lfDocumentContentBuilder.processScript(getOtDefMsgs.script, "Default Messages", "Name");
            }

            // processing message
            var getProcMsgs = new GlideRecord('sys_cs_processing_message');
            getProcMsgs.addEncodedQuery('message_valueISNOTEMPTY');
            getProcMsgs.query();
            while (getProcMsgs.next()) {
                lfDocumentContentBuilder.processString(getProcMsgs.message_value.toString(), "Processing Messages", "Name");
            }

            // now we need to get some default system messages
            var sysDef = new GlideRecord('sys_properties');
            sysDef.addEncodedQuery('sys_package=62d89c2a47350110093b2782846d4338^ORsys_package=18f8146a47350110093b2782846d4344^ORsys_package=bad89c2a47350110093b2782846d436b^ORsys_package=33425e2e47419150f1023524846d430c^ORsys_package=eed89c2a47350110093b2782846d4337^ORsys_package=88f8d06a47350110093b2782846d43d2^ORsys_package=2a033def8c0d48528902a381dc0bd347^ORsys_package=4445ac3866711d6597ee150ac2fe2025^ORsys_package=7cc4ec81533121106b38ddeeff7b12ee^ORsys_package=80663851eb20311054009861eb5228a6^ORsys_package=6ab9d83dc318121029e6554d05013158^ORsys_package=ec3d8fc287213110febf85d80cbb3544^ORsys_package=d3e8d06a47350110093b2782846d4310^ORsys_package=ad7699413b112210f7a77034c3e45a0c^ORsys_package=32808450431302106c3603295bb8f245^type=string^ORnameLIKE.message^ORnameLIKEmessage.^descriptionLIKEdefault header^ORdescriptionLIKEmessage^ORdescriptionLIKEan option^ORdescriptionLIKEconversation^descriptionNOT LIKEmessages^descriptionNOT LIKEcomma separated list^descriptionNOT LIKEdetermines if^descriptionNOT LIKEused to set^descriptionNOT LIKEthis list will^descriptionNOT LIKEcomma-separated list^descriptionNOT LIKElanguage code^descriptionNOT LIKEcontrols whether^descriptionNOT LIKEif true^descriptionNOT LIKEis listed here^descriptionNOT LIKEis true^descriptionNOT LIKEqueue length^descriptionNOT LIKElimit in minutes^descriptionNOT LIKEcomma-delimited list');
            sysDef.query();
            while (sysDef.next()) {
                if (sysDef.value != '') {
                    // it's best to be defensive here
                    lfDocumentContentBuilder.processString(sysDef.value.toString(), "System Messages", "Name");
                }
            }

            // CS profile messages
            var getMsgs = new GlideRecord('sys_cs_context_profile_message');
            getMsgs.addQuery('context_profile', contextProfile.sys_id);
            getMsgs.addEncodedQuery('context_profile.active=true^context_profile.model_type=llm'); // to narrow down the results
            getMsgs.query();
            while (getMsgs.next()) {
                lfDocumentContentBuilder.processString(getMsgs.message.toString(), "Context Message", getMsgs.context_profile.getDisplayValue()); // it needs to be both MSG
                lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getMsgs, "Context Message", getMsgs.context_profile.getDisplayValue()); // whilst the message field is a TRT field, we might need the translation to also be a TRT
            }
        }

        function _getLib(getSys) {
            try {
                var libs = [];
                var libMsgStr = [];
                if (!libs.toString().includes(getSys.toString())) {
                    // we need the GR so we can parse it
                    var getRootElC = new GlideRecord('sys_ux_lib_component');
                    getRootElC.addQuery('sys_id', getSys.toString());
                    getRootElC.query();
                    if (getRootElC.next()) {
                        if (getRootElC.required_translation_keys.toString().includes('message')) {
                            var rootC = getRootElC;
                            rootC = JSON.parse(rootC.required_translation_keys.toString(), function(LibKey, libVal) {
                                if (LibKey == 'message' && (libVal != '' && libVal != ' ')) {
                                    if (!libMsgStr.toString().includes(libVal.toString())) {
                                        // because of the sheer quantity of core macroponents, this is to reduce unnecessary repeated calls to the same key
                                        lfDocumentContentBuilder.processString(libVal.toString(), 'Default Messages', getRootElC.tag.toString());
                                        libMsgStr.push(libVal.toString());
                                    }
                                }
                            });
                            rootC = ''; // we need to reset the JSON object
                        } else {
                            if (getRootElC.required_translation_keys.toString() != '[]' && getRootElC.required_translation_keys != '') {
                                // there are some records that do not contain a JSON object
                                var reqKeys = getRootElC.required_translation_keys.toString().split("\n");
                                for (var iR = 0; iR < reqKeys.length; iR++) {
                                    lfDocumentContentBuilder.processString(reqKeys[iR].toString(), "UX Lib Component", getRootElC.tag.toString());
                                }
                                reqKeys = ''; // reset the array
                            }
                        }
                    }
                    libs.push(getSys);
                }
            } catch (err) {
                //gs.log("Error in _getLib - " + err.name + " - " + err.message);
            }
        }

        return lfDocumentContentBuilder.build();
    },



    /**********
     * Uncomment the saveTranslatedContent function to override the default behavior of saving translations
     * 
     * @Param documentContent LFDocumentContent object
     * @return
     **********/
    /**********
        saveTranslatedContent: function(documentContent) {},
    **********/

    type: 'LF_gen_ai_skill'
});

 

The logic of this query is to follow the assistant you've made, then to find all the messages associated to that assistant, then to find all the default messages which are mostly coming from some system properties. If you are finding some strings are still not translated, you may also need to run the specific "topic" artifact to translate the corresponding VA topics you've made.

 

When you have it set up and working, you should see an output like this in the LFTask:

AlexCoopeSN_3-1763636207741.png

 

NOTE: if you want to use this with the Localization Workspace, as usual you will need to define some RCA's and create some Cross Scope privileges for the "execute API" call (to the Script include) and "read" action to the table [sys_cs_context_profile].

 

As usual, because this is a prototype, no support or warranties are offered on this artifact, this is just being shared for the benefit of the community to help you progress faster.

 

So, again we've seen that we can keep expanding the translation capability with more artifacts to help simplify how we translate more areas of the platform.

 

 

 

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

 

 

Comments
Daniel Madsen
Mega Sage

Thank you very much for the guide, Alex! This has saved us many hours, and made it possible for us to move forward with testing NowAssist in Danish. I saved the translation task in XLIFF format and used AI to translate everything in less than 5 minutes.

 

DanielMadsen_0-1766433939702.png

Alex Coope - SN
ServiceNow Employee

Ah that's great to hear @Daniel Madsen, good luck with the go-live 🙂

Version history
Last update:
11 hours ago
Updated by:
Contributors