- Post History
- Subscribe to RSS Feed
- Mark as New
- Mark as Read
- Bookmark
- Subscribe
- Printer Friendly Page
- Report Inappropriate Content
02-26-2024 12:54 AM - edited 08-12-2025 01:21 AM
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.
- Easy - (LF Artifact) - Extending the Localization Framework to Portal Announcements
- Easy - (LF Artifact) - Translating Surveys and Assessments
- Easy - (LF Artifact) - Need to translate a Guided Tour? - check this
- Medium - (LF Artifact) - Need to translate a PA Dashboard? - check this
- Medium - (LF Artifact) - Need to translate you Office / Building / Indoor maps? - check this
- Complex - (LF Artifact) - Need to translate a portal - check this
- Complex - (LF Artifact) - Need to translate a configurable workspace? - check this
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:
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:
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:
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:
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:
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):
And if we were to go through and perform the translations as necessary and publish them, we should have an end result like this:
* 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
- 2,958 Views
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
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?
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
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