- Post History
- Subscribe to RSS Feed
- Mark as New
- Mark as Read
- Bookmark
- Subscribe
- Printer Friendly Page
- Report Inappropriate Content
on 04-03-2023 06:42 AM
I had an interesting question from a partner this past week, in that they wanted to translate their "Indoor Maps" they had made. As I'm always up for a challenge, putting pen to paper, I think you might guess what happened next.
Get your favourite beverage and snack ready because this one is going to be interesting and it might be a bit long. Here goes.
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
Medium / Complex - Need to translate a dashboard? - check this
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 Medium.
What we're aiming for.
In the above image I'm using some demo location data that's part of the "Workplace Management" and "Indoor Mapping" applications, just so that I have a reference point to use. What we're specifically aiming for here is a non-English scenario.
To achieve this, we will need to do a few things. The first is that we'll need to translate the "Service Portal" (if you haven't seen this blog post, I highly recommend reviewing it before continuing with this one). As we will need to ensure the Portal's UI and potentially this page (/wsd_location_directory) is translated into our desired Language. In this case we're going to translate into French.
Then we also need to make sure we're really familiar with how the translation tables work, so if you're a bit rusty give this blog post a quick re-read as we're going to be using [sys_translated_text] a lot.
A few things we need to know first.
When it comes to embedded maps there's a few things we need to know about them and how they behave. Typically in the platform we either use Google maps (e.g. for CSM workspace) or as is the case here we use "OpenStreetMap".
There are quite a few reasons for this but I shall save that for another day. What we need to know for our immediate needs is that the way these maps are represented is essentially a combination of "tiles" (think of them as images). And very often you'll find that the street names, roads, city names etc are baked into those tiles. So this means that we have very little control over them.
Usually the language of the street names etc, is in the local language of the Country you are viewing and in my demo example my location is based in Lille France. This isn't always the case, because with Google if you are viewing https://maps.google.com and you click on the hamburger icon in the top left, you very often can change the UI language which will then present you tiles in the language you selected (providing they are available).
This therefore means, that we should be able to translate our important / created building maps providing we know where they are and providing we know what type of field type they are. The good news is that they indeed should be translatable. But, we need to grab the names from a few different places. On the bright-side, all the strings that we want to translate should be "translated_text" fields, which means we should be able to create a nice query to pick them up for the Localization Framework. This also means that for everyone of these fields / values we should see a "TRT" prefix when / if the i18n debugger is enabled, and we should also expect to populating the [sys_translated_text] table with our translations.
How we can do it.
As usual (with any artifact) we need to understand the "what" we want to translate. What tables is it in, where are the strings held, and what is our baseline. Just for reference, he's our English baseline:
The pyramid of data that we need to look at here is a bit like this:
- [sn_wsd_core_campus]
- [sn_wsd_core_site]
- [sn_wsd_core_building]
- [sn_wsd_core_floor]
- [sn_wsd_core_area]
- [sn_wsd_core_space]
- [sn_wsd_core_space_type]
It's not going to be just those tables that we need to look at, there's actually some parallel records that we need to also pull from the [sn_map_core] hierarchy which you'll see in my query. However, this should act as a good starting point (as with the other prototype artifacts) to get you most of the way there.
The Artifact.
As usual, let's define our new Artifact. We're going to call it "Proto - WSD locations" with an internal name of "proto_wsd_locations". We're going to define it from the [sn_wsd_core_campus] table (this will also be where our "Request Translation" UI action will go, more on that in a bit):
And here's the where the magic is, the Processor script:
var LF_WSDcampus = Class.create();
LF_WSDcampus.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
**********/
var tableName = params.tableName;
var sysId = params.sysId;
var language = params.language;
var groupName = "";
var lfDocumentContentBuilder = new global.LFDocumentContentBuilder("v1", language, sysId, tableName);
// let's get the Campus first
var getCampus = new GlideRecord('sn_wsd_core_campus');
getCampus.addQuery('sys_id', sysId);
getCampus.query();
if (getCampus.next()) {
lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getCampus, "Campus", "Name");
// we need to get the campus from the map record
var getCampusMap = new GlideRecord('sn_map_core_campus');
getCampusMap.addQuery('name', getCampus.name);
getCampusMap.query();
if(getCampusMap.next()){
lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getCampus, "Campus - Map", "Name");
}
// let's get the site
var getSite = new GlideRecord('sn_wsd_core_site');
getSite.addQuery('sys_id', getCampus.site.sys_id);
getSite.orderBy('name');
getSite.query();
if (getSite.next()) {
lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getSite, "Site", "Site - " + getSite.name);
}
// now we need to loop through the buildings
var getBuilding = new GlideRecord('sn_wsd_core_building');
getBuilding.addQuery('campus', getCampus.sys_id);
getBuilding.orderBy('name');
getBuilding.query();
while (getBuilding.next()) {
lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getBuilding, "Buildings", getBuilding.name);
// we need to get the buidling from the map record
var getBuildingMap = new GlideRecord('sn_map_core_building');
getBuildingMap.addQuery('name', getBuilding.name);
getBuildingMap.query();
if(getBuildingMap.next()){
lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getBuildingMap, "Buildings - Map", "Name");
}
// now we need each floor per building
var getFloor = new GlideRecord('sn_wsd_core_floor');
getFloor.addQuery('building', getBuilding.sys_id);
getFloor.orderBy('name');
getFloor.query();
while (getFloor.next()) {
lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getFloor, "Building - "+getBuilding.name, "Building - "+getBuilding.name+" Floor - " + getFloor.name);
// we need to get the floor from the map record
var getFloorMap = new GlideRecord('sn_map_core_floor');
getFloorMap.addQuery('name', getFloor.name);
getFloorMap.query();
if(getFloorMap.next()){
lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getFloorMap, "Floor - Map", "Name");
}
// now we need any areas
var getArea = new GlideRecord('sn_wsd_core_area');
getArea.addQuery('floor', getFloor.sys_id);
getArea.orderBy('name');
getArea.query();
while (getArea.next()) {
lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getArea, "Building - "+getBuilding.name+" Areas", "Building - "+getBuilding.name+" Area - " + getArea.name);
}
// now we need any spaces
var getSpace = new GlideRecord('sn_wsd_core_space');
getSpace.addQuery('floor', getFloor.sys_id);
getSpace.orderBy('name');
getSpace.query();
while (getSpace.next()) {
lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getSpace, "Building - "+getBuilding.name+" Spaces", "Building - "+getBuilding.name+" Space - " + getSpace.name);
// we need to get the space from the map record
var getSpaceMap = new GlideRecord('sn_map_core_place');
getSpaceMap.addQuery('name', getSpace.name);
getSpaceMap.query();
if(getSpaceMap.next()){
lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getSpaceMap, "Building - "+getBuilding.name+" Spaces - Map", "Building - "+getBuilding.name+" Place - " + getSpaceMap.name)
}
}
}
}
}
// lets also process space types
var getSpaceTypes = new GlideRecord('sn_wsd_core_space_type');
getSpaceTypes.addQuery('active', true);
getSpaceTypes.orderBy('name');
getSpaceTypes.query();
while (getSpaceTypes.next()) {
lfDocumentContentBuilder.processTranslatableFieldsForSingleRecord(getSpaceTypes, "Space Types", "Name");
}
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_WSDcampus'
});
The UI action will be like this on the [sn_wsd_core_campus] table:
^ Remember to put the "internal_name" of the artifact into the condition of the UI action so that it knows which one to call.
Also, remember you may need a "setting" in the Localization Framework for this new artifact if you haven't got one already applied to all.
One this to bear in mind, it's quite possible that if you have a lot of "sites" or "spaces", you may have a lot of strings to translate. It might look like the same one is repeated but in reality it's quite likely that there's multiple same things on the map. In my example I have many "fire extinguishers" yet they are all distinctly different and in different places.
The artifact is currently designed to group by each data level, but feel free to change that if you wish:
What have we learned?
Well, we've learned that it is absolutely possible to translate our Building maps, we now understand the data hierarchy and what type of translation it would, coupled with also being able to integrate such an activity into any process we already translate with via the Localization Framework.
Please remember that this is just a Prototype to get you started, and that your set-up might be a little different to what I have here, however feel free to propose any tweaks or share ideas for the benefit of the community.
And as always, please like share and subscribe if you found this helpful,
- 2,848 Views
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
It's a very good job, but I honestly don't understand why it's so complicated when you can order a translation, pay the money, and save time. This is, of course, my own opinion, and I have no complaints about your work, only a sense of respect for it, but it would have been easier to order a translation.
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
@Batsy,
That is the exact point, with this it is how you can "order" those translations because it identifies all of the source text, bundles it into a task then you can choose to MT it or send it to a TMS (if you are using an Agency). This is the reason we have a capability like the Localization Framework. The artifact can be built once then used repeatedly without loads of custom other things, ergo removing the complexity you mention.
These posts (about how I show how to design an artifact) are a learning exercise for people to see how they too could design and write their own (should they ever need to). So the query itself might be complex, because it might need to be checking lots of places for source strings, but the outcome should be as simple as possible,
Many thanks,
Kind regards
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
I saw this and I wonder how this would affect this in the future releases. ServiceNow acquires Mapwize, aims to add indoor mapping to Now Platform | ZDNET