- Post History
- Subscribe to RSS Feed
- Mark as New
- Mark as Read
- Bookmark
- Subscribe
- Printer Friendly Page
- Report Inappropriate Content
03-21-2023 03:33 AM - edited 03-21-2023 11:13 AM
This past week, I had an interesting question from one of our SC's. He's working with one of our Customers and they would like to be able to translate some of their Dashboards into other Languages. Immediately I started thinking - what type of dashboard, what languages, how could we identify all the different strings. Before I knew it, I had started to map out some of the table hierarchies involved. And a few short hours later you guessed it, we had another prototype Localization Framework artifact.
If you haven't read my other LF Artifact blog posts, check them out here before continuing as this post will go
into some fairly technical concepts:
Easy - Extending the Localization Framework to Portal Announcements
Easy - Need to translate a Guided Tour? - check this
Medium - Translating Surveys and Assessments
Complex - Need to translate a portal - check this
Complex - Need to translate a configurable workspace? - check this
Note - They progress in levels of difficulty, this one is between Medium and Complex.
Dashboards come in different styles.
When it comes to dashboards, the top of the data pyramid we're going to focus on here is the entries in the [pa_dashboards] table. From here, there are basically two types of Dashboard. Those with a portal page and those without.
It is important to note, that there are many various Child and grand-Child records that we need to follow, which can sometimes go as far as cubes, widgets, breakdowns and their respective sources.
For the sake of this prototype, we tested against two dashboard examples from the demo data:
- "IT Manager"
- "Service Catalog Overview"
This is because these two dashboards exemplify the idea of these two different type of dashboards we are trying to focus on.
- There are others, but for now we're just focussing on these.
After mapping out the relationships and understanding how a dashboard knows what report to show and where, we have the skeleton of our query.
The Configuration.
Skipping ahead a little bit (to save time), we are going to define our "Request Translation" UIAction on that [pa_dashboards] table, and it will look like this:
I've highlighted the "internal name" that we're going to give to our Artifact (proto_pa_dashboards) - this will be important next:
Our Artifact will be called like this:
And our Processor script will be this:
var LF_PAdashboards = Class.create();
LF_PAdashboards.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 need to kow what is related to the dashboard we're requesting to be translated
var tableName = params.tableName;
var sysId = params.sysId;
var language = params.language;
var lfDocumentContentBuilder = new global.LFDocumentContentBuilder("v1", language, sysId, tableName);
var pageArr = [];
try {
// let's get this dashboard's record
var getDash = new GlideRecord('pa_dashboards');
getDash.addQuery('sys_id', sysId);
getDash.query();
if (getDash.next()) {
lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getDash, "Dashboard", getDash.getDisplayValue());
// we have the dashboard, no we need to check tabs and breakdown sources
var tabMap = new GlideRecord('pa_m2m_dashboard_tabs');
tabMap.addEncodedQuery('dashboard.sys_id = ' + getDash.sys_id);
tabMap.orderBy('order');
tabMap.query();
while (tabMap.next()) {
var getTabs = new GlideRecord('pa_tabs');
getTabs.addQuery('sys_id', tabMap.tab.sys_id);
getTabs.query();
if (getTabs.next()) {
// now we need to process the "page" of this tab
lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getTabs, "Tab", getTabs.getDisplayValue());
_getPage(getTabs.page.sys_id);
}
}
// we need to get the Breakdown sources for this dashboard as well
var breakMap = new GlideRecord('pa_m2m_dashboard_sources');
breakMap.addEncodedQuery('dashboard.sys_id=' + getDash.sys_id);
breakMap.orderBy('order');
breakMap.query();
while (breakMap.next()) {
var getBreakdownSource = new GlideRecord('pa_dimensions');
getBreakdownSource.addQuery('sys_id', breakMap.breakdown_source.sys_id);
getBreakdownSource.query();
if (getBreakdownSource.next()) {
lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getBreakdownSource, "Breakdown", getBreakdownSource.getDisplayValue());
// now we need to go down into each breakdown
var getBreakdown = new GlideRecord('pa_breakdowns');
getBreakdown.addEncodedQuery('dimension.sys_id=' + getBreakdownSource.sys_id);
getBreakdown.query();
while (getBreakdown.next()) {
lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getBreakdown, "Breakdowns", getBreakdown.getDisplayValue());
// Breakdown Mappings
var paBreakMap = new GlideRecord('pa_breakdown_mappings');
paBreakMap.addEncodedQuery('breakdown.sys_id=' + getBreakdown.sys_id);
paBreakMap.query();
while (paBreakMap.next()) {
lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(paBreakMap, "Breakdown Mapping", paBreakMap.getDisplayValue());
}
// Breakdown Relations
var paBreakRel = new GlideRecord('pa_breakdown_relations');
paBreakRel.addEncodedQuery('breakdown.sys_id=' + getBreakdown.sys_id);
paBreakRel.query();
while (paBreakRel.next()) {
lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(paBreakRel, "Breakdown Relations", paBreakRel.getDisplayValue());
}
// Indicators
var getIndicatorBreakdown = new GlideRecord('pa_indicator_breakdowns');
getIndicatorBreakdown.addEncodedQuery("breakdown.sys_id=" + getBreakdown.sys_id);
getIndicatorBreakdown.query();
while (getIndicatorBreakdown.next()) {
// we shouldn't need to follow the Breakdown Source as it should already be covered by a previous query
// we need to check for the indicators and the cubes
var getIndicator = new GlideRecord('pa_indicators');
getIndicator.addQuery('sys_id', getIndicatorBreakdown.indicator.sys_id);
getIndicator.query();
if (getIndicator.next()) {
lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getIndicator, "Indicator Breakdown", getIndicator.name+' - '+getIndicator.getDisplayValue());
// lets check the "Indicator Source"
var getIndSource = new GlideRecord('pa_cubes');
getIndSource.addQuery('sys_id', getIndicator.cube.sys_id);
getIndSource.query();
if (getIndSource.next()) {
lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getIndSource, "Indicator Source", getIndSource.name+' - '+getIndSource.getDisplayValue());
}
}
}
}
}
}
}
// we need to get some standard strings
lfDocumentContentBuilder.processString('No data to display', "Standard Messages", "Message");
lfDocumentContentBuilder.processString('Name', "Standard Messages", "Message");
lfDocumentContentBuilder.processString('Score', "Standard Messages", "Message");
lfDocumentContentBuilder.processString('Trend', "Standard Messages", "Message");
lfDocumentContentBuilder.processString('Breakdowns', "Standard Messages", "Message");
lfDocumentContentBuilder.processString('Records', "Standard Messages", "Message");
lfDocumentContentBuilder.processString('Scorecard', "Standard Messages", "Message");
lfDocumentContentBuilder.processString('Loading...', "Standard Messages", "Message");
} catch (err) {
gs.log('Error in getDash - ' + err.name + ' - ' + err.message);
}
function _getPage(pageSYS) {
try {
var portalPage = new GlideRecord('sys_portal_page');
portalPage.addQuery('sys_id', pageSYS);
portalPage.query();
if (portalPage.next()) {
lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(portalPage, "Portal Page", portalPage.getDisplayValue());
// now we need to go through the dropdowns
var getPortalDropzones = new GlideRecord('sys_portal');
getPortalDropzones.addEncodedQuery('page.sys_id=' + portalPage.sys_id);
getPortalDropzones.query();
while (getPortalDropzones.next()) {
lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getPortalDropzones, "Portal Dropzones", getPortalDropzones.getDisplayValue());
// now we need to get the title of the report on the dashboard
var getTitle = new GlideRecord('sys_portal_preferences');
getTitle.addEncodedQuery('portal_section.sys_id=' + getPortalDropzones.sys_id);
getTitle.addQuery('name', 'sys_id');
getTitle.query();
while (getTitle.next()) {
// we need to get the exact report title
var getRep = new GlideRecord('sys_report');
getRep.addQuery('sys_id', getTitle.value.toString());
getRep.query();
// we also need to check pa_widgets if we don't have a report
if (!getRep.hasNext()) {
var getPaWidget = new GlideRecord('pa_widgets');
getPaWidget.addQuery('sys_id', getTitle.value.toString());
getPaWidget.query();
if (getPaWidget.next()) {
if (getPaWidget.type != "process") {
lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getPaWidget, "PA Widget", getPaWidget.getDisplayValue());
// we need to now go into the "Indicator"
var getWidInd = new GlideRecord('pa_indicators');
getWidInd.addQuery('sys_id', getPaWidget.indicator.sys_id);
getWidInd.query();
while (getWidInd.query()) {
lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getWidInd, "PA Widget Indicator", getWidInd.getDisplayValue());
}
} else {
// this is for Workbench / Process types
var getMainWidInd = new GlideRecord('pa_widget_indicators');
getMainWidInd.addQuery('widget', getPaWidget.sys_id);
getMainWidInd.query();
while(getMainWidInd.next()){
lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getMainWidInd, "Main Widget Indicator", getMainWidInd.getDisplayValue());
// now we also need to get the Element of these
// these are choice entries - will need to create a Processor Script for these
// we need to get the Supporting Widget Indicators
var getSupWidInd = new GlideRecord('pa_widget_indicators');
getSupWidInd.addQuery('widget_indicator', getMainWidInd.sys_id);
getSupWidInd.query();
while(getSupWidInd.next()){
lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getSupWidInd, "Supporting Widget Indicators", getSupWidInd.getDisplayValue());
}
}
}
}
}
// if we do have a report
if (getRep.next()) {
lfDocumentContentBuilder.processString(getRep.title.toString(), "Report Title", getRep.getDisplayValue());
}
}
}
}
} catch (err) {
gs.log('Error in _getPage - ' + 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_PAdashboards'
});
Please remember that this is a prototype, and as such it almost certainly won't pick up everything on a Dashboard. For example, it won't pick up field labels and choices used in graphs, however you can use it to get you the majority of the way there.
It's also fair to say that the code could be significantly optimised with more time. Essentially the idea with the artifact is to start from the very top table where all the relationships from a dashboard begin, then follow each and every "Related List" until no more are found.
Did it work?
So, let's look at the "IT Manager" dashboard. Here it is in English:
Here is an LF task of it being translated into Japanese:
And here is the Dashboard translated into Japanese:
What have we learned?
So we can see that it is indeed possible to translate the majority of a dashboard with an artifact, and it got us the majority of the way there.
Now you have the choice of whether you want to translate all of your dashboards, or just the ones shared across user groups - that might be quite a big decision for some of you out there.
Let me know in the comments if you have any ideas for tweaks / improvements based on your testing as it will benefit the community. And feel free to DM / email me any suggestions for more artifacts my team can potentially prototype.
And if there's enough asks, we might bundle our prototypes into an update-set on the store. We've even been working on one for the mobile apps 😉
And as usual, please like, share and subscribe as it always helps
- 5,514 Views

- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Great article, Alex! I see in your example you used a UI16 dashboard. As customers are being nudged to migrate those over to highly configurable Seismic/Next Experience Dashboards, I’m curious: Does this work for Next Experience dashboards as well?
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
@Travis Rogers,
That's a good question, because Siesmic/Next Experiences are a bit different in their structure. So, you could leverage our prototype workspace Artifact to get the majority of the UI in say "Platform Analytics Workspace" translated, but it probably won't pickup all of the reports and dashboards in it,
I'll add it to our list 👍, but feel free to test out the Workspace artifact to see how much it identifies in your use-case,
Many thanks,
kind regards
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Thanksfor sharing it ca it translate into UAE language.
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Hi Alex,
Thank you so much for the article. I have a question: how can we get the 'Edit Translation' button on the dashboard page? Currently, I can only see the 'Request Translation' button, which is used for localization. However, I prefer the 'Edit Translations' option for making minor changes and easily checking for missing translations.
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
@timjeoung,
I'm afraid currently the "edit translations" feature is only available for the Catalog Items artifact ootb as it requires some very specific set up. I will however feedback it back to raise in our next strategy discussions,
Many thanks,
kind regards