logo

NJP

Need to translate a form? - check this

New article articles in ServiceNow Community ยท Mar 17, 2026 ยท article
This is a brand-new prototype artifact, on the **medium** to **hard** level of complexity.

 

When I first started dabbling with translations in the platform (over 12 years ago now) I actually thought that field labels and choices were the easiest of the areas to translate. Mainly because back in those days, it was fairly simple to make an extract of [sys_documentation] and [sys_choice] based on the table I was looking to translate - then there's a dark art of "field annotations" (which I still have nightmares of to this day).

 

Then we launched the Localization Framework in the  Quebec release. However, it had a bit of a gap, in that when it first launched, there weren't usable API's for field labels and choices. 

 

Today, that changes! I'm here to tell you, that as of  Yokohama Patch 11Zurich Patch 7 and the upcoming  Australia releases, it now does! ๐Ÿค—.

 

I know many of you have waited a long time for this day (just like I have).

 

 

That's all well and good....

I hear you. It's great that there's now 2 new API's, but the question is how do we use them?

 

Well, I've done the hard part for you - you should know me by now ๐Ÿ™‚.

 

So, without any further delay, let's break down a brand new POC artifact I've put together on how to translate a Form - this will work both in CoreUI (aka UI11-UI16) and a Workspace form (because the underlying architecture is actually the same).

 

 

The UI Action

As usual with LF artifact's, lets define the UI action:

UI Action.png

 

For those who've followed my other artifact POC's, the "internal" name I'm using will be "lf_forms", and the table we're defining the artifact on will be the [sys_ui_form] table.

 

 

 

The Artifact record

artifact.png

 

 

The Script

I'm not going to delay this part any longer:

