logo

NJP

Need to translate a configurable workspace? - check this

Import · Nov 09, 2022 · article

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 :slightly_smiling_face: ), 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 * * @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 **********/ // 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 * * @Param 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

View original source

https://www.servicenow.com/community/international-localization/need-to-translate-a-configurable-workspace-check-this/ta-p/2376015