Through the language configuration file, we have made the translation to the Spanish language, however, we detected that different texts, labels or messages are still displayed that are not translated through this process and that are found directly in the code . Because the sources of each interface correspond to different languages (groovy, react, Jquery) we request your guidance about some standard or utility for the correct integration of the use of tags, which would be contained in the language files.
Some of the sections that we identify with tags declared directly in the code are: loginLocations.gsp, LocationChooser.jsx, Footer.jsx, paginated tables, Selection lists (generated scaffolding), XLSX and CSV documents, column headers, ImportData.gsp.
Yes, I’ve noticed this as well. I’ve actually been trying to solve this in an automated way (at least to detect where these literal strings occur) using an IntelliJ IDEA Hard-coded string literals | IntelliJ IDEA or a script Hardcodes - Find Hardcoded Strings From Source Code – PentestTools but have not had much luck yet. A linter Hey Lint, help me getting out of this hardcoded strings hell | by Paolo Montalto | Medium should help here but I haven’t time to explore this option in any detail.
So I think our best course of action will be to explore one of the approach above to detect all hard-coded strings in the .gsp and .groovy files and then replace them one-by-one.
Or we can take a brute force approach and look through every file to find occurrences and replace them as we go.
Update: I forgot that I had upgraded to the latest version of IntelliJ recently. So I just reran the Hardcoded strings inspection and it worked as expected. I configured the inspection to treat the Hardcoded string pattern as an error so it’s more obvious. However, it only seems to work for .java and .groovy files.
Here’s how to configure the Code Inspection to be automated.
So while this is a good first step, we still need a way to make it work for .gsp files since that’s where most of the problems lie.
I just found a linter GitHub - jwarby/i18n-lint: `i18n-lint` is a tool for detecting possible hardcoded strings in HTML and that might be usful.
Here’s how to install it.
[jmiranda@jmiranda-ThinkPad-W540 openboxes (OBKN-69)]$ npm install -g jwarby/i18n-lint
/home/jmiranda/.nvm/versions/node/v14.0.0/bin/i18n-lint -> /home/jmiranda/.nvm/versions/node/v14.0.0/lib/node_modules/i18n-lint/bin/i18n-lint
+ i18n-lint@1.1.0
added 35 packages from 17 contributors in 2.915s
However, running the linter on a file that I knew had 0 issues, showed a false positive.
[jmiranda@jmiranda-ThinkPad-W540 openboxes (OBKN-69)]$ i18n-lint grails-app/views/dashboard/old.gsp
grails-app/views/dashboard/old.gsp
5 | <title>${warehouse.message(code: 'default.dashboard.label', default: 'Dashboard')}</title>
^ Hardcoded <title> tag
And then I ran it on a file that I knew had one specific issues (see Line 30) and it reported a lot of false positives.
[jmiranda@jmiranda-ThinkPad-W540 openboxes (OBKN-69)]$ i18n-lint grails-app/views/migration/index.gsp
grails-app/views/migration/index.gsp
10 | <div id="migration-status" class="right tag tag-info">None</div>
^ Hardcoded <div> tag
30 | <div class="loading">Loading...</div>
^ Hardcoded <div> tag
35 | cookie : {
^ Hardcoded <g:javascript> tag
36 | expires : 1
^ Hardcoded <g:javascript> tag (continued)
38 | ajaxOptions: {
^ Hardcoded <g:javascript> tag (continued)
40 | var errorMessage = "Error loading tab: " + xhr.status + " " + xhr.statusText;
^ Hardcoded <g:javascript> tag (continued)
41 | // Attempt to get more detailed error message
^ Hardcoded <g:javascript> tag (continued)
42 | if (xhr.responseText) {
^ Hardcoded <g:javascript> tag (continued)
43 | var json = JSON.parse(xhr.responseText);
^ Hardcoded <g:javascript> tag (continued)
44 | if (json.errorMessage) {
^ Hardcoded <g:javascript> tag (continued)
45 | errorMessage = json.errorMessage
^ Hardcoded <g:javascript> tag (continued)
48 | // Display error message
^ Hardcoded <g:javascript> tag (continued)
49 | $(anchor.hash).text(errorMessage);
^ Hardcoded <g:javascript> tag (continued)
51 | // Reload the page if session has timed out
^ Hardcoded <g:javascript> tag (continued)
52 | if (xhr.statusCode == 401) {
^ Hardcoded <g:javascript> tag (continued)
56 | beforeSend: function() {
^ Hardcoded <g:javascript> tag (continued)
65 | $("#" + object.id).load('${request.contextPath}/migration/' + object.id + '?format=count');
^ Hardcoded <g:javascript> tag (continued)
69 | $(this).text("Loading...");
^ Hardcoded <g:javascript> tag (continued)
70 | var url = $(this).data("url");
^ Hardcoded <g:javascript> tag (continued)
71 | $(this).load(url, {}, function(xhr, textStatus, request) {
^ Hardcoded <g:javascript> tag (continued)
72 | if (textStatus !== "success") {
^ Hardcoded <g:javascript> tag (continued)
74 | var error = JSON.parse(xhr);
^ Hardcoded <g:javascript> tag (continued)
75 | $(this).text("Error: " + error.errorMessage);
^ Hardcoded <g:javascript> tag (continued)
89 | var url = $(this).data("url");
^ Hardcoded <g:javascript> tag (continued)
90 | onLoading();
^ Hardcoded <g:javascript> tag (continued)
91 | $.post(url, {}, function(data, textStatus, xhr) {
^ Hardcoded <g:javascript> tag (continued)
92 | onComplete();
^ Hardcoded <g:javascript> tag (continued)
98 | function onLoading() {
^ Hardcoded <g:javascript> tag (continued)
101 | $("#migration-status").text("Starting migration...")
^ Hardcoded <g:javascript> tag (continued)
104 | function onComplete() {
^ Hardcoded <g:javascript> tag (continued)
107 | $("#migration-status").text("Completed migration! ")
^ Hardcoded <g:javascript> tag (continued)
If there’s a way to figured this to ignore these false positives then it might be a good way to move forward. In fact, if we can get it working reliably we should add this as a code check during the pull request process in GitHub.
I found a way to get rid of most of the false positives using the ignore-tags argument. However, I cannot get template-delimiter argument to remove the other false positives.
[jmiranda@jmiranda-ThinkPad-W540 openboxes (OBKN-69)]$ i18n-lint -v grails-app/views/**/*.gsp --ignore-tags g:javascript,script,style,r:script --template-delimiter '${,}'
grails-app/views/admin/controllerActions.gsp
16 | <li>${action }</li>
^ Hardcoded <li> tag
grails-app/views/admin/index.gsp
22 | <a class="${c.name}" href="${createLink(uri: '/' + c.logicalPropertyName)}"">${c.fullName}</a>
^ Hardcoded <a> tag
grails-app/views/admin/showSettings.gsp
65 | ${grails.util.GrailsUtil.environment}
^ Hardcoded <td> tag
128 | ${new Date() }
^ Hardcoded <td> tag
144 | ${locale?.getDisplayName(locale ?: defaultLocale)}
^ Hardcoded <g:if> tag
149 | ${locale?.getDisplayName(locale?:defaultLocale)} (${locale?.getDisplayLanguage(session.user.locale?:defaultLocale)})
^ Hardcoded <a> tag
167 | ${java.nio.charset.Charset.defaultCharset()}
^ Hardcoded <td> tag
181 | ${util.StringUtil.mask(property.value, "*")}
^ Hardcoded <g:if> tag
184 | ${property.value }
^ Hardcoded <g:else> tag
198 | ${grailsApplication.config.grails.config.locations }
^ Hardcoded <td> tag
205 | <label>${property.key }</label>
^ Hardcoded <label> tag
209 | ${util.StringUtil.mask(property?.value, "*")}
^ Hardcoded <g:if> tag
212 | ${property.value }
^ Hardcoded <g:else> tag
225 | ${prop.key }
^ Hardcoded <td> tag
228 | ${prop.value }
^ Hardcoded <td> tag
246 | <td>${printService.name}</td>
^ Hardcoded <td> tag
266 | <td>${supportedDocFlavor.mimeType}</td>
^ Hardcoded <td> tag
278 | ${supportedAttributeCategory.name}
^ Hardcoded <td> tag
281 | ${printService.getDefaultAttributeValue(supportedAttributeCategory)}
^ Hardcoded <td> tag
295 | <h2> ${quartzScheduler.schedulerName} ${quartzScheduler.schedulerInstanceId}</h2>
^ Hardcoded <h2> tag
302 | <pre>${quartzScheduler.metaData}</pre>
^ Hardcoded <pre> tag
311 | <label>${property.key }</label>
^ Hardcoded <label> tag
315 | ${util.StringUtil.mask(property?.value, "*")}
^ Hardcoded <g:if> tag
318 | ${property.value }
^ Hardcoded <g:else> tag
335 | <g:remoteLink class="button" controller="json" action="statusCalculateHistoricalQuantityJob" update="jobStatus">Show Status</g:remoteLink>
^ Hardcoded <g:remotelink> tag
336 | <g:remoteLink class="button" controller="json" action="enableCalculateHistoricalQuantityJob">Enable</g:remoteLink>
^ Hardcoded <g:remotelink> tag
337 | <g:remoteLink class="button" controller="json" action="disableCalculateHistoricalQuantityJob">Disable</g:remoteLink>
^ Hardcoded <g:remotelink> tag
360 | ${g.message(code:'jobs.backgroundJobs.label', default: 'Background Jobs')}
^ Hardcoded <g:link> tag
373 | <th>name</th>
^ Hardcoded <th> tag
374 | <th>status</th>
^ Hardcoded <th> tag
375 | <th>eternal</th>
^ Hardcoded <th> tag
376 | <th>overflowToDisk</th>
^ Hardcoded <th> tag
377 | <th>maxElementsInMemory</th>
^ Hardcoded <th> tag
378 | <th>maxElementsOnDisk</th>
^ Hardcoded <th> tag
379 | <th>memoryStoreEvictionPolicy</th>
^ Hardcoded <th> tag
380 | <th>timeToLiveSeconds</th>
^ Hardcoded <th> tag
381 | <th>timeToIdleSeconds</th>
^ Hardcoded <th> tag
382 | <th>diskPersistent</th>
^ Hardcoded <th> tag
383 | <th>diskExpiryThreadIntervalSeconds</th>
^ Hardcoded <th> tag
389 | <td>${cache.cacheConfiguration.eternal}</td>
^ Hardcoded <td> tag
390 | <td>${cache.cacheConfiguration.overflowToDisk}</td>
^ Hardcoded <td> tag
391 | <td>${cache.cacheConfiguration.maxElementsInMemory}</td>
^ Hardcoded <td> tag
392 | <td>${cache.cacheConfiguration.maxElementsOnDisk}</td>
^ Hardcoded <td> tag
393 | <td>${cache.cacheConfiguration.memoryStoreEvictionPolicy}</td>
^ Hardcoded <td> tag
394 | <td>${cache.cacheConfiguration.timeToLiveSeconds}</td>
^ Hardcoded <td> tag
395 | <td>${cache.cacheConfiguration.timeToIdleSeconds}</td>
^ Hardcoded <td> tag
396 | <td>${cache.cacheConfiguration.diskPersistent}</td>
^ Hardcoded <td> tag
397 | <td>${cache.cacheConfiguration.diskExpiryThreadIntervalSeconds}</td>
^ Hardcoded <td> tag
grails-app/views/admin/showUpgrade.gsp
54 | <label>Downloading file:</label> <b>${command?.remoteWebArchiveUrl }</b>
^ Hardcoded <label> tag
54 | <label>Downloading file:</label> <b>${command?.remoteWebArchiveUrl }</b>
^ Hardcoded <b> tag
57 | <label>Last updated:</label> <b>${command?.remoteFileLastModifiedDate }</b>
^ Hardcoded <label> tag
57 | <label>Last updated:</label> <b>${command?.remoteFileLastModifiedDate }</b>
^ Hardcoded <b> tag
60 | <label>Remote file size:</label> <b>${command?.remoteFileSize }</b>
^ Hardcoded <label> tag
60 | <label>Remote file size:</label> <b>${command?.remoteFileSize }</b>
^ Hardcoded <b> tag
63 | <label>Local file size:</label> <b>${command?.localWebArchive?.size() }</b>
^ Hardcoded <label> tag
63 | <label>Local file size:</label> <b>${command?.localWebArchive?.size() }</b>
^ Hardcoded <b> tag
76 | ${command?.progressPercentage}% complete
^ Hardcoded <div> tag
79 | Download was canceled.
^ Hardcoded <g:if> tag
82 | Download has been completed!!!
^ Hardcoded <g:elseif> tag
99 | ${command?.localWebArchive?.absolutePath }
^ Hardcoded <td> tag
100 | - <b>${session?.command?.future?.done ? "Ready" : "Not ready" }</b>
^ Hardcoded <b> tag
125 | (Please wait for download to complete)
^ Hardcoded <g:else> tag
grails-app/views/admin/status.gsp
10 | <content tag="globalLinksMode">append</content>
^ Hardcoded <content> tag
11 | <content tag="localLinksMode">override</content>
^ Hardcoded <content> tag
19 | <h1>Application Status</h1>
^ Hardcoded <h1> tag
21 | <li>App version: <g:meta name="app.version"></g:meta></li>
^ Hardcoded <li> tag
22 | <li>Grails version: <g:meta name="app.grails.version"></g:meta></li>
^ Hardcoded <li> tag
23 | <li>JVM version: ${System.getProperty('java.version')}</li>
^ Hardcoded <li> tag
24 | <li>Controllers: ${grailsApplication.controllerClasses.size()}</li>
^ Hardcoded <li> tag
25 | <li>Domains: ${grailsApplication.domainClasses.size()}</li>
^ Hardcoded <li> tag
26 | <li>Services: ${grailsApplication.serviceClasses.size()}</li>
^ Hardcoded <li> tag
27 | <li>Tag Libraries: ${grailsApplication.tagLibClasses.size()}</li>
^ Hardcoded <li> tag
31 | <h1>Installed Plugins</h1>
^ Hardcoded <h1> tag
40 | <h1>Available Controllers</h1>
^ Hardcoded <h1> tag
grails-app/views/attribute/edit.gsp
33 | <h2><g:message code="default.${actionName}.label" args="[entityName]" /> <small>${attributeInstance.name}</small></h2>
^ Hardcoded <small> tag
grails-app/views/attribute/list.gsp
56 | <g:sortableColumn property="entityTypeCode" title="${warehouse.message(code: 'attribute.entityTypeCode.label', default: 'Entity Type')}" />
^ Hardcoded 'title' attribute
60 | <g:sortableColumn property="allowOther" title="${warehouse.message(code: 'attribute.allowOther.label')}" />
^ Hardcoded 'title' attribute
66 | <td><g:link action="edit" id="${attributeInstance.id}">${fieldValue(bean: attributeInstance, field: "code")}</g:link></td>
^ Hardcoded <g:link> tag
69 | <td>${attributeInstance.options.size()} <warehouse:message code="attribute.options.label" default="Options"/></td>
^ Hardcoded <td> tag
grails-app/views/attribute/_renderFormList.gsp
28 | <option value="_other"<g:if test="${otherSelected}"> selected</g:if>>
^ Hardcoded <option> tag
Ok I got close to solving the template-delimiter issue
$ i18n-lint grails-app/views/**/*.gsp --ignore-tags "g:javascript,script,style,r:script" --template-delimiters "{,}" | wc -l
1696
While running the command I get an error that makes we wonder if it’s actually getting through all of the files. So maybe we need to run this directory by directory.
_stream_writable.js:289
throw new ERR_INVALID_ARG_TYPE(
^
TypeError [ERR_INVALID_ARG_TYPE]: The "chunk" argument must be of type string or an instance of Buffer or Uint8Array. Received an instance of TypeError
at WriteStream.Writable.write (_stream_writable.js:289:13)
at /home/jmiranda/.nvm/versions/node/v14.0.0/lib/node_modules/i18n-lint/bin/i18n-lint:193:26
at Array.forEach (<anonymous>)
at /home/jmiranda/.nvm/versions/node/v14.0.0/lib/node_modules/i18n-lint/bin/i18n-lint:172:11
at f (/home/jmiranda/.nvm/versions/node/v14.0.0/lib/node_modules/i18n-lint/node_modules/once/once.js:25:25)
at Glob.<anonymous> (/home/jmiranda/.nvm/versions/node/v14.0.0/lib/node_modules/i18n-lint/node_modules/glob/glob.js:133:7)
at Glob.emit (events.js:315:20)
at Glob._finish (/home/jmiranda/.nvm/versions/node/v14.0.0/lib/node_modules/i18n-lint/node_modules/glob/glob.js:172:8)
at done (/home/jmiranda/.nvm/versions/node/v14.0.0/lib/node_modules/i18n-lint/node_modules/glob/glob.js:159:12)
at Glob._processSimple2 (/home/jmiranda/.nvm/versions/node/v14.0.0/lib/node_modules/i18n-lint/node_modules/glob/glob.js:670:3) {
code: 'ERR_INVALID_ARG_TYPE'
}
1 Like