var LF_Forms = Class.create(); LF_Forms.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 **********/ var tableName = params.tableName; var sysId = params.sysId; var language = params.language; var groupName = ''; var lfDocumentContentBuilder = new global.LFDocumentContentBuilder("v1", language, sysId, tableName); var fieldList = []; try { // 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.addQuery('sys_id', sysId); wForm.query(); if (wForm.next()) { // this makes our query a bit more efficient, as we don't want to care what view, as any view needs translating. var tableQ = new GlideRecord(wForm.name.toString()); tableQ.setLimit(1); tableQ.query(); if (tableQ.next()) { var gFields = new GlideRecordUtil(); fieldList = gFields.getFields(tableQ); // we don't even to query the table to get fields, we can just pull all the fields on the table as it's more efficient // we now need to loop through the fieldList and check if they are for this table for (var i = 0; i < fieldList.length; i++) { var getField = new GlideRecord('sys_documentation'); getField.addEncodedQuery('name=' + wForm.name.toString() + 'ORname=' + tableQ[fieldList[i].toString()].getBaseTableName()); getField.addEncodedQuery('element!=NULL'); getField.addQuery('element', fieldList[i].toString()); getField.addQuery('language', gs.getProperty('glide.sys.language')); getField.orderBy('label'); getField.query(); while (getField.next()) { lfDocumentContentBuilder.processTranslatableFieldsForSysDocumentation(getField, "Field Label - " + getField.name.getDisplayValue() + " - " + getField.getDisplayValue(), "name"); lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getField, 'Fields - ' + wForm.name.toString(), getField.getDisplayValue()); // now we need to find the right choice fields var getChoiceField = new GlideRecord('sys_choice'); getChoiceField.addEncodedQuery('name=' + getField.name); getChoiceField.addQuery('element', getField.element); getChoiceField.addQuery('language', gs.getProperty('glide.sys.language')); getChoiceField.orderBy('sequence'); getChoiceField.query(); while (getChoiceField.next()) { lfDocumentContentBuilder.processTranslatableFieldsForSysChoice(getChoiceField, "Choices - " + getField.name.getDisplayValue() + " - " + getField.getDisplayValue(), "name"); } } } // now we need to get the form sections var formSec = new GlideRecord('sys_ui_form_section'); formSec.addEncodedQuery('sys_ui_form.name=' + wForm.name.toString()); formSec.orderBy('position'); formSec.query(); while (formSec.next()) { // we're going to process any and all for this table, because it makes the most sense to not have to worry about each individual "view" // 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, "Form Section - " + wForm.name.toString(), "name"); // We could process fields here, by looping through what's on each form, but this would be inefficient (compared to what we do above) and risks missing a potential field on the table that doesn't happen to yet be on any form view. // Field labels are actually covered by our loop to [sys_documentation] // we do however need to find each of the annotations to process those var getAnnoEl = new GlideRecord('sys_ui_element'); getAnnoEl.addEncodedQuery('typeINannotationsys_ui_section=' + formSecCheck.sys_id); getAnnoEl.query(); while (getAnnoEl.next()) { // now we need to take the "element" value, and query the [sys_ui_annotation] table var getAnno = new GlideRecord('sys_ui_annotation'); getAnno.addEncodedQuery('sys_id=' + getAnnoEl.element.toString()); getAnno.query(); // there will only be one per if (getAnno.next()) { if (getAnno.text.toString().includes('getMessage')) { lfDocumentContentBuilder.processScript(getAnno.text, "Annotation - " + wForm.name.toString(), formSecCheck.caption.toString()); // there are some use-cases the call getMessage } else { lfDocumentContentBuilder.processString(getAnno.text, "Annotation - " + wForm.name.toString(), formSecCheck.caption.toString()); // there are some use-cases that don't need to call getMessage } } } } } // now we need to look for UIactions / Buttons var getActsBut = new GlideRecord('sys_ui_action'); getActsBut.addQuery('active=truetable=' + wForm.name.toString()); getActsBut.query(); while (getActsBut.next()) { // process the actions lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getActsBut, "UI Action", "Name"); lfDocumentContentBuilder.processScript(getActsBut.script, "UI Action", "Script"); // check if it's a Workspace button if (getActsBut.form_button_v2 == 'true') { lfDocumentContentBuilder.processScript(getActsBut.client_script_v2, "UI Action", "Workspace Button Script"); } } // now we need to look for any Process Flow Formatters var getProcessFlow = new GlideRecord('sys_process_flow'); getProcessFlow.addEncodedQuery("active=truetable=" + wForm.name.toString()); getProcessFlow.orderBy('order'); getProcessFlow.query(); while (getProcessFlow.next()) { lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getProcessFlow, "Process Flow Stages", "Name"); } // now we need to look for any "steppers" - in Workspaces. var getSteps = new GlideRecord('sttrm_state'); getSteps.addEncodedQuery('initial_state=truesttrm_model.table_name=' + wForm.name.toString()); getSteps.query(); while (getSteps.next()) { lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getSteps, "Process Steps", "State Label"); // we also need to get the Model var getStepModel = new GlideRecord('sttrm_model'); getStepModel.addQuery("model", getSteps.sttrm_model.sys_id); getStepModel.query(); if (getStepModel.next()) { lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getStepModel, "Process Steps", "Model"); } } // now we need to check for Client-scripts var getCS = new GlideRecord('sys_script_client'); getCS.addEncodedQuery('table=' + wForm.name.toString() + 'ORtableIN' + wForm.name.toString() + ',taskactive=true'); getCS.query(); while (getCS.next()) { lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getCS, "Client Script", getCS.name.toString()); lfDocumentContentBuilder.processScript(getCS.script, "Client Script", getCS.name.toString()); } // now we need to get any BR's on this table, just in-case they present a message to the fulfiller. var getBR = new GlideRecord('sys_script'); getBR.addEncodedQuery('collection=' + wForm.name.toString() + 'active=true'); getBR.query(); while (getBR.next()) { lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getBR, "Business Rule", getBR.name.toString()); // this should cover if there's a static message populated in the 'message' field when "add_message" is true lfDocumentContentBuilder.processScript(getBR.script, "Business Rule", getBR.name.toString()); } // now we need to check for UI policies var getPol = new GlideRecord('sys_ui_policy'); getPol.addEncodedQuery('table=' + wForm.name.toString() + 'active=truerun_scripts=true'); getPol.query(); while (getPol.next()) { lfDocumentContentBuilder.processScript(getPol.script_true, "UI Policy - True ", getPol.short_description.toString()); lfDocumentContentBuilder.processScript(getPol.script_false, "UI Policy - False", getPol.short_description.toString()); } } } return lfDocumentContentBuilder.build(); } catch (err) { gs.log('Error in LF_forms - ' + err.name + ' - ' + err.message); } }, /********** * Uncomment the saveTranslatedContent function to override the default behavior of saving translations * * documentContent LFDocumentContent object * @return **********/ /********** saveTranslatedContent: function(documentContent) {}, **********/ type: 'LF_Forms' });

 

There are a few very important notes about this artifact:

  • It has to be defined in the "global" scope
  • It's intended to not only find Field Labels but also Choices in choice fields (assuming they are choice fields) - if it misses any, it's likely because it's a "string" field with choices - I'll do an update for these in the near future.
  • It's intended to identify Form sections
  • It's intended to identify Annotations
  • It's intended to identify UI actions

    • It's intended to find any getMessage calls in their scripts also
  • It's intended to identify Process Flow stages

  • It's intended to identify Process Flow Steppers' steps

  • It's intended to identify getMessage calls in Client Scripts

  • It's intended to identify getMessage calls in Business Rules

  • It's intended to identify getMessage calls in UI Policies (true and false) scripts

All of the above is form View agnostic. In that, it's deliberate to not focus on a singular Form View to reduce the chance of missing something or increasing the workload unnecessarily. You can of course change that, but I wouldn't recommend it. 

 

 

What does it look like?

Here's an image of what a field label to be translated would look like in the Comparison UI:

fields.png

 

Here's an image of what a bunch of "choices" will look like in the Comparison UI:

choices.png

 

Here's what everything else (after field labels) will look like in the Comparison UI:

everything else.png

 

To prove that it works, this is a Canadian First Nation language called "Inuktitut" (I apologize in advance to any native speakers if the quality is not good, I'm using my typical MS DT flow here):

form translated.png

^ this is the [change_request] form on my ZP7 instance. The very same instance I use for Demo's, workshops, customer meetings, events etc. If you've seen any of my webinars etc, it's that same instance.

 

Everything you see above was translated using this Artifact and only this Artifact.

 

If using the _Localization Workspace_ with this artifact, don't forget to add the Cross Scope privileges ("read"   
for the table, and "execute" for the API.  

Also - in LW, it might take a few minutes in Stage 3 (scope) to generate the list of what to select. This is to   
be expected, it's because the [sys\_ui\_form] table has a few thousand entries (which is a better alternative   
to [sys\_db\_object]. Once you deselect what you don't want (table wise), you can then search for what you   
do want and you **only** need one entry for that table - you will see multiple because of form views, as that's   
what that table is holding.

 

 

This is a Prototype

As with the other extended artifacts that my team put together, this is a prototype. I can't promise it's going to be perfect in this first iteration so please try it, let me know (in this thread) if there's an area that doesn't quite work as you expect (e.g. it misses something), or if you'd like to have something added.

 

For example:

  • I purposefully haven't included "Related Lists". This is for a few reasons, mainly because it could lead to an endless loop - my suggestion would be to run a task per form of the Related Lists you want to cover.
  • I purposefully haven't included this in the Workspace artifact because it will lead to significant performance degradation when running such a large query at once. Internally we're going to look at how to break-up such scenarios, maybe trigger cascading artifacts based on when certain calls are met / triggered. But that's something for the future. Right now, if you want to fully translate a Workspace, you would need to run the Workspace artfact on that Workspace, then this Forms artifact per Form in that Workspace (and it's corresponding Related Lists that lead for a form view.). 
  • This purposefully is View agnostic. It just looks at what's on the table and where-necessary it will try to also find base table fields to take into account inheritance behaviour.

    As with the other POC artifacts - I have to say that there is no support or warranties provided.

 

 

Summary

So, we are now able to cover all 5 of the translation tables with the Localization Framework and _Localization Workspace, _which means, the library of Artifacts is now able to grow even more.

 

And yes, I'm already sketching out another one, but let me know what you would like to see next and we'll see what we can do.

 

If you found this helpful and useful, please like, share and subscribe as it always helps.

 

 

 

 

View original source

https://www.servicenow.com/community/international-localization/need-to-translate-a-form-check-this/ta-p/3509638