The Zurich release has arrived! Interested in new features and functionalities? Click here for more

Alex Coope - SN
ServiceNow Employee
ServiceNow Employee

Now that the Tokyo release is out and people have started to play around with UIB for a few releases and started to learn it's intricacies, it's time to start going through how translations with this new UI concept (we internally called "Project Polaris") works. There's quite a bit to get through with this post, so make sure you have your favourite beverage and snacks to hand (because I'm going to need a lot of coffee myself as I write this) and let's go.

 

This post has been updated to take into account new features that have been added in some of the latest Workspaces, including
"landing pages", "lists", "related lists", and now "PAR Dashboards".

Since the first version of this prototype, it has now received some significant updates. Please keep the feedback and asks coming!

 

 

Configurable vs non-Configurable workspaces, and where does "Next Experience" fit in?

First we need to understand what is a "Workspace" and what is "Next Experiences".

 

 

 

Way back in the Madrid release we introduced a new UI concept called "Workspaces". The idea with a workspace was to have a User Experience (UX) concept targeted to a very specific persona. Initially this was for "Agents" in the HR world, where they didn't need the UI bloat of unnecessary menus, applications, forms etc. Their job is hard enough as it is, so with a more streamlined UX specifically focused to their needs workspace was born.

 

This laid the ground works for how we could streamline many other aspects in the UI (should we need to). As more and more Customers started to use this new "Agent Workspace", there was more and more asks for it to be editable / configurable. In the super early versions, the form was editable via a hard-coded form view called "workspace". So, if you cast your minds back to the Quebec release, we introduced "Configurable Workspaces" which was the next step in this evolution.

 

The first fully configurable Workspace was the "CSM/FSM Configurable Workspace", which I'm sure many of you have had a look at in UIB (UI Builder) to see how it works. If you're super interested in the topic of UIB, there is also an installable demo plugin pack called "UXR Demos" that you try on your PDI's to see examples of Portal Experiences, Landing pages etc, on-top of an awesome course on NowLearning.

 

So the short of it, is Platform developers now have another way of building more advanced and more focussed User Experiences in the platform should they want to. Either something similar to a Workspace for fulfillers, or even a Portal experience. There are themes, pre-canned layouts, all sorts, with the idea of being more empowering for your needs.

 

Does this mean that UI16 and Service Portals are going away? No, they're not going anywhere (at least not that I know of ðŸ™‚ ), but it does mean because you can build with UIB (just like we can) we need to start thinking about how the translation aspects work.

 

 

Are translations different?

If we remember out training, we know that UI translations use the 5 tables:

  • [sys_documentation] - for field labels
  • [sys_choice] - for drop down choices
  • [sys_ui_message] - for scripted messages
  • [sys_translated] - for translated field types
  • [sys_translated_text] - for translated_text and translated_html field types 

This logic is not going to change with UIB based "experiences" except where it does. In that, we are still going to leverage the 5 tables (this will not change, because they work very well), what does change is how we use one particular table (sys_ui_message). 

 

We're still going to call a "key",  and the records are still going to be populated in the exact same way, but how we interact with the table is a little different.

 

Below is a screenshot I've taken from my demo Tokyo P2 instance in UIB of the aforementioned "CSM/FSM Configurable Workspace":

AlexCoopeSN_0-1667912977756.png

 

For the header we are working on ("Important Items"), you can see on the righthand side "translatable turned on" is enabled. 

