
- Post History
- Subscribe to RSS Feed
- Mark as New
- Mark as Read
- Bookmark
- Subscribe
- Printer Friendly Page
- Report Inappropriate Content
on ‎10-11-2021 05:27 AM
Parse widget templates from a target instance and detect hardcoded strings
Description:
For most clients having a multilingual portal is important. When we develop widgets we need to code them properly so the strings we put in there are translatable. However, mistakes happen. With a small node.js script and a neat linting library you can save yourself some time and find any hardcoded strings in widgets.
Pre-requisites:
- node.js installed
- access to use the table API on the target ServiceNow instance
Steps:
- Create an empty folder somewhere on your computer
- Open a console in the new folder and install the required npm modules - Axios and i18n-lint.
npm install --save axios​
npm install --save jwarby/i18n-lint
- Create a new js file for example app.js with the following content
const username = 'user'; // CHANGE ME! const pass = 'pass'; // CHANGE ME! const apiUrl = 'https://devxxxxx.service-now.com/api/now/table/'; // CHANGE ME! const table = 'sp_widget'; const query = 'sys_created_on<javascript:gs.beginningOfYesterday()'; // filter the query // CHANGE ME! const limit = 50; // limit the query CHANGE ME! const fields = 'sys_id,name,template'; // use only fields that we need const requestFullPath = apiUrl + table + '?sysparm_query=' + query + '&sysparm_fields=' + fields + '&sysparm_limit=' + limit; const axios = require('axios'); // Load axios for easier http requests from node.js const usernamePasswordBuffer = Buffer.from(username + ':' + pass); const base64data = usernamePasswordBuffer.toString('base64'); // encode the username and the pass to give them in the Authorization header const result = { bottomLine: { cleanWidgets: { count: 0, names: [] }, pollutedWidgets: { count: 0, names: [] } }, hardcodeFindings: [] }; axios.get(requestFullPath, { withCredentials: true, headers: { 'Content-Type': 'application/json', 'Authorization': `Basic ${base64data}`, } }, {}) .then(response => { const fs = require('fs'); const i18nlint = require('i18n-lint'); // Load the linting library which does the parsing and recognition const widgetArr = response.data.result; widgetArr.forEach(function(widget){ const context = widget.template; const options = { templateDelimiters: [['{', '}']] // Set the delimiters for our use case since the i18n-lint can be used for various template languages }; const errors = i18nlint.scan(context, options); if(errors.length > 0){ errors.forEach(function(error){ error.evidence = error.evidence.toString(); }); result.bottomLine.pollutedWidgets.count++; result.bottomLine.pollutedWidgets.names.push(widget.name); result.hardcodeFindings.push({sys_id: widget.sys_id, name: widget.name, errors: errors}); }else{ result.bottomLine.cleanWidgets.count++; result.bottomLine.cleanWidgets.names.push(widget.name); } }); fs.writeFile('widgetLog.json', JSON.stringify(result), function (err) { // Write the results as a .json file on the system if (err) return console.log(err); console.log('Widgets info written > widgetLog.json'); }); }) .catch(error => { console.log(error); });
How to use:
- Adjust the username, pass, apiUrl, limit and query in the file as per your needs. You can play with the rest of the parameters too if you have some nice ideas. Also the linting library can take more configuration but it looks very well as it is. It does not only scan the strings between the opening and closing tags but also scans attributes(by default: title, placeholder, alt). Also it ignores irrelevant elements like <script> and <style> and you can add more.
- Run the node.js file from the console
node app.js​
- It will generate a file in the same directory called "widgetLog.json"
- Read the json and inspect the findings
Results:
The results have the following structure
{
bottomLine: {
cleanWidgets: {
count: 0,
names: []
},
pollutedWidgets: {
count: 0,
names: []
}
},
hardcodeFindings: []
}
The findings contain detailed information on where the string was found. Here is an example report:
{
"bottomLine": {
"cleanWidgets": {
"count": 10,
"names": [
"Community Bucket Facet",
"Community - Post All Content",
"Communities T&C Blocker",
"Breakout Game",
"knowledge Homepage Search",
"Test Context",
"Knowledge Language Facet",
"CSM Unified Portal Header Menu Widget",
"Knowledge Article Comments",
"Events Ribbon"
]
},
"pollutedWidgets": {
"count": 3,
"names": [
"Test widget 1",
"Test widget 2",
"Test widget 3"
]
}
},
"hardcodeFindings": [
{
"sys_id": "xxxxxxxxxxxxxxxxxxxxxxxxx",
"name": "Test widget 1",
"errors": [
{
"id": "(error)",
"code": "W001",
"reason": "Hardcoded <div> tag",
"evidence": "/(\\} Hours)/",
"line": 7,
"character": 89,
"scope": " <div class=\"row\" style=\"padding-top:20px;\">{{data.hours}} Hours</div>"
},
{
"id": "(error)",
"code": "W001",
"reason": "Hardcoded <div> tag",
"evidence": "/(Expected)/",
"line": 8,
"character": 18,
"scope": " <div class=\"row\">Expected</div>"
}
]
},
{
"sys_id": "xxxxxxxxxxxxxxxxxxxxxxxxx",
"name": "Test Widget 2",
"errors": [
{
"id": "(error)",
"code": "W001",
"reason": "Hardcoded <th> tag",
"evidence": "/(Criteria)/",
"line": 48,
"character": 22,
"scope": " <th scope=\"col\">Criteria</th>"
},
{
"id": "(error)",
"code": "W001",
"reason": "Hardcoded <button> tag",
"evidence": "/(Close)/",
"line": 65,
"character": 77,
"scope": " <button type=\"button\" class=\"btn btn-primary\" data-dismiss=\"modal\">Close</button>"
}
]
},
{
"sys_id": "xxxxxxxxxxxxxxxxxxxxxxxxx",
"name": "Test Widget 3",
"errors": [
{
"id": "(error)",
"code": "W001",
"reason": "Hardcoded <div> tag",
"evidence": "/(Viewed)/",
"line": 4,
"character": 37,
"scope": " <div class = \"text-center\">Viewed </div>"
},
{
"id": "(error)",
"code": "W002",
"reason": "Hardcoded 'title' attribute",
"evidence": "/(Author)/m",
"line": 12,
"character": 46,
"scope": " <span title =\"Author\"> <i class=\"fa fa-user\" aria-hidden=\"true\"></i>{{c.data.author}} </span><span title=\"Created\">"
}
]
}
]
}
Conclusion:
There are some situations where this method won't detect issues - for example hardcoded string in a server or client variable which are used in the template won't be detected. Also, some deep nesting of translatable strings like ${Some string {{someVar}} end of string} but such things should be rare because they are a not so clean to manage in the translation tables anyway. Besides such situations, it seems pretty reliable.
Go scan your widgets! If you get 0 elements in the pollutedWidgets.count treat yourself with a beer! 😄
P.S.
Thanks to @jwarby for the nice linting tool!
- 690 Views
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Awesome ! Really helpful. Thanks for sharing