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

Alex Coope - SN
ServiceNow Employee
ServiceNow Employee

A little while ago we prototyped our "PA Dashboard" artifact. For a time it was great (and it still is for those types of dashboards) but now with things moving on in the platform and leaning more towards "PAR Dashboards", it's time to revisit this subject.

 

If you haven't read my other LF Artifact blog posts, check them out below before continuing as this post will 
go into some fairly technical concepts.


Note - They progress in levels of difficulty, this one I would say is in between medium and complex.

 

 

PAR dashboards, what are they?

So a few short releases ago, as the new "components" in our UI framework called "Siesmic" became more and more expanded (learn more about siesmic here) it became needed that we revisited how dashboards are shown. The idea of workspace landing pages required them but also to be more flexible than the dashboards of old.

 

Fun fact, the first landing page you see in "Admin Center" is actually a "PAR Dashboard", which you can easily identify because of the "edit" option on the right hand side of the page:

AlexCoopeSN_0-1708935779582.png

 


The question then becomes - "How do we find the strings we want to translate?" and this is a very good question.

 

 

The structure

With PAR dashboards, there are actually a few super useful tables that hold what we need which we'll be able to query, but there are also some that need a bit more nuance, usually because they are stored or related to a seismic component.

 

You'll also find that many are directly related to a "Next Experience", such as "Platform Analytics" or a specific workspace - more on that in a bit.

 

So, our artifact will need to be defined on the [par_dashboard] table, because this will be the top of our pyramid (so to speak) then we'll need to be querying the following tables:

  • [par_dashboard_tab] - to give us the tabs of our dashboard
  • [par_dashboard_canvas] - the JSON has some super important values we may need
  • [par_dashboard_widget] - shows the data we care about
  • [par_dashboard_visibility] -  this will tell us if it's being used and active

and there are a few more.

 

 

The Set-up

The first thing we need to do is create our artifact just as with the others:

AlexCoopeSN_1-1708936166196.png

As with all the other artifacts, it's always best to make these in the Global scope and allow it permissions into all scopes, it will make things a lot simpler in the long run.

 

Let's make a note of the "internal name" value, as we'll need this for the UI action later.

 

The Processor Script will be set up as follows:

AlexCoopeSN_2-1708936272784.png

 

And the script for it is here:

 

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

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

        try {
            var tableName = params.tableName;
            var sysId = params.sysId;
            var language = params.language;
            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()) {
                // we also need to process the actual dashboard
                lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getRec, "PAR Dashboard", "Dashboard");

                // we also need to check if we've got any specific Dashboards for this workspace
                var parDashCheck = new GlideRecord('par_dashboard_visibility');
                parDashCheck.addEncodedQuery('dashboard=' + getRec.sys_id);
                parDashCheck.query();
                while (parDashCheck.next()) {
                    // 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()) {
                        // 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 any translatable fields
                            lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getDashWid, "PAR Dashboard Label", "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", "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, "PA Indicator", "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, "PA Indicator Breakdown", "Name");

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

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

            }

			// now we need to see if there's a dashboard cache in this language and purge it
			var checkDashCache = new GlideRecord('par_dashboard_cache');
			checkDashCache.addEncodedQuery('dashboard='+sysId);
			checkDashCache.addQuery('language', language.toString());
			checkDashCache.query();
			if(checkDashCache.next()){
				// we have one, so we need to delete it. It will be recreated when the first next user in that language navigates to it
				checkDashCache.deleteRecord();
			}
        } catch (err) {
            //gs.log("Error in LF_PARdashboard" + err.name + " - " + err.message);
        }
        return lfDocumentContentBuilder.build();
    },

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

    type: 'LF_PARdashboard'
});

 

 

Please remember that this is just another "proof of concept", so your mileage may vary, however I do ask that if you have issues with it picking things up to let me know in the comments below, so we can review and tweak as and where necessary.

 

That just leaves the UI action:

AlexCoopeSN_3-1708936558798.png

AlexCoopeSN_4-1708936597334.png

 

I've highlighted in the condition the "internal name" of our artifact so that you can see when it's called.

 

Once you've set it up, don't forget to also make a new setting in the Localization Framework because if one is missing or not applied to "all artifacts" then you won't be able to use and won't be able to select a language to translate to. It needs to know what to do 😉.

 

 

When to use this artifact

So, when should this artifact be used? Well, it's mainly for times where someone has made and shared a PAR dashboard, such as a personal one that ends up being used for a team and such like.

 

If however you are using PAR Dashboards within a given workspace then I would highly recommend using the updated "Workspace artifact" as that should in theory pick up all of them associated to that given workspace (think HR, Admin Center, CSM, Platform Analytics Workspace / Experience, etc etc). This is also true if you want to blanket translate all PAR dashboards that are used in the "Platform Analytics Workspace / Experience" then I would also use the Workspace level artifact as it should pick them up there.

 

But does this one actually work? Let's go back to our landing page example, the "Shared Admin Dashboard" and request a translation of that, once we've found it in the [par_dashboard] table. I'm going to use Japanese as an example:

AlexCoopeSN_5-1708937028965.png

 

And once we go into the task and through to the comparison UI we should sections that look like these (they will be expanded when you first see them):

AlexCoopeSN_6-1708937121291.png

 

And if we were to go through and perform the translations as necessary and publish them, we should have an end result like this:

AlexCoopeSN_7-1708937336630.png

* Please forgive any mistranslations, these were performed through AzureMT for demo purposes.

 

 

Summary

And there you have it. Here is another artifact for our awesome community to leverage for your needs. You can see it's absolutely possible to translate "PAR Dashboards" using the Localization Framework using the same methods, processes you use for other areas of the platform.

 

And please do let us know how you get on with this one, feedback is always welcome.

 

 

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

 

 

 




 

Comments
timjeoung
Tera Contributor

Hi Alex, thanks for this awesome article.

Is it also possible to apply this to Inherent Assessment (sn_risk_advanced_inherent_assessment)? I've noticed that localization processor scripts differ for each area, such as catalog items, dashboards, and Guided Tours.

Therefore, I think to apply this to Inherent Assessment, the Inherent Assessment artifact will need its own specific processor script. If yes, what would be the best approach to do this? 

Alex Coope - SN
ServiceNow Employee
ServiceNow Employee

hi @timjeoung,

Yes you are quite right, in that each concept you want to translate requires a different query, hence the different "Processor Scripts" as the structures are very different,

 

Being honest, I haven't yet looked at "Inherent Assessments" so I'll check, but this is rapidly turning into it's own topic of discussion for a new thread 🙂

 

Depending on how it's structured, there could be two options:
1. If it's within the same schema as an existing artifact then it could just be an extra loop with some conditional checks (much like the prototype Portal and Workspace artifacts)

2. If it's completely different, then a fresh processor script specific for this need at the top of it's schema structure which might indeed be the [sn_risk_advanced_inherent_assessment] table (assuming the strings are not picked up any other way),

 

Many thanks,

kind regards

Version history
Last update:
‎08-12-2025 01:21 AM
Updated by:
Contributors