Check Service Portal widgets for hardcoded strings
Import
·
Oct 11, 2021
·
article
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 and <style> and you can add more.</li> <li>Run the node.js file from the console<br> <code> node app.js </code></li> <li>It will generate a file in the same directory called "widgetLog.json"</li> <li>Read the json and inspect the findings</li> </ol> <h2>Results:</h2> <p>The results have the following structure</p> <pre><code>{ bottomLine: { cleanWidgets: { count: 0, names: [] }, pollutedWidgets: { count: 0, names: [] } }, hardcodeFindings: [] } </code></pre> <p>The findings contain detailed information on where the string was found. Here is an example report:</p> <pre><code>{ "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\">" } ] } ] } </code></pre> <h2>Conclusion:</h2> <p>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.</p> <p>Go scan your widgets! If you get 0 elements in the pollutedWidgets.count treat yourself with a beer! <img src="https://community.servicenow.com/community/s/html/@0B372E9B29CB703373423EBED8EADBB8/emoticons/1f604.png" alt="image"></p> <p>P.S.</p> <p>Thanks to @jwarby for the nice linting tool!</p> <p>Labels: </p> <ul> <li><a href="https://community.servicenow.com/community/developer-articles/tkb-p/developer-kb/label-name/multiple%20versions?labels=multiple+versions" target="_blank" rel="noopener">Multiple Versions</a></li> <li><a href="https://community.servicenow.com/community/developer-articles/tkb-p/developer-kb/label-name/service%20portal%20development?labels=service+portal+development" target="_blank" rel="noopener">Service Portal Development</a></li> </ul> <p><a href="https://community.servicenow.com/now-platform/latest-release.html" target="_blank" rel="noopener"><img src="https://community.servicenow.com/community/s/html/assets/Xanadu-Community_Banner_1_314x314.jpg" alt="image"></a></p>
View original source
https://www.servicenow.com/community/developer-articles/check-service-portal-widgets-for-hardcoded-strings/ta-p/2316544