What this does is a bit of logic when you save your experience / update this page, and adds that string to a field (for that level in the table hierarchy - we'll go more into that in a bit) called "Required Translations". These values are then used as the calls to keys in the [sys_ui_message] table, and if we know what these are, it means we can do some super clever things. If you're a keen reader of my blog, you might have guessed where I'm going to go with this...

 

So when this feature is present, this is how the call gets stored:

AlexCoopeSN_1-1667913366116.png

 

Side note - for those who end-up wanting to make their own "components" in the future, on the Developer portal, look up "Translation Literal" as you'll need to learn this to make your components work.

 

Just to sanity check, lets have a quick look at the records in the [sys_ui_message] table:

AlexCoopeSN_2-1667913494026.png

As we can see, still the same as we know.

 

 

How can we find these strings quickly?

 A few weeks ago, I sat down with my team in our London office, and we started mapping out the table hierarchy of how UIB holds it's records. One filled board and a few empty pens later and we had the initial mappings:

AlexCoopeSN_3-1667913716794.jpeg

Why did we do this you might ask? Well, the reason we did this is so that we could prototype another Localization Framework artifact for you. Yep, you read that right, we've PoC'd another artifact that I'm going to share with you just like we did with the Service Portal one.

 

This time we called it "LF_workspace" and defined it at the top of the UIB hierarchy of the [sys_ux_page_registry] table. This is where all (ootb & custom) UIB experiences get stored. So think of this table just like [sp_portal] is for Service Portals:

AlexCoopeSN_0-1688755183279.png


Now, the processor script is also designed for Non-Configurable workspaces as well. If you want to use it for those the table would be [sys_aw_master_config].


* Remember to pay attention to your "internal name" as that is what you'll use to call the artifact in your UI Action that you'll add to the [sys_ux_page_registry] table:

AlexCoopeSN_5-1667914076585.png

 

And now for the code of the artifact it self. Remember this a prototype, so I can't promise it will pick up everything on the first try, however as with all of the other artifacts we've prototyped, feel free to modify as you wish or expand as you wish. 

 

Note - When you click "Request Translations" on this artifact, it will likely take a few minutes or more because it is a very large query! Do not navigate away from the page or it will not complete.

var LF_workspace = Class.create();
LF_workspace.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
         **********/

        // we will need some arrays later for de-dupe checks
        var tableArr = [];
        var eventArr = [];
        var pMac = [];
        var gScr = [];
        var getUXCSI = [];
        var filterArr = [];
        var buttArr = [];
        var buttonName = [];
        var dbs = [];
        var uxCLStr = [];
        var libs = [];
        var libMsgStr = [];
        var decChecks = [];
        var formRelChecks = [];
        var formScopes = [];
        var formTables = [];

        var recRTarr = []; // we need the array to be available before we use it in the sub-function

        try {
            var tableName = params.tableName;
            var sysId = params.sysId;
            var language = params.language;
            var appCheck = '';
            var lfDocumentContentBuilder = new global.LFDocumentContentBuilder("v1", language, sysId, tableName);

            var getRec = new GlideRecord(tableName); // this will go to the sys_ux_page_registry table for a Next Experience
            getRec.addQuery('sys_id', sysId);
            getRec.query();

            if (getRec.next()) {
                // there are some default messages we need to factor in as they get shown across various workspaces
                var msgs = [
                    "No records to display.",
                    "No certified dashboards yet",
                    "No",
                    "Yes",
                    "Certified dashboards yet",
                    "There is no data available for the selected criteria.",
                    "View all",
                    "{0} records",
                    "records",
                    "rows per page",
                    "Loading",
                    "Loading visualizations...",
                    "Loading visualization data",
                    "Close Tab",
                    "Close other Tabs",
                    "Advanced view",
                    "New",
                    "Edit",
                    "Update",
                    "Export",
                    "List export",
                    "Close list menu",
                    "File Type",
                    "Delivery type",
                    "Last refreshed {lastrefreshedtimeago}",
                    "last refreshed {0} {1} ago.",
                    "last refreshed {0}",
                    "last refreshed",
                    "{0}m ago",
                    "{0}d ago",
                    "{0} days ago",
                    "Use existing filter",
                    "Save filter",
                    "results matching criteria",
                    "undo",
                    "redo",
                    "or",
                    "and",
                    "apply",
                    "groups",
                    "agents",
                    "skills",
                    "clear",
                    "clear all",
                    "editor",
                    "details",
                    "build a filter by adding conditions that contain a field, operator, and value(s).",
                    "delete condition",
                    "new condition set",
                    "showing {0}-{1} of {2}",
                    "refresh list",
                    "list actions",
                    "edit columns",
                    "Save as",
                    "reset widths",
                    "select columns and put them in the order you want.",
                    "available columns",
                    "selected columns",
                    "restore to column defaults",
                    "search",
                    "move the selected row in the available column to the selected column",
                    "is",
                    "is not",
                    "is empty",
                    "is not empty",
                    "ok",
                    "cancel",
                    "close dialog",
                    "more options",
                    "refresh",
                    "reload",
                    "add",
                    "copy url for",
                    "url copied to clipboard",
                    "type your {0} here",
                    "everyone can see this comment",
                    "work notes (private)",
                    "toggle compose settings",
                    "rich text editor",
                    "stacked view",
                    "stacked view enabled",
                    "type your {fieldlabel} here",
                    "post an activity stream message",
                    "show filter panel for {0} ({1} conditions)",
                    "filter",
                    "show filter",
                    "hide filter",
                    "open filters",
                    "saves filters",
                    "Expand filter overview",
                    "Open search bar",
                    "additional comments",
                    "additional comments (customer visible)",
                    "additional comments - visible to customer",
                    "additional comments or work notes",
                    "field changes",
                    "show calendar",
                    "Filter Overview",
                    "Recommended actions",
                    "No active recommendations",
                    "No recommendations to show",
                    "Contact",
                    "Timeline",
                    "now",
                    "show {0}",
                    "related search results",
                    "select a search resource",
                    "no matches found",
                    "try modifying your search text or filter to find what you're look for.",
                    "no attachments available",
                    "browse for a file to add it as an attachment",
                    "browse",
                    "templates",
                    "response templates",
                    "my templates",
                    "filter templates",
                    "clear search",
                    "create template",
                    "current",
                    "sort the available list in descending order",
                    "sort the available list in ascending order",
                    "Today",
                    "Yesterday",
                    "Tomorrow",
                    "This week",
                    "Last week",
                    "Next week",
                    "This month",
                    "Last month",
                    "Next month",
                    "Last 3 months",
                    "Last 6 months",
                    "Last 9 months",
                    "Last 12 months",
                    "This quarter",
                    "Last quarter",
                    "Last 2 quarters",
                    "Next quarter",
                    "Next 2 quarters",
                    "This year",
                    "Next year",
                    "Last year",
                    "Last 2 years",
                    "Last 7 days",
                    "Last 30 days",
                    "Last 60 days",
                    "Last 90 days",
                    "Last 120 days",
                    "Current hour",
                    "Last hour",
                    "Last 2 hours",
                    "Current minute",
                    "Last minute",
                    "Last 15 minutes",
                    "Last 30 minutes",
                    "Last 45 minutes",
                    "One year ago",
                    "start with",
                    "ends with",
                    "contains",
                    "does not contain",
                    "is anything",
                    "is one of",
                    "is not one of",
                    "is empty string",
                    "is (dynamic)",
                    "between",
                    "less than",
                    "less than or is",
                    "greater than",
                    "greater than or is",
                    "on",
                    "not on",
                    "before",
                    "at or before",
                    "after",
                    "at or after",
                    "is a",
                ];

                for (var msgI = 0; msgI < msgs.length; msgI++) {
                    lfDocumentContentBuilder.processString(msgs[msgI].toString(), "Default Messages", "Name");
                }

                // there's a few analytics strings we need to also consider
                lfDocumentContentBuilder.processString("Ask a question about your data", "Analytics", "Name");
                lfDocumentContentBuilder.processString("You can see how things are performing now and trends over time.", "Analytics", "Name");
                lfDocumentContentBuilder.processString("What do you want to see?", "Analytics", "Name");
                lfDocumentContentBuilder.processString("Ask", "Analytics", "Name");
                lfDocumentContentBuilder.processString("How can I improve my results?", "Analytics", "Name");

                // we need to grab any app dependencies from the admin panel's definition
                if (getRec.sys_scope != 'global' && getRec.dependencies != '') {
                    var appDeps = getRec.sys_scope.dependencies.toString().split(',');
                    appDeps.push(getRec.sys_scope.scope);
                    for (var iA = 0; iA < appDeps.length; iA++) {
                        var appCl = appDeps[iA].toString().replace(/(\:sys)|(\:[\d].*?(?=\,))/g, '');
                        var getApp = new GlideRecord('sys_package');
                        getApp.addQuery('source', appCl);
                        getApp.query();
                        if (getApp.next()) {
                            appCheck = getApp.sys_id;
                            _dataBrokerChecks('sys_ux_data_broker', appCheck);
                            _processApp(getRec, appCheck, sysId);

                            // we need to also check each scope we come across, and there could be quite a few grand-children
                            _checkScope(getRec, appCl, appCheck);
                        }
                    }
                } else {
                    appCheck = getRec.sys_scope.sys_id;
                    _dataBrokerChecks('sys_ux_data_broker', appCheck);
                    _processApp(getRec, appCheck);
                }
                // now we need to ensure we also process any core macroponents, and there might be quite a lot due to how inheritences work in UIB
                var procCoreMacros = new GlideRecord('sys_ux_macroponent');
                procCoreMacros.addEncodedQuery('sys_scopeLIKEsn-^ORsys_scopeLIKEservicenow^ORsys_scopeLIKEpolaris^ORsys_scope=' + getRec.sys_scope.sys_id.toString() + '^required_translations!=^required_translations!=[]^required_translations!=[ ]');
                procCoreMacros.query();
                while (procCoreMacros.next()) {
                    _getMacro(procCoreMacros.sys_id.toString());
                }

                // now we also need to check for any core lib components
                var libChecks = new GlideRecord('sys_ux_lib_component');
                libChecks.addEncodedQuery('sys_scopeLIKEsn-^ORsys_scopeLIKEservicenow^ORsys_scopeLIKEpolaris^ORsys_scope=' + getRec.sys_scope.sys_id.toString() + '^required_translation_keysLIKEmessage^required_translation_keysNOT LIKEUI Builder');
                libChecks.query();
                while (libChecks.next()) {
                    _getLib(libChecks.sys_id.toString());
                }

                // we also need to check if we've got any specific Dashboards for this workspace
                var parDashCheck = new GlideRecord('par_dashboard_visibility');
                parDashCheck.addEncodedQuery('experience.sys_id=' + getRec.sys_id.toString());
                parDashCheck.query();
                while (parDashCheck.next()) {
					// lets process this record
					lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(parDashCheck, "PAR Dashboard Visibility - "+parDashCheck.dashboard.getDisplayValue(), "Name");

                    // now we need to check each dashboard
                    var getDashCanv = new GlideRecord('par_dashboard_canvas');
                    getDashCanv.addEncodedQuery('dashboard.sys_id=' + parDashCheck.dashboard.sys_id.toString());
                    getDashCanv.query();
                    while (getDashCanv.next()) {
						// lets process the canvas
						lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getDashCanv, "PAR Dashboard Canvas - "+parDashCheck.dashboard.getDisplayValue(), "Name");

                        // now we need to get the PAR widgets
                        var getDashWid = new GlideRecord('par_dashboard_widget');
                        getDashWid.addQuery('canvas.sys_id=' + getDashCanv.sys_id.toString());
                        getDashWid.query();
                        while (getDashWid.next()) {
                            // we need to process the macroponents here
                            _getMacro(getDashWid.component.sys_id.toString());

                            // we need to process any translatable fields
                            lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getDashWid, "PAR Dashboard Widget - "+parDashCheck.dashboard.getDisplayValue(), "Widget");

                            // now we need to process any labels in the JSON
                            var comps = getDashWid.component_props.toString();
                            var compJSON = JSON.parse(comps, function(key, val) {
                                if (compJSON != '') {
                                    if (val != null) {
                                        // this is a defensive check as some objects may contain the word "null"

                                        if (key == 'label' && (val.toString() != '' && val.toString() != ' ')) {
                                            lfDocumentContentBuilder.processString(val.toString(), "PAR Dashboard Details - "+parDashCheck.dashboard.getDisplayValue(), "Label");

                                            // now we need to check if this has a PA indicator associated to it
                                            var checkPAind = new GlideRecord('pa_indicators');
                                            checkPAind.addQuery('name', val.toString());
                                            checkPAind.query();
                                            if (checkPAind.next()) {
                                                // let's process this indicator
                                                lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(checkPAind, "PAR PA Indicator - "+parDashCheck.dashboard.getDisplayValue(), "Name");

                                                // now we need to check all Indicator Breakdowns
                                                var checkPAIndBreak = new GlideRecord('pa_indicator_breakdowns');
                                                checkPAIndBreak.addQuery('indicator', checkPAind.sys_id);
                                                checkPAIndBreak.query();
                                                while (checkPAIndBreak.next()) {
                                                    // now we need to follow to each breakdown
                                                    lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(checkPAIndBreak, "PAR PA Indicator Breakdown - "+parDashCheck.dashboard.getDisplayValue(), "Name");

                                                    var getPAIndBreakdown = new GlideRecord('pa_breakdowns');
                                                    getPAIndBreakdown.addQuery('sys_id', checkPAIndBreak.breakdown.sys_id);
                                                    getPAIndBreakdown.query();
                                                    if (getPAIndBreakdown.next()) {
                                                        lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getPAIndBreakdown, "PAR PA Breakdown - "+parDashCheck.dashboard.getDisplayValue(), "Name");
                                                    }
                                                }
                                            }
                                        }
                                        if (key == 'emptyStateHeading' && (val.toString() != '' && val.toString() != ' ')) {
                                            lfDocumentContentBuilder.processString(val.toString(), "PAR Dashboard Details - "+parDashCheck.dashboard.getDisplayValue(), "Empty State Heading");
                                        }
                                        if (key == 'emptyStateContent' && (val.toString() != '' && val.toString() != ' ')) {
                                            lfDocumentContentBuilder.processString(val.toString(), "PAR Dashboard Details - "+parDashCheck.dashboard.getDisplayValue(), "Empty State Content");
                                        }
                                        if (key == 'headerTitle' && (val.toString() != '' && val.toString() != ' ')) {
                                            lfDocumentContentBuilder.processString(val.toString(), "PAR Dashboard Details - "+parDashCheck.dashboard.getDisplayValue(), "Header Title");
                                        }
                                        if (key == 'description' && (val.toString() != '' && val.toString() != ' ')) {
                                            lfDocumentContentBuilder.processString(val.toString(), "PAR Dashboard Details - "+parDashCheck.dashboard.getDisplayValue(), "Description");
                                        }
                                        if (key == 'metrics' && (val.toString() != '' && val.toString() != ' ')) {
                                            lfDocumentContentBuilder.processString(val.toString(), "PAR Dashboard Details - "+parDashCheck.dashboard.getDisplayValue(), "Metric Label");
                                        }
                                        if (key == 'html' && (val.toString() != '' && val.toString() != ' ')) {
                                            lfDocumentContentBuilder.processString(val.toString(), "PAR Dashboard Details - "+parDashCheck.dashboard.getDisplayValue(), "HTML");
                                        }
                                        if (key == 'filterName' && (val.toString() != '' && val.toString() != ' ')) {
                                            lfDocumentContentBuilder.processString(val.toString(), "PAR Dashboard Details - "+parDashCheck.dashboard.getDisplayValue(), "Filter Name");
                                        }
                                        if (key == 'xAxisTitle' && (val.toString() != '' && val.toString() != ' ')) {
                                            lfDocumentContentBuilder.processString(val.toString(), "PAR Dashboard Details - "+parDashCheck.dashboard.getDisplayValue(), "xAxis Title");
                                        }
                                        if (key == 'yAxis0Title' && (val.toString() != '' && val.toString() != ' ')) {
                                            lfDocumentContentBuilder.processString(val.toString(), "PAR Dashboard Details - "+parDashCheck.dashboard.getDisplayValue(), "yAxis0 Title");
                                        }
                                        if (key == 'yAxis1Title' && (val.toString() != '' && val.toString() != ' ')) {
                                            lfDocumentContentBuilder.processString(val.toString(), "PAR Dashboard Details - "+parDashCheck.dashboard.getDisplayValue(), "yAxis1 Title");
                                        }
                                        if (key == 'additionalGroupByConfig' && (val.toString() != '' && val.toString() != ' ')) {
                                            lfDocumentContentBuilder.processString(val.toString(), "PAR Dashboard Details - "+parDashCheck.dashboard.getDisplayValue(), "Alternative Group By");
                                        }
                                        if (key == "dataSources" && (val.toString() != '' && val.toString() != ' ')) {
                                            lfDocumentContentBuilder.processString(val.toString(), "PAR Dashboard Details - "+parDashCheck.dashboard.getDisplayValue(), "Data sources");
                                        }
                                        if (key == "title" && (val.toString() != '' && val.toString() != ' ')) {
                                            lfDocumentContentBuilder.processString(val.toString(), "PAR Dashboard Details - "+parDashCheck.dashboard.getDisplayValue(), "Title");
                                        }

                                        // we need to do some quick specific checks for "Filter" macroponent
                                        if (getDashWid.component.name == "Filter" && key == "filterName") {
                                            lfDocumentContentBuilder.processString(val.toString(), "PAR Dashboard Details - "+parDashCheck.dashboard.getDisplayValue(), "Filter name");
                                        }
                                    }
                                }
                            });
                        }

                        // we need to get the dashboard tabs as well
                        var parDashTab = new GlideRecord('par_dashboard_tab');
                        parDashTab.addQuery('dashboard', getDashCanv.dashboard);
						parDashTab.addQuery('active', 'true');
                        parDashTab.addQuery('name', getDashCanv.dashboard_tab.getDisplayValue());
                        parDashTab.query();
                        while (parDashTab.next()) {
                            lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(parDashTab, "PAR Dashboard Tab - "+parDashCheck.dashboard.getDisplayValue(), "Tab");
                        }
                    }

                    // we also need to process the actual dashboard
                    var getParDash = new GlideRecord('par_dashboard');
                    getParDash.addQuery('sys_id', parDashCheck.dashboard.sys_id);
                    getParDash.query();
                    if (getParDash.next()) {
                        lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getParDash, "PAR Dashboard - "+parDashCheck.dashboard.getDisplayValue(), "Dashboard");

                        // Experience - we should just run this artifact against the experience here, otherwise the payload could be too big

                        // Macroponent - now we need to check for the UXF Page Macroponent
                        _getMacro(getParDash.macroponent.sys_id.toString());

                        // UX Screen - we shouldn't need to check this

                        // UX Screen Collection - we shouldn't need to check this

                        // UXF Page Routes - we shouldn't need to check this
                    }
                }
                // we need to check a few specifics at this page registry level
                if (getRec.root_macroponent != '') {
                    _getMacro(getRec.root_macroponent.sys_id.toString());
                }

                // now we need to check if there's a Parent App associated
                if (getRec.parent_app != '') {
                    _processApp(getRec.parent_app.primary_experience.sys_id.toString(), getRec.parent_app.primary_experience.sys_scope.sys_id.toString());

                    // we also need to check if there's an App Shell Rout UI as well
                    _getMacro(getRec.parent_app.shell_root.sys_id.toString());
                }
            }
        } catch (err) {
            gs.log("Error in getTranslatableContent - Next Experiences - " + err.name + " - " + err.message);
        }

        function _checkScope(getRec, appCl, appCheck) {
            // now we need to go and check for any other installed store apps who have a dependency on one of these scopes
            var checkDepScope = new GlideRecord('sys_scope');
            checkDepScope.addQuery('sys_id', "ISNOT", getRec.sys_scope.sys_id); // we don't want to accidentally pick up our current scope record
            checkDepScope.addQuery('scope', "ISNOT", appCl); // we don't want to pick up the current loop
            checkDepScope.addQuery('ref_dependencies', "CONTAINS", appCheck); // any other app has a dependency on this one
            checkDepScope.query();
            while (checkDepScope.next()) {
                appCheck = checkDepScope.sys_id;
                if (appCheck) {
                    // we need to check for Data Brokers of these scopes
                    _dataBrokerChecks("sys_ux_data_broker", appCheck);

                    // we need to process the apps
                    _processApp(getRec, appCheck);

                    // we also need to check any Scope specific macroponents
                    var MacCheckScopes = new GlideRecord('sys_ux_macroponent');
                    MacCheckScopes.addEncodedQuery('sys_scopeLIKEsn-^ORsys_scopeLIKEservicenow^ORsys_scopeLIKEpolaris^ORsys_scope=' + appCheck.toString() + '^required_translations!=^required_translations!=[]^required_translations!=[ ]');
                    MacCheckScopes.query();
                    while (MacCheckScopes.next()) {
                        _getMacro(MacCheckScopes.sys_id.toString());
                    }

                    // we need to check for specific Lib Components in this scope
                    var LibCheckScopes = new GlideRecord('sys_ux_lib_component');
                    LibCheckScopes.addEncodedQuery('sys_scopeLIKEsn-^ORsys_scopeLIKEservicenow^ORsys_scopeLIKEpolaris^ORsys_scope=' + appCheck.toString() + '^required_translation_keysLIKEmessage^required_translation_keysNOT LIKEUI Builder');
                    LibCheckScopes.query();
                    while (LibCheckScopes.next()) {
                        _getLib(LibCheckScopes.sys_id.toString());
                    }
                }
            }
        }

        function _getLib(getSys) {
            try {
                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(), 'UX Lib Component', getRootElC.tag.toString());
                                        libMsgStr.push(libVal.toString());
                                    }
                                }
                            });
                        } 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());
                                }
                            }
                        }
                    }
                    libs.push(getSys);
                }
            } catch (err) {
                gs.log("Error in _getLib - " + err.name + " - " + err.message);
            }
        }

        function _processApp(getRec, appScope, sysId) {
            try {
                // there are some specific checks we need to perform if it's a new Now Experience UI
                if (tableName == 'sys_ux_page_registry' && getRec.admin_panel != '') {
                    // we need to check if the there are any translations in the page properties
                    var getPageProps = new GlideRecord('sys_ux_page_property');
                    getPageProps.addEncodedQuery('page.sys_id=' + sysId);
                    //getPageProps.addQuery('sys_scope', appScope); // might not be needed
                    getPageProps.orderBy('name');
                    getPageProps.query();
                    while (getPageProps.next()) {
                        if (getPageProps.required_translations.toString().includes('message')) {
                            // lets call our Required Translation function
                            _reqTranslations(getPageProps.sys_id.toString());
                        }
                    }

                    // we now need to go to the Admin panel before anything
                    var nowPageReg = new GlideRecord(getRec.admin_panel.sys_class_name.toString());
                    nowPageReg.addQuery('sys_id', getRec.admin_panel.sys_id.toString());
                    nowPageReg.orderBy('name');
                    nowPageReg.query();
                    if (nowPageReg.next()) {
                        var appRouteCheck = new GlideRecord('sys_ux_app_route');
                        appRouteCheck.addEncodedQuery('app_config.sys_id=' + nowPageReg.sys_id + '^parent_macroponent!=NULL');
                        appRouteCheck.orderBy('name');
                        appRouteCheck.query();
                        while (appRouteCheck.next()) {
                            if (appRouteCheck.parent_macroponent) {
                                if (!pMac.toString().includes(appRouteCheck.parent_macroponent.sys_id.toString())) {
                                    lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(appRouteCheck, 'Page Routes', 'Name');
                                    _getMacro(appRouteCheck.parent_macroponent.sys_id.toString());
                                    pMac.push(appRouteCheck.parent_macroponent.sys_id.toString());
                                }
                            }

                            if (appRouteCheck.screen_type) {
                                // there are some specific screens we need to get also
                                var getScrColls = new GlideRecord('sys_ux_screen');
                                getScrColls.addEncodedQuery('screen_type=' + appRouteCheck.screen_type.sys_id);
                                getScrColls.orderBy('name');
                                getScrColls.query();
                                while (getScrColls.next()) {
                                    if (!gScr.toString().includes(getScrColls.sys_id.toString())) {
                                        lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getScrColls, "UX Screen", "Name");
                                        if (getScrColls.required_translations.toString().includes('message')) {
                                            _reqTranslations(getScrColls.sys_id.toString());
                                        }
                                        // we need to check the related Page Definition (macroponent)
                                        _getMacro(getScrColls.macroponent.sys_id.toString());

                                        // we need to also process the "parent macroponent"
                                        _getMacro(getScrColls.parent_macroponent.sys_id.toString());

                                        // now we need to check if the screen_type is empty
                                        if (getScrColls.screen_type != '') {
                                            var getScrCollsType = new GlideRecord('sys_ux_screen_type');
                                            getScrCollsType.addQuery('sys_id', getScrColls.screen_type.sys_id);
                                            getScrCollsType.query();
                                            if (getScrCollsType.next()) {
                                                lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getScrCollsType, "UX Screen Type", "Name");
                                            }
                                        }
                                    }
                                    gScr.push(getScrColls.sys_id.toString());
                                }
                            }

                            // now we need to do various data-broker checks
                            _dataBrokerChecks('sys_ux_data_broker', appRouteCheck.sys_scope.sys_id);

                            // now we need to check for Next Experience lists
                            if (appRouteCheck.name.toString().toLowerCase() == 'list') {
                                var getNowLists = new GlideRecord('sys_ux_list_menu_config');
                                getNowLists.addEncodedQuery('sys_scope.sys_id=' + appRouteCheck.sys_scope.sys_id);
                                getNowLists.addQuery('active', 'true');
                                getNowLists.orderBy('name');
                                getNowLists.query();
                                if (getNowLists.next()) {
                                    lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getNowLists, 'Next Experience List', getNowLists.getDisplayValue());
                                    // we need to get the list categories
                                    var getNowListCat = new GlideRecord('sys_ux_list_category');
                                    getNowListCat.addQuery('configuration', getNowLists.sys_id.toString());
                                    getNowListCat.addQuery('active', 'true');
                                    getNowListCat.orderBy('order');
                                    getNowListCat.query();
                                    while (getNowListCat.next()) {
                                        lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getNowListCat, "Next Experience List Category", getNowLists.name + ' - ' + getNowListCat.getDisplayValue());
                                        // from this list we need to process the items in the categories
                                        var getNowListItems = new GlideRecord('sys_ux_list');
                                        getNowListItems.addQuery('category', getNowListCat.sys_id.toString());
                                        getNowListItems.addQuery('active', 'true');
                                        getNowListItems.orderBy('order');
                                        getNowListItems.query();
                                        while (getNowListItems.next()) {
                                            lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getNowListItems, "Next Experience List Items", +getNowListCat.category.getDisplayValue() + ' - ' + getNowListItems.getDisplayValue() + ' ' + getNowListCat.getDisplayValue());
                                            // if we want to process field labels and column headers, we need to process the "columns", this will be something in the future

                                            // now we need to check for any specific UI actions
                                            //_getUIAs(getNowListItems.table); // might not be needed

                                            // now we need to check for any filters
                                            _checkFilters(getNowListItems.table.toString());

                                            // now we need to check for UIactions / Buttons
                                            _checkButtons(getNowListItems.table.toString());
                                        }
                                    }
                                }
                            }
                        }
                        // now we need to get the screens
                        var getUXs = new GlideRecord('sys_ux_screen');
                        getUXs.addEncodedQuery('app_config.sys_id=' + nowPageReg.sys_id.toString());
                        getUXs.orderBy('name');
                        getUXs.query();
                        while (getUXs.next()) {
                            lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getUXs, 'UX Screen', 'Name');
                            if (getUXs.macroponent) {
                                _getMacro(getUXs.macroponent.sys_id.toString()); // we need to check for any related macroponents
                            }
                            if (getUXs.parent_macroponent) {
                                _getMacro(getUXs.parent_macroponent.sys_id.toString()); // we need to check for any related parent macroponents
                            }
                            if (getUXs.required_translations.toString().includes('message')) {
                                _reqTranslations(getUXs.sys_id.toString());
                            }

                            // now we need to check if the screen_type is empty
                            if (getUXs.screen_type != '') {
                                var getUXsType = new GlideRecord('sys_ux_screen_type');
                                getUXsType.addQuery('sys_id', getUXs.screen_type.sys_id);
                                getUXsType.query();
                                if (getUXsType.next()) {
                                    lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getUXsType, "UX Screen Type", "Name");
                                }
                            }
                        }
                    }

                    // we might need some specific pages in this app
                    _processPage(tableName.toString(), getRec.page.sys_id.toString());
                }

                lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getRec, 'Experience', 'Name');

                // lists in the workspace (the non-next Experiences)
                var adminP = '';
                if (getRec.admin_panel.sys_class_name == 'sys_aw_master_config') {
                    adminP = getRec.admin_panel.sys_id.toString();
                    _processPage(tableName.toString(), getRec.admin_panel.sys_id.toString());
                } else {
                    _processPage(tableName.toString(), getRec.sys_id.toString());
                    adminP = getRec.sys_id.toString();
                }

                var getWlists = new GlideRecord('sys_aw_list');
                getWlists.addNotNullQuery('workspace');
                getWlists.addQuery('workspace', adminP);
                getWlists.query();

                if (!getWlists.hasNext()) {
                    // if this isn't for an Experience from UIB, we need to do something a bit different
                    _UIForms(getRec.sys_scope.toString(), 'true', getRec.sys_scope.sys_id);

                }
                while (getWlists.next()) {
                    lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getWlists, 'Non-Experience Lists', 'Lists');
                    _UIForms(getWlists.table.toString(), 'false', getRec.sys_scope.sys_id);
                }

                // now we need to get the Action configs
                var getUxActConf = new GlideRecord('sys_ux_action_config');
                getUxActConf.addQuery('sys_scope.sys_id', appScope); // we have to match to the application scope
                getUxActConf.query();
                if (getUxActConf.next()) {
                    var getUxActM2M = new GlideRecord('sys_ux_m2m_action_assignment_action_config');
                    getUxActM2M.addEncodedQuery('action_configuration.sys_id=' + getUxActConf.sys_id);
                    getUxActM2M.query();
                    while (getUxActM2M.next()) {
                        // now we need to get the actual record
                        var getUxAsActConf = new GlideRecord(getUxActM2M.action_configuration.sys_class_name.toString());
                        getUxAsActConf.addQuery('sys_id', getUxActM2M.action_configuration.sys_id);
                        getUxAsActConf.query();
                        if (getUxAsActConf.next()) {
                            lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getUxAsActConf, 'Action Configs', getUxAsActConf.getDisplayValue());
                        }
                        var decActAss = new GlideRecord(getUxActM2M.action_assignment.sys_class_name.toString());
                        decActAss.addQuery('sys_id', getUxActM2M.action_assignment.sys_id);
                        decActAss.query();
                        if (decActAss.next()) {
                            lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(decActAss, 'Declarative Actions', decActAss.getDisplayValue());
                            var getActModFld = new GlideRecord('sys_declarative_action_model_field');
                            getActModFld.addQuery('sys_id', decActAss.action_config.sys_id);
                            getActModFld.query();
                            while (getActModFld.next()) {
                                lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getActModFld, 'Declaratitive Action Models', getActModFld.getDisplayValue());
                            }
                        }
                    }
                    var getFormActLay = new GlideRecord('sys_ux_form_action_layout');
                    getFormActLay.addQuery('action_config.sys_id', getUxActConf.sys_id);
                    getFormActLay.query();
                    while (getFormActLay.next()) {
                        lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getFormActLay, 'Form Action Layout', getFormActLay.getDisplayValue());
                        // let's get some UI actions
                        var getActLay = new GlideRecord('sys_ux_m2m_action_layout_item');
                        getActLay.addQuery('ux_form_action_layout.sys_id', getFormActLay.sys_id);
                        getActLay.query();
                        while (getActLay.next()) {
                            // we need to get the actual record
                            var getActLayItem = new GlideRecord(getActLay.ux_form_action_layout_item.sys_class_name.toString());
                            getActLayItem.addQuery('sys_id', getActLay.ux_form_action_layout_item.sys_id);
                            getActLayItem.query();
                            if (getActLayItem.next()) {
                                lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getActLayItem, 'Action Layout Item', getActLayItem.getDisplayValue());
                            }
                            var getFormLayItem = new GlideRecord('sys_ux_form_action_layout_item');
                            getFormLayItem.addQuery('sys_id', getActLay.ux_form_action_layout_item.sys_id);
                            getFormLayItem.query();
                            if (getFormLayItem.next()) {
                                lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getFormLayItem, 'Form Action Items', getFormLayItem.getDisplayValue());
                            }
                        }
                    }
                }
            } catch (err) {
                gs.log("Error in _processApp - " + err.name + " - " + err.message);
            }
        }

        function _getUIAs(table) {
            try {
                if (!tableArr.toString().includes(table.toString())) {
                    // we need to ensure we don't cause duplicate checks
                    var getUIAs = new GlideRecord('sys_ui_action');
                    getUIAs.addEncodedQuery('form_button_v2=true^ORform_menu_button_v2=true^table=' + table + '^active=true');
                    getUIAs.query();
                    while (getUIAs.next()) {
                        lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getUIAs, "UI Actions / Buttons", getUIAs.getDisplayValue());
                        lfDocumentContentBuilder.processScript(getUIAs.client_script_v2, "UI Actions / Buttons", getUIAs.getDisplayValue());
                    }
                    tableArr.push(table);
                }
            } catch (err) {
                gs.log('Error in _getUIAs - ' + err.name + ' - ' + err.message);
            }
        }

        // we need to force task and global tables for checks
        _checkButtons('global');
        _checkButtons('task');

        // while we're here, we also need to check for any core / global specific data brokers
        var dataBCheck = new GlideRecord('sys_ux_data_broker');
        dataBCheck.addEncodedQuery('sys_scopeLIKEsn-^ORsys_scopeLIKEservicenow^ORsys_scopeLIKEpolaris');
        dataBCheck.query();
        while (dataBCheck.next()) {
            _dataBrokerChecks('sys_ux_data_broker', dataBCheck.sys_scope.sys_id.toString());
        }

        function _checkButtons(tableName) {
            try {
                var table = tableName.toString();
                if (!buttArr.toString().includes(table)) {
                    // we also need to grab any other (traditional UI actions in this experience)
                    var getOldUIAs = new GlideRecord('sys_ux_form_action');
                    getOldUIAs.addEncodedQuery('table=global^ORtable=task^ORtable=' + table);
                    getOldUIAs.addQuery('active', 'true');
                    getOldUIAs.query();
                    while (getOldUIAs.next()) {
                        if (getOldUIAs.action_type == 'declarative_action') {
                            lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getOldUIAs, 'UI Actions - Declarative Actions', "Name - " + getOldUIAs.getDisplayValue() + ' - ' + table.toString());

                            // now we need to process any directly related declarative action
                            if (getOldUIAs.declarative_action != '') {
                                var checkDec = new GlideRecord('sys_declarative_action_assignment');
                                checkDec.addQuery('sys_id', getOldUIAs.declarative_action.sys_id);
                                checkDec.addQuery('active', 'true');
                                checkDec.query();
                                while (checkDec.next()) {
                                    if (!decChecks.toString().includes(checkDec.sys_id.toString())) {
                                        lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(checkDec, "UI Actions - Declarative Actions", "Action - " + table.toString());

                                        // we need to check the server script
                                        lfDocumentContentBuilder.processScript(checkDec.server_script, "UI Actions - Declarative Actions", "Server Script - " + table.toString());

                                        // now we need to process any confirmation message
                                        if (checkDec.confirmation_required) {
                                            lfDocumentContentBuilder.processString(checkDec.confirmation_message.toString(), "UI Actions - Declarative Actions", "Confirmation Message - " + table.toString());
                                        }

                                        // now we need to check if there's an associated UI component
                                        if (checkDec.ui_component) {
                                            _getLib(checkDec.ui_component.sys_id);
                                        }
                                        decChecks.push(checkDec.sys_id, toString());
                                    }
                                }
                            }
                        } else if (getOldUIAs.action_type == 'ui_action') {
                            var getUIA = new GlideRecord('sys_ui_action');
                            getUIA.addEncodedQuery('form_button=true^ORform_button_v2=true^ORform_menu_button_v2=true^active=true^table=' + table);
                            getUIA.addQuery('sys_id', getOldUIAs.ui_action.sys_id);
                            getUIA.query();
                            if (getUIA.next()) {
                                if (!buttonName.toString().includes(getUIA.sys_id.toString())) {
                                    lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getUIA, 'UI Actions / Buttons', "Name - " + getUIA.getDisplayValue() + ' - ' + table.toString());

                                    if (getUIA.script.toString().includes('getMessage')) {
                                        lfDocumentContentBuilder.processScript(getUIA.script, "UI Actions / Buttons", "Script - " + table.toString());
                                    }

                                    if (getUIA.client_script_v2.toString().includes('getMessage')) {
                                        lfDocumentContentBuilder.processScript(getUIA.client_script_v2, "UI Actions / Buttons", "Script - " + table.toString());
                                    }

                                    buttonName.push(getUIA.sys_id.toString());
                                }
                            }
                        }
                    }

                    // we also need to make specific checks for the sys_declarative_action_assignment records associated to any of the tables we care about
                    var decActCheck = new GlideRecord('sys_declarative_action_assignment');
                    decActCheck.addEncodedQuery('table=' + table);
                    decActCheck.addQuery('active', 'true');
                    decActCheck.query();
                    while (decActCheck.next()) {
                        if (!decChecks.toString().includes(decActCheck.sys_id.toString())) {
                            lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(decActCheck, "UI Actions - Declarative Actions", "Action - " + table.toString());

                            // now we need to check if there's an associated UI component
                            if (decActCheck.ui_component) {
                                _getLib(decActCheck.ui_component.sys_id);
                            }

                            // we need to check the server script
                            lfDocumentContentBuilder.processScript(decActCheck.server_script, "UI Actions - Declarative Actions", "Server Script - " + table.toString());

                            // now we need to process any confirmation message
                            if (decActCheck.confirmation_required) {
                                lfDocumentContentBuilder.processString(decActCheck.confirmation_message.toString(), "UI Actions - Declarative Actions", "Confirmation Message - " + table.toString());
                            }
                            decChecks.push(decActCheck.sys_id.toString());
                        }
                    }

                    // now we also need to do a double check for any other type of UI action
                    var UIactCheck = new GlideRecord('sys_ui_action');
                    UIactCheck.addEncodedQuery('form_button=true^ORform_button_v2=true^ORform_menu_button_v2=true^active=true^table=' + table);
                    UIactCheck.query();
                    while (UIactCheck.next()) {
                        if (!buttonName.toString().includes(UIactCheck.sys_id.toString())) {
                            // to make sure we don't inadvertantly process one we've already checked from a Action button perspective
                            var UIFActCheck = new GlideRecord('sys_ux_form_action');
                            UIFActCheck.addQuery('ui_action.sys_id', UIactCheck.sys_id);
                            UIFActCheck.query();
                            if (!UIFActCheck.hasNext()) {
                                // we only want to know if there isn't a match rather than grabbing the entire record
                                lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(UIactCheck, "UI Actions / Buttons", "Name - " + UIactCheck.getDisplayValue() + " - " + table.toString());

                                // we also need to process the script
                                if (UIactCheck.script.toString().includes('getMessage')) {
                                    lfDocumentContentBuilder.processScript(UIactCheck.script, "UI Actions / Buttons", "Script - " + table.toString());
                                }

                                if (UIactCheck.client_script_v2.toString().includes('getMessage')) {
                                    lfDocumentContentBuilder.processScript(UIactCheck.client_script_v2, "UI Actions / Buttons", "Script - " + table.toString());
                                }

                                buttonName.push(UIactCheck.sys_id.toString());
                            }
                        }
                    }

                    buttArr.push(table.toString());
                }
            } catch (err) {
                gs.log('Error in _checkButtons - ' + err.name + ' - ' + err.message);
            }
        }

        function _checkFilters(table) {
            try {
                if (!filterArr.toString().includes(table.toString())) {
                    var procFilters = new GlideRecord('sys_filter');
                    procFilters.addQuery('table', table);
                    procFilters.query();
                    while (procFilters.next()) {
                        lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(procFilters, "Table Filters", "Name");
                        lfDocumentContentBuilder.processString(procFilters.title.toString(), "Table Filters", "Message");
                    }
                    filterArr.push(table.toString());
                }
            } catch (err) {
                gs.log('Error in _checkFilters - ' + err.name + ' - ' + err.message);
            }
        }

        function _checkEvents(eventSYS) {
            try {
                if (!eventArr.toString().includes(eventSYS.toString())) {
                    // let's get the event
                    var getEvent = new GlideRecord('sys_ux_event');
                    getEvent.addQuery('sys_id', eventSYS);
                    getEvent.query();
                    if (getEvent.next()) {
                        lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getEvent, "Event Labels", "Name");
                        if (getEvent.required_translations.toString().includes('message')) {
                            _reqTranslations(getEvent.sys_id.toString());
                        }
                    }
                    eventArr.push(eventSYS);
                }
            } catch (err) {
                gs.log("Error in _checkEvents - " + err.name + " - " + err.message);
            }
        }

        function _dataBrokerChecks(table, appScope) {
            try {
                // we need to process the dataBroker checks
                var getDataBroker = new GlideRecord(table);
                getDataBroker.addEncodedQuery('sys_scope.sys_id=' + appScope);
                getDataBroker.orderBy('name');
                getDataBroker.query();
                while (getDataBroker.next()) {
                    if (!dbs.toString().includes(getDataBroker.sys_id.toString())) {
                        // we don't need to process a databroker we've already seen

                        lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getDataBroker, "Data Broker", "Name");
                        _reqTranslations(getDataBroker.sys_id.toString());

                        // some records in extended tables might not be processing required_translations and actually be using script fields
                        if (getDataBroker.sys_class_name == 'sys_ux_data_broker_transform' || getDataBroker.sys_class_name == 'sys_ux_data_broker_scriptlet') {
                            // we need to get the actual record so we can call it's script field
                            var getDBs = new GlideRecord(getDataBroker.sys_class_name.toString());
                            getDBs.addQuery('sys_id', getDataBroker.sys_id);
                            getDBs.query();
                            if (getDBs.next()) {
                                lfDocumentContentBuilder.processScript(getDBs.script, "Data Broker Transforms", "Name");
                            }
                        }

                        if (getDataBroker.sys_class_name == 'sys_ux_data_broker_proxy') {
                            _getMacro(getDataBroker.macroponent.sys_id);
                        }

                        // we need to do a few specific checks for GraphQL's
                        if (getDataBroker.sys_class_name == "sys_ux_data_broker_proxy") {
                            // we need to process the props field
                            if (getDataBroker.props) {
                                var propsJSON = getDataBroker.props.toString();
                                var propsCheck = JSON.parse(propsJSON, function(dBkey, dBval) {
                                    if (dBkey == 'label') {
                                        lfDocumentContentBuilder.processString(dBval.toString(), "Data Broker Graph QL", "Label");
                                    }
                                    if (dBkey == 'description') {
                                        lfDocumentContentBuilder.processString(dBval.toString(), "Date Broker Graph QL", "Description");
                                    }
                                });
                            }
                        }
                        dbs.push(getDataBroker.sys_id.toString()); // update the array
                    }
                }
            } catch (err) {
                gs.log("Error in _dataBrokerChecks -" + err.name + " - " + err.message);
            }
        }


        function _UIForms(source, flag, scope) {
            try {
                var query = '';
                var srcRec = source.toString();
                if (flag == 'false') {
                    if (!formTables.toString().includes(source.toString())) {
                        // non-configurable workspace
                        query = 'name' + '=' + srcRec.toString();
                    }
                } else if (flag == 'true') {
                    if (!formScopes.toString().includes(source.toString())) {
                        // UIB experience
                        query = "view.nameLIKEworkspace^sys_scope=" + scope + "^ORsys_scope=global";
                    }
                }
                query = query.toString();

                // 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.addEncodedQuery(query);
                wForm.orderBy('table');
                wForm.query();
                while (wForm.next()) {
                    // need to check the lists for this view
                    var wFormLists = new GlideRecord('sys_ui_list');
                    wFormLists.addEncodedQuery('view.sys_id=' + wForm.view.sys_id);
                    wFormLists.query();
                    while (wFormLists.next()) {
                        if (wFormLists.relationship != '') {
                            // now we need to check each relationship
                            var getTableRel = new GlideRecord('sys_relationship');
                            getTableRel.addQuery('sys_id', wFormLists.relationship.sys_id);
                            getTableRel.query();
                            while (getTableRel.next()) {
                                if (!formRelChecks.toString().includes(getTableRel.sys_id.toString())) {
                                    lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getTableRel, "Form Relationships", "Name");
                                }
                            }
                        }
                    }

                    lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(wForm, 'Form', wForm.name);
                    // now we need to get the form sections
                    var formSec = new GlideRecord('sys_ui_form_section');
                    formSec.addEncodedQuery('sys_ui_form=' + wForm.sys_id);
                    formSec.orderBy('position');
                    formSec.query();
                    while (formSec.next()) {
                        lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(formSec, 'Form Section', formSec.name);
                        // 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, 'UI Section - ' + wForm.getDisplayValue(), formSecCheck.name);
                            // now we have the actual form section, we need the elements that make it up
                            var formSecEl = new GlideRecord('sys_ui_element');
                            formSecEl.addEncodedQuery('sys_ui_section.sys_id=' + formSecCheck.sys_id);
                            formSecEl.addQuery('element', 'DOES NOT CONTAIN', 'split');
                            formSecEl.orderBy('position');
                            formSecEl.query();
                            while (formSecEl.next()) {
                                lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(formSecEl, 'Fields', formSecEl.element);
                                // the "Element" record here is the db name of a field, this will come in a future release
                            }
                        }
                    }
                }
            } catch (err) {
                gs.log("Error in _UIForms - " + err.name + " - " + err.message);
            }
        }


        function _processPage(table, p_sys) {
            try {
                var rootElem = '';
                if (table == 'sys_aw_master_config') {
                    rootElem = UxFrameworkScriptables.getLandingPagePlaceholderSysId(p_sys);
                } else {
                    rootElem = p_sys;
                }

                // landing pages
                var wLanding = new GlideRecord('sys_ux_custom_content_root_elem');
                wLanding.addQuery('placeholder', rootElem);
                wLanding.orderBy('name');
                wLanding.query();
                while (wLanding.next()) {
                    lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(wLanding, wLanding.sys_class_name.getDisplayValue(), wLanding.getDisplayValue());
                    // UX Page elements
                    var UXPelements = new GlideRecord('sys_ux_page_element');
                    UXPelements.addQuery('parent', rootElem);
                    UXPelements.query();
                    while (UXPelements.next()) {
                        lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(UXPelements, UXPelements.sys_class_name.getDisplayValue(), UXPelements.getDisplayValue());
                    }
                    // we also need to check the ux_macrocomponents
                    _getMacro(wLanding.macroponent.sys_id.toString());
                }

                // New Record Items
                var newRec = GlideRecord('sys_aw_new_menu_item');
                newRec.addQuery('workspace_config', rootElem);
                newRec.query();
                while (newRec.next()) {
                    lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(newRec, newRec.sys_class_name.getDisplayValue(), newRec.getDisplayValue());
                }

                // Workspace module
                var wkModule = new GlideRecord('sys_aw_module');
                wkModule.addQuery('workspace_config', rootElem);
                wkModule.query();
                while (wkModule.next()) {
                    lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(wkModule, wkModule.sys_class_name.getDisplayValue(), wkModule.getDisplayValue());
                }
            } catch (err) {
                gs.log("Error in _processPage - " + err.name + " - " + err.message);
            }
        }

        var macroArr;

        function _getMacro(sys) {
            if (!macroArr.toString().includes(sys.toString())) {
                macroArr += ',' + sys;
                return macroArr;
            }
        }
        // when we have all of the Macro's collated we can process them to reduce chances of duplucates
        _cleanMacro(macroArr);

        function _cleanMacro(macroSYS) {
            try {
                var cleanMacArr = [];
                cleanMacArr = macroSYS.split(',');

                var cleanMac = new ArrayUtil();
                cleanMac = cleanMac.unique(cleanMacArr);

                for (var iC = 0; iC < cleanMac.length; iC++) {
                    if (cleanMac[iC].toString() != 'undefined') {
                        var furtherCheck = false;
                        // now we can process each macro we have received
                        var getMac = new GlideRecord('sys_ux_macroponent');
                        getMac.addQuery('sys_id', cleanMac[iC].toString());
                        getMac.query();
                        if (getMac.next()) {
                            if (getMac.required_translations.toString().includes('message')) {
                                _reqTranslations(getMac.sys_id.toString());
                                furtherCheck = true;
                            }

                            // we also need to process the "root element definition"
                            if (getMac.root_component.required_translation_keys.toString().includes('message')) {
                                // we need the GR so we can parse it
                                _getLib(getMac.root_component.sys_id);
                                furtherCheck = true;
                            }

                            if (furtherCheck == true) {
                                // within this macroponent, we need to check in Client Scripts also
                                var uxMCS = new GlideRecord('sys_ux_client_script');
                                uxMCS.addQuery('macroponent', getMac.sys_id.toString());
                                uxMCS.query();
                                while (uxMCS.next()) {
                                    _reqTranslations(uxMCS.sys_id.toString());
                                    // let's be safe and also process the script field
                                    lfDocumentContentBuilder.processScript(uxMCS.script, "UX Client Script", "Name");

                                    // now we need to check for Client Script Includes
                                    if (uxMCS.includes) {
                                        // we might have more than one value
                                        var uxI = uxMCS.includes;
                                        var csIarr = uxI.toString().split(',');
                                        for (var csI = 0; csI < csIarr.length; csI++) {
                                            var uxCSI = new GlideRecord('sys_ux_client_script_include');
                                            uxCSI.addEncodedQuery('sys_id', csIarr[csI]);
                                            uxCSI.query();
                                            while (uxCSI.next()) {
                                                if (!getUXCSI.toString().includes(uxCSI.sys_id.toString())) {
                                                    _reqTranslations(uxCSI.sys_id.toString());
                                                    // to be safe, lets process the script field
                                                    lfDocumentContentBuilder.processScript(uxCSI.script, "UX Client Script Includes", "Name");

                                                    // we also need to factor in whether there is use of the translate() function
                                                    var regToCheck = /\.translate\([\r\s]*(['"])(.*?)\1|\.translate\s*\((['"])(.*?)\1/gm;
                                                    var trMatches = '';
                                                    if (uxCSI.script.toString().match(regToCheck)) {
                                                        trMatches = uxCSI.script.toString().match(regToCheck);
                                                        if (trMatches) {
                                                            for (var tr1 = 0; tr1 < trMatches.length; tr1++) {
                                                                // now we need to clean the output
                                                                var justStr = /(['"])(.*?)\1/gm; // this is to get just the string
                                                                var trStr = trMatches[tr1].toString().match(justStr);
                                                                var cleanReg = /^(['"])|(['"])$/gm; // this is to remove the flanking quotes
                                                                var trClean = trStr.toString().replace(cleanReg, '');

                                                                if (!uxCLStr.toString().includes(trClean)) {
                                                                    // now we can process the string after checking for it being a potential duplicate
                                                                    lfDocumentContentBuilder.processString(trClean.toString(), "UX Client Script Includes", "Translate String");
                                                                    uxCLStr.push(trClean);
                                                                }
                                                            }
                                                        }
                                                    }
                                                    getUXCSI.push(uxCSI.sy_id.toString());
                                                }
                                            }
                                        }
                                    }
                                }

                                // we need to check "Parent Screens"
                                var getParentScreens = new GlideRecord('sys_ux_screen');
                                getParentScreens.addQuery('macroponent', getMac.sys_id.toString());
                                getParentScreens.query();
                                while (getParentScreens.next()) {
                                    _reqTranslations(getParentScreens.sys_id.toString());
                                    if (getParentScreens.required_translations.toString().includes('message')) {
                                        _reqTranslations(getParentScreens.sys_id.toString());
                                    }

                                    // we also need to check for any maroponents
                                    if (getParentScreens.macroponent != '') {
                                        _getMacro(getParentScreens.macroponent.sys_id.toString());
                                    }
                                    if (getParentScreens.parent_macroponent != '') {
                                        _getMacro(getParentScreens.parent_macroponent.sys_id.toString());
                                    }

                                    // now we need to check if the screen_type is empty
                                    if (getParentScreens.screen_type != '') {
                                        var getParentType = new GlideRecord('sys_ux_screen_type');
                                        getParentType.addQuery('sys_id', getParentScreens.screen_type.sys_id);
                                        getParentType.query();
                                        if (getParentType.next()) {
                                            lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getParentType, "UX Screen Type", "Name");
                                        }
                                    }
                                }

                                // we also need to check "Child Screens"
                                var getChildScreens = new GlideRecord('sys_ux_screen');
                                getChildScreens.addQuery('parent_macroponent', getMac.sys_id.toString());
                                getChildScreens.query();
                                while (getChildScreens.next()) {
                                    _reqTranslations(getChildScreens.sys_id.toString());
                                    if (getChildScreens.required_translations.toString().includes('message')) {
                                        _reqTranslations(getChildScreens.sys_id.toString());
                                    }

                                    // we also need to check for any macroponents
                                    if (getChildScreens.macroponent != '') {
                                        _getMacro(getChildScreens.macroponent.sys_id.toString());
                                    }
                                    if (getChildScreens.parent_macroponent != '') {
                                        _getMacro(getChildScreens.parent_macroponent.sys_id.toString());
                                    }

                                    // now we need to check if the screen_type is empty
                                    if (getChildScreens.screen_type != '') {
                                        var getChildsType = new GlideRecord('sys_ux_screen_type');
                                        getChildsType.addQuery('sys_id', getChildScreens.screen_type.sys_id);
                                        getChildsType.query();
                                        if (getChildsType.next()) {
                                            lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getChildsType, "UX Screen Type", "Name");
                                        }
                                    }
                                }

                                // now we need to process the various events on this macroponent
                                if (getMac.dispatched_events) {
                                    // we need to process each possble entry
                                    var macDis = [];
                                    macDis = getMac.dispatched_events.toString().split(",");
                                    for (var m = 0; m < macDis.length; m++) {
                                        _checkEvents(macDis[m]);
                                    }
                                }

                                if (getMac.handled_events) {
                                    var macHd = [];
                                    macHd = getMac.handled_events.toString().split(',');
                                    for (var h = 0; h < macHd.length; h++) {
                                        _checkEvents(macHd[h]);
                                    }
                                }
                            }

                            // now we need to check for any controllers
                            var macContCheck = new GlideRecord('sys_ux_controller');
                            macContCheck.addQuery('controller_macroponent', getMac.sys_id.toString());
                            macContCheck.query();
                            while (macContCheck.next()) {
                                lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(macContCheck, "UX Controller", "Name");
                                // now we need to check the UX Controller presets
                                var macContPresCheck = new GlideRecord('sys_ux_component_preset');
                                macContPresCheck.addQuery('controller', macContCheck.sys_id);
                                macContPresCheck.query();
                                while (macContPresCheck.next()) {
                                    lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(macContPresCheck, "UX Controller Preset", "Name");

                                    // there might be a macroponent we need to check
                                    if (macContPresCheck.component != '') {
                                        _getMacro(macContPresCheck.compponent.sys_id.toString());
                                    }
                                }
                            }
                        }
                    }
                }
            } catch (err) {
                gs.log('Error in _cleanMacro - ' + err.name + ' - ' + err.message);
            }
        }

        function _reqTranslations(recRT) {
            if (recRT != '' && !recRTarr.toString().includes(recRT.toString())) {
                recRTarr.push(recRT.toString()); // we need to push every record we receive into this array so we can strip duplicated at the end
            }
        }

        // once everything has been done, we can then call our array based function
        _processReqTrans(recRTarr);

        function _processReqTrans(recArr) {
            try {
                var recRTArray = [];
                recRTArray = recArr.toString().split(',');

                for (var iM = 0; iM < recRTArray.length; iM++) {
                    // we need to get the actual record so we can dot-walk to the "required_translations" field
                    var getRecMeta = new GlideRecord('sys_metadata');
                    getRecMeta.addQuery('sys_id', recRTArray[iM].toString());
                    getRecMeta.query();
                    if (getRecMeta.next()) {
                        var getActReq = new GlideRecord(getRecMeta.sys_class_name);
                        getActReq.addQuery('sys_id', getRecMeta.sys_id);
                        getActReq.query();
                        if (getActReq.next()) {
                            if (getActReq.required_translations.toString().includes('message:')) {
                                var Check = JSON.stringify(getActReq.required_translations.toString());
                                // this little check is for when the values are not necessarily stored in the JSON format we'd ideally want
                                var msgReg = /(message\:).*?(\"|\').*?(\"|\')/gi;
                                var msgMatches = Check.match(msgReg);
                                if (msgMatches) {
                                    var msgMatchArr = msgMatches.toString().split(',');
                                    for (var iMSG = 0; iMSG < msgMatchArr.length; iMSG++) {
                                        var msgMatchCl = /(message\:\s)|(?:['"])|(?:[\\])/gi;
                                        var clMsgStr = msgMatchArr[iMSG].replace(msgMatchCl, '');
                                        lfDocumentContentBuilder.processString(clMsgStr.toString(), getActReq.sys_class_name.getDisplayValue() + ' - ' + getActReq.name, 'Required Translations');
                                    }
                                }
                            } else if (getActReq.required_translations.toString().includes('message')) {
                                var rtMSGs = JSON.parse(getActReq.required_translations.toString(), function(Reqkey, Reqvalue) {
                                    if (Reqkey == 'message' && (Reqvalue != ' ' && Reqvalue != null)) {
                                        lfDocumentContentBuilder.processString(Reqvalue.toString(), getActReq.sys_class_name.getDisplayValue() + ' - ' + getActReq.name, 'Required Translations');
                                    }
                                });
                            }
                        }
                    }
                }
            } catch (err) {
                gs.log("Error in _processReqTrans - " + err.name + " - " + err.message);
            }
        }
        return lfDocumentContentBuilder.build();
    },

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

    type: 'LF_workspace'
});

 

You'll see in the comments of the code that we've prepared it for some future aspects. This is because currently the Localization Framework doesn't yet have a method to support the translation of field labels, however everything else it can do. 

 

 

Did the artifact pick up our example?

This is a good question, let's have a look:

AlexCoopeSN_6-1667914363449.png

Indeed it did. Infact, in my demo instance it identifies approximately 8000 strings for translation across the different areas in this CSM / FSM Workspace (spanning multiple application scopes).

 

Over time I'm sure we'll make some tweaks to this artifact's script (just like we did for the Portal one) but again I think it's important to share with the community how we wrote them and how important it is to understand how table hierarchies can be leveraged to write seemingly complex queries to achieve simplicity. The heavy lifting is done once, so that you can re-use that thing multiple times.

 

 

What did we learn?

This time we learned that the new Experiences are absolutely translatable, and we know how to ensure that they are.

 

Feel free to experiment with this artifact and if you have any ideas for tweaks comment down below for the benefit of the community and as always please like, share and subscribe as it always helps

 

 

Comments
Rajshekhar Pau2
ServiceNow Employee
ServiceNow Employee

A big round of applause from me. Thanks for this wonderful article.

Markus Kraus
Kilo Sage

I've built a app using a generic translation for whole scoped applications (hopefully you've built your custom workspace in a custom scoped application):
https://github.com/kr4uzi/ServiceNow-Localization-Framework-Scoped-Apps

 

You can simply go to the sys_app record of your scoped application, and a "Edit Translation" Button will appear. It will scan the whole app, and in addition it allows you to "Edit Translation" on any metadata record (tables, choices, client scripts, ...).

Alex Coope - SN
ServiceNow Employee
ServiceNow Employee

@Markus Kraus,
Interesting, but you might find some issues with that approach with a workspace like "CSM/FSM Configurable Workspace" when you have the "Dispatcher Workspace" added and "Workforce Optimization" added as well as they are all different scopes and have multiple dependencies,  

If you're about in a few weeks time (as I'm travelling), shall we organise a call to discuss it further?

Many thanks,
Kind regards 

isaac wen
Tera Expert

Hey @Alex Coope - SN ,

Sorry to revive this thread, but I was wondering if this is still the latest / best way to do Configurable workspace translation? I was asking because I tried to implement this in my test environment and ran into a whole bunch of issues. Running the UI action seems to give me a vague "Failed to request translations." error, and there are no traces of the system logs from any of the try / catch statements in your artifact code. I'll try to do some more digging tomorrow, but I was wonder if there were any obvious steps I missed. 

 

Thanks!

Alex Coope - SN
ServiceNow Employee
ServiceNow Employee

@isaac wen,

 

Yes it is. However there's a couple of things to consider (and I might update the blog post to reflect this):

1. Make sure the UI action and Artifact records are made in the "global" scope

2. After running it the first time, double check if there are any "restricted caller access" requests generated. If there are, you'll need to approve them and that should resolve the issue,

 

Many thanks,
Kind regards 

isaac wen
Tera Expert

@Alex Coope - SN 

 

Hey Alex, 

 

Thanks for the quick response, I did create everything in a global scope, but no cross-scope / restricted caller records were requested. I did notice when running "new global.LFTaskUtils().showUIAction(...)" in xplore though, that I was getting a cross-scope error, so I resolved that one by manually creating the cross scope. I still get the "Failed to request translations" error though so I will have to keep digging. 

 

EDIT: When making the script include for the artifact processor.... I forgot to copy the "extendsObject" part since I named the script something else....

 

 

 

Thanks!

Isaac

Alex Coope - SN
ServiceNow Employee
ServiceNow Employee

ah, that will do it 🙂

isaac wen
Tera Expert

@Alex Coope - SN 

 

Sorry to bother you again, but since you're the LF expert, I was wondering if it would be possible to split the task / items instead of having them all in one task.

 

Right now when I run the translation, it creates 1 task and item containing 8000+ items, which is super laggy and slow to load. I was wondering if it was somehow possible to split them into smaller tasks of, lets say 500. Looking around at the code it doesn't really seem possible with the way LF is implemented. It seems like from the UI action, it calls a post request with the sys_ids. Since we are doing it off of the ux page registry, its just 1 sys_id and the code to create the items themselves is in the function "global.LFTranslations._createLocalizationRequestedItems()", which calls directly the artifact and attaches the results to an item, so there's no way to change this without modifying the OOTB code, or without creating separate artifacts for each type of record. 

I was wondering if you had any thoughts about this? We could probably just live with 8000 items in a task I guess, it's a little slow. 

Alex Coope - SN
ServiceNow Employee
ServiceNow Employee

Hi @isaac wen,

Typically I'd suggest breaking up the artifact script into sub-level artifacts. Think a Page in a Portal vs the entirety of the Portal (which we did with the Portal artifact). The challenge with the Workspace one is that, the query to find all the dependencies used in that given Workspace is indeed very large. For example, if you were to run it on a fully loaded CSM workspace it could be as much (or even more) than 20k strings.

 

So, the challenge is, if we wanted to break it up (which is absolutely doable) where would the requests start from for those given "chunks" (let's call them)? I'm not a huge fan of making you guys run a task per macroponent (because there's thousands), or ux-screen etc (because their's thousands of those too). Hence, if we get it to run and find everything it covers the UX challenge of making sure you've got the best possible chance of picking up all the necessary strings, even if there is a very large number of them.

 

I do very much get (and agree) with the performance issue. Which to be fair is only really an issue for this artifact and the Portal artifact purely because of how many strings they are pulling, but (without giving anything away) we are working on something to mitigate that in a future release ðŸ˜‰,


Many thanks,
kind regards 

isaac wen
Tera Expert

Hey @Alex Coope - SN 

It seems like when I try to publish my translations, the update set that is created saves my changes as "deleted". Any idea of why this is happening? 

 

isaacwen_0-1733242382036.png

 

Alex Coope - SN
ServiceNow Employee
ServiceNow Employee

hi @isaac wen

that's interesting, are you able to confirm in the XML if it's a specific translation record?

isaac wen
Tera Expert

@Alex Coope - SN 

 

Yeah it's part of the LF task update set, and contains the correct translation value, however its a delete instead of insert or update. I did notice it's for a sys_ui_message, but that should work correctly too. 

isaacwen_0-1733243980710.png

 

honamiUeo
Tera Contributor

@Alex Coope - SN 

 

Thank you for sharing the detailed information.

Let me confirm one thing.
When I want to create a translation label for the component label below, how do I find the key?

honamiUeo_0-1737425940472.png
I created the translation record shown below, but it didn't work.

honamiUeo_1-1737426102141.png

 


Thank you in advance.

Alex Coope - SN
ServiceNow Employee
ServiceNow Employee

Hi @honamiUeo,

Have you tried running the LF artifact to see if it's correctly identifying the string from the component? I also don't see "translatable turned on" in your screenshot, so it might be that specific string isn't the one to be translated. Have a look at my first screenshot at the top of this blog post to see what I mean (the one with the green line), Ultimately, you shouldn't need to add the translations manually as that's the purpose of the artifact,

Many thanks,
Kind regards

Hardik10
Tera Expert

@Alex Coope - SN , thanks for the article. 

One basic question, this won't be a onetime activity. There will be new additional in terms of messages, fields, buttons, etc. what do you propose to keep this automatically trigger translation tasks only for missing translation objects? Also, in current solution, does it overwrite the translation or is it checking if it is already translated?

Alex Coope - SN
ServiceNow Employee
ServiceNow Employee

Hi @Hardik10,

So there's a couple of things to unpack with your question there. I'll split it into two parts:

  • Workspaces are typically store apps (with a few minor exceptions), so assuming you are translating into Languages we don't provide a Language Pack for, then my suggestion would be to run this either after a Family Release upgrade or Store App update as that's when new source strings would be introduced. I would not suggest any automation of the creation of these tasks, due to the size and volume we're talking about here, plus it would be very difficult to know under what condition to correctly trigger the task automatically.
  • As with any Localization Framework artifact, they identify all translatable text. If a translation already exists in the instance, it is typically locked to prevent it from accidentally being overwritten. This behaviour is managed at the Localization Framework level, where as the source text identification is from the "artifact" level.

 

Many thanks,
kind regards

Hardik10
Tera Expert

Thanks @Alex Coope - SN . 

For employee center pro portal scenario, how to handle it. The user will continue to create topics, quick links, mega menu items, etc.

On another article, you have explained translation for the portal with hierarchy, but when new topic or taxonomy added, how to detect only that and use translation framework to keep translating the content.

Alex Coope - SN
ServiceNow Employee
ServiceNow Employee

@Hardik10,

That's probably a question for that thread (for topic consistency) 😉 

The idea of the Portal artifact, is that you can copy out sections of it for more narrow focused artifacts if necessary. For example, you could absolutely make a "taxonomy" + "topic" artifact using the code in the Portal level artifact, same for the Theme if you wish to cover the menus etc.

It was always my intention to have:
- Portal artifact for the bulk scenario of picking up as much of the portal as possible,
- Page level artifact for the ad-hoc page or pages not picked up from the Portal artifact,

- Theme for theme changes or updates (the code for this is in the Portal artifact, just copy it and define a new one on the Theme table),

- Taxonomy for updates to the taxonomy (the code for this is in the Portal artifact, just copy it and define a new one on the taxonomy table)

etc

 

 

Many thanks,

kind regards

Hardik10
Tera Expert

thanks @Alex Coope - SN . That I understand that I can make more focused artifacts. What I am trying to achieve is to run some schedule job to translate the content e.g. Topics records, but it should pick up only the one which are not yet translated, how to get that delta. There would be a many contents type where we can auto translate and auto-publish without human review, but I need delta records to make it more efficient. 

Alex Coope - SN
ServiceNow Employee
ServiceNow Employee

hi @Hardik10,

This is definitely turning into a topic for it's own thread. We don't recommend generating tasks from scheduled jobs because it could prove problematic for a number of reasons, also it's not best-practice to follow only "delta's" as that would miss scenarios where the english source has changed leading to unidentified (and now) mistranslated strings. Artifacts pick up everything that can be translated regardless of it's state, this is to ensure they can be correctly reviewed and validated should anything have changed,

 

In this scenario, our suggestion would be to create a Taxonomy level artifact (using the same query from the Portal artifact) and run it on the [taxonomy] table to find all the "topics" associated to that taxonomy, at a time of need (e.g. once the taxonomy has been updated and maybe is in UAT). There is no harm generating new tasks frequently (e.g. weekly etc) as part of a structured and specified process.

 

Typically once Development of something has been complete, that's when translations would be requested, then during the UAT phase that's when the translations can also be tested and validated in the system,

Many thanks,
kind regards

 

 

Hardik10
Tera Expert

@Alex Coope - SN , I agree with this for some of the content type or artifact. For workspace, it is making sense. But, topic or quick links are added directly to the production instance. So, those will not follow usual cycle of UAT. Also, for this type of content, I would like to setup auto-translate and auto-publish, so basically no human review. 

I also noticed that ServiceNow doesn't detect change in the source text and mistranslated strings will occurs. 

Version history
Last update:
‎02-18-2025 06:53 AM
Updated by: