Additional language settings

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