Get a first look at what's coming. The Developer Passport Australia Release Preview kicks off March 12. Dive in! 

Alex Coope - SN
ServiceNow Employee
This is a brand-new prototype artifact, on the medium to hard level of complexity.

 

When I first started dabbling with translations in the platform (over 12 years ago now) I actually thought that field labels and choices were the easiest of the areas to translate. Mainly because back in those days, it was fairly simple to make an extract of [sys_documentation] and [sys_choice] based on the table I was looking to translate - then there's a dark art of "field annotations" (which I still have nightmares of to this day).

 

Then we launched the Localization Framework in the Quebec release. However, it had a bit of a gap, in that when it first launched, there weren't usable API's for field labels and choices. 

 

Today, that changes! I'm here to tell you, that as of Yokohama Patch 11Zurich Patch 7 and the upcoming Australia releases, it now does! 🤗.

 

I know many of you have waited a long time for this day (just like I have).

 

 

That's all well and good....

I hear you. It's great that there's now 2 new API's, but the question is how do we use them?

 

Well, I've done the hard part for you - you should know me by now 🙂.

 

So, without any further delay, let's break down a brand new POC artifact I've put together on how to translate a Form - this will work both in CoreUI (aka UI11-UI16) and a Workspace form (because the underlying architecture is actually the same).

 

 

The UI Action

As usual with LF artifact's, lets define the UI action:

UI Action.png

 

For those who've followed my other artifact POC's, the "internal" name I'm using will be "lf_forms", and the table we're defining the artifact on will be the [sys_ui_form] table.

 

 

 

The Artifact record

artifact.png

 

 

The Script

I'm not going to delay this part any longer:

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

    /**********
     * Extracts the translatable content for the artifact record
     * 
     *  params.tableName The table name of the artifact record
     *  params.sysId The sys_id of the artifact record 
     *  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);
        var fieldList = [];

        try {
            // we need to loop through and get all of the field labels for each of the forms listed
            var wForm = new GlideRecord('sys_ui_form');
            wForm.addQuery('sys_id', sysId);
            wForm.query();

            if (wForm.next()) {
                // this makes our query a bit more efficient, as we don't want to care what view, as any view needs translating.

                var tableQ = new GlideRecord(wForm.name.toString());
                tableQ.setLimit(1);
                tableQ.query();

                if (tableQ.next()) {
                    var gFields = new GlideRecordUtil();
                    fieldList = gFields.getFields(tableQ); // we don't even to query the table to get fields, we can just pull all the fields on the table as it's more efficient


                    // we now need to loop through the fieldList and check if they are for this table
                    for (var i = 0; i < fieldList.length; i++) {
                        var getField = new GlideRecord('sys_documentation');
                        getField.addEncodedQuery('name=' + wForm.name.toString() + '^ORname=' + tableQ[fieldList[i].toString()].getBaseTableName());
                        getField.addEncodedQuery('element!=NULL');
                        getField.addQuery('element', fieldList[i].toString());
                        getField.addQuery('language', gs.getProperty('glide.sys.language'));
                        getField.orderBy('label');
                        getField.query();
                        while (getField.next()) {
                            lfDocumentContentBuilder.processTranslatableFieldsForSysDocumentation(getField, "Field Label - " + getField.name.getDisplayValue() + " - " + getField.getDisplayValue(), "name");
                            lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getField, 'Fields - ' + wForm.name.toString(), getField.getDisplayValue());

                            // now we need to find the right choice fields
                            var getChoiceField = new GlideRecord('sys_choice');
                            getChoiceField.addEncodedQuery('name=' + getField.name);
                            getChoiceField.addQuery('element', getField.element);
                            getChoiceField.addQuery('language', gs.getProperty('glide.sys.language'));
                            getChoiceField.orderBy('sequence');
                            getChoiceField.query();
                            while (getChoiceField.next()) {
                                lfDocumentContentBuilder.processTranslatableFieldsForSysChoice(getChoiceField, "Choices - " + getField.name.getDisplayValue() + " - " + getField.getDisplayValue(), "name");
                            }
                        }
                    }

                    // now we need to get the form sections
                    var formSec = new GlideRecord('sys_ui_form_section');
                    formSec.addEncodedQuery('sys_ui_form.name=' + wForm.name.toString());
                    formSec.orderBy('position');
                    formSec.query();
                    while (formSec.next()) {
                        // we're going to process any and all for this table, because it makes the most sense to not have to worry about each individual "view"

                        // now we need to follow to the actual form section
                        var formSecCheck = new GlideRecord('sys_ui_section');
                        formSecCheck.addQuery('sys_id', formSec.sys_ui_section.sys_id);
                        formSecCheck.query();
                        if (formSecCheck.next()) {
                            lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(formSecCheck, "Form Section - " + wForm.name.toString(), "name");

                            // We could process fields here, by looping through what's on each form, but this would be inefficient (compared to what we do above) and risks missing a potential field on the table that doesn't happen to yet be on any form view.
                            // Field labels are actually covered by our loop to [sys_documentation]

                            // we do however need to find each of the annotations to process those
                            var getAnnoEl = new GlideRecord('sys_ui_element');
                            getAnnoEl.addEncodedQuery('typeINannotation^sys_ui_section=' + formSecCheck.sys_id);
                            getAnnoEl.query();
                            while (getAnnoEl.next()) {
                                // now we need to take the "element" value, and query the [sys_ui_annotation] table
                                var getAnno = new GlideRecord('sys_ui_annotation');
                                getAnno.addEncodedQuery('sys_id=' + getAnnoEl.element.toString());
                                getAnno.query();
                                // there will only be one per
                                if (getAnno.next()) {
                                    if (getAnno.text.toString().includes('getMessage')) {
                                        lfDocumentContentBuilder.processScript(getAnno.text, "Annotation - " + wForm.name.toString(), formSecCheck.caption.toString()); // there are some use-cases the call getMessage
                                    } else {
                                        lfDocumentContentBuilder.processString(getAnno.text, "Annotation - " + wForm.name.toString(), formSecCheck.caption.toString()); // there are some use-cases that don't need to call getMessage
                                    }
                                }
                            }
                        }						
                    }

                    // now we need to look for UIactions / Buttons
                    var getActsBut = new GlideRecord('sys_ui_action');
                    getActsBut.addQuery('active=true^table=' + wForm.name.toString());
                    getActsBut.query();
                    while (getActsBut.next()) {
                        // process the actions
                        lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getActsBut, "UI Action", "Name");
                        lfDocumentContentBuilder.processScript(getActsBut.script, "UI Action", "Script");

                        // check if it's a Workspace button
                        if (getActsBut.form_button_v2 == 'true') {
                            lfDocumentContentBuilder.processScript(getActsBut.client_script_v2, "UI Action", "Workspace Button Script");
                        }
                    }

                    // now we need to look for any Process Flow Formatters
                    var getProcessFlow = new GlideRecord('sys_process_flow');
                    getProcessFlow.addEncodedQuery("active=true^table=" + wForm.name.toString());
                    getProcessFlow.orderBy('order');
                    getProcessFlow.query();
                    while (getProcessFlow.next()) {
                        lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getProcessFlow, "Process Flow Stages", "Name");
                    }


                    // now we need to look for any "steppers" - in Workspaces.
                    var getSteps = new GlideRecord('sttrm_state');
                    getSteps.addEncodedQuery('initial_state=true^sttrm_model.table_name=' + wForm.name.toString());
                    getSteps.query();
                    while (getSteps.next()) {
                        lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getSteps, "Process Steps", "State Label");

                        // we also need to get the Model
                        var getStepModel = new GlideRecord('sttrm_model');
                        getStepModel.addQuery("model", getSteps.sttrm_model.sys_id);
                        getStepModel.query();
                        if (getStepModel.next()) {
                            lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getStepModel, "Process Steps", "Model");
                        }
                    }


                    // now we need to check for Client-scripts
                    var getCS = new GlideRecord('sys_script_client');
                    getCS.addEncodedQuery('table=' + wForm.name.toString() + '^ORtableIN' + wForm.name.toString() + ',task^active=true');
                    getCS.query();
                    while (getCS.next()) {
                        lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getCS, "Client Script", getCS.name.toString());
                        lfDocumentContentBuilder.processScript(getCS.script, "Client Script", getCS.name.toString());
                    }


                    // now we need to get any BR's on this table, just in-case they present a message to the fulfiller.
                    var getBR = new GlideRecord('sys_script');
                    getBR.addEncodedQuery('collection=' + wForm.name.toString() + '^active=true');
                    getBR.query();
                    while (getBR.next()) {
                        lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getBR, "Business Rule", getBR.name.toString()); // this should cover if there's a static message populated in the 'message' field when "add_message" is true
                        lfDocumentContentBuilder.processScript(getBR.script, "Business Rule", getBR.name.toString());
                    }


                    // now we need to check for UI policies
                    var getPol = new GlideRecord('sys_ui_policy');
                    getPol.addEncodedQuery('table=' + wForm.name.toString() + 'active=true^run_scripts=true');
                    getPol.query();
                    while (getPol.next()) {
                        lfDocumentContentBuilder.processScript(getPol.script_true, "UI Policy - True ", getPol.short_description.toString());
                        lfDocumentContentBuilder.processScript(getPol.script_false, "UI Policy - False", getPol.short_description.toString());
                    }
                }
            }

            return lfDocumentContentBuilder.build();

        } catch (err) {
            gs.log('Error in LF_forms - ' + err.name + ' - ' + err.message);
        }
    },

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

    type: 'LF_Forms'
});

 

There are a few very important notes about this artifact:

  • It has to be defined in the "global" scope
  • It's intended to not only find Field Labels but also Choices in choice fields (assuming they are choice fields) - if it misses any, it's likely because it's a "string" field with choices - I'll do an update for these in the near future.
  • It's intended to identify Form sections
  • It's intended to identify Annotations
  • It's intended to identify UI actions
    • It's intended to find any getMessage calls in their scripts also
  • It's intended to identify Process Flow stages
  • It's intended to identify Process Flow Steppers' steps
  • It's intended to identify getMessage calls in Client Scripts
  • It's intended to identify getMessage calls in Business Rules
  • It's intended to identify getMessage calls in UI Policies (true and false) scripts

All of the above is form View agnostic. In that, it's deliberate to not focus on a singular Form View to reduce the chance of missing something or increasing the workload unnecessarily. You can of course change that, but I wouldn't recommend it. 

 

 

What does it look like?

Here's an image of what a field label to be translated would look like in the Comparison UI:

fields.png

 

Here's an image of what a bunch of "choices" will look like in the Comparison UI:

choices.png

 

Here's what everything else (after field labels) will look like in the Comparison UI:

everything else.png

 

To prove that it works, this is a Canadian First Nation language called "Inuktitut" (I apologize in advance to any native speakers if the quality is not good, I'm using my typical MS DT flow here):

form translated.png

^ this is the [change_request] form on my ZP7 instance. The very same instance I use for Demo's, workshops, customer meetings, events etc. If you've seen any of my webinars etc, it's that same instance.

 

Everything you see above was translated using this Artifact and only this Artifact.

 

If using the Localization Workspace with this artifact, don't forget to add the Cross Scope privileges ("read" 
for the table, and "execute" for the API.

Also - in LW, it might take a few minutes in Stage 3 (scope) to generate the list of what to select. This is to
be expected, it's because the [sys_ui_form] table has a few thousand entries (which is a better alternative
to [sys_db_object]. Once you deselect what you don't want (table wise), you can then search for what you
do want and you only need one entry for that table - you will see multiple because of form views, as that's
what that table is holding.

 

 

This is a Prototype

As with the other extended artifacts that my team put together, this is a prototype. I can't promise it's going to be perfect in this first iteration so please try it, let me know (in this thread) if there's an area that doesn't quite work as you expect (e.g. it misses something), or if you'd like to have something added.

 

For example:

  • I purposefully haven't included "Related Lists". This is for a few reasons, mainly because it could lead to an endless loop - my suggestion would be to run a task per form of the Related Lists you want to cover.
  • I purposefully haven't included this in the Workspace artifact because it will lead to significant performance degradation when running such a large query at once. Internally we're going to look at how to break-up such scenarios, maybe trigger cascading artifacts based on when certain calls are met / triggered. But that's something for the future. Right now, if you want to fully translate a Workspace, you would need to run the Workspace artfact on that Workspace, then this Forms artifact per Form in that Workspace (and it's corresponding Related Lists that lead for a form view.). 
  • This purposefully is View agnostic. It just looks at what's on the table and where-necessary it will try to also find base table fields to take into account inheritance behaviour.
As with the other POC artifacts - I have to say that there is no support or warranties provided.

 

 

Summary

So, we are now able to cover all 5 of the translation tables with the Localization Framework and Localization Workspace, which means, the library of Artifacts is now able to grow even more.

 

And yes, I'm already sketching out another one, but let me know what you would like to see next and we'll see what we can do.

 

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

 

 

 

 

Version history
Last update:
an hour ago
Updated by:
Contributors