i18n-tasks icon indicating copy to clipboard operation
i18n-tasks copied to clipboard

Patterns in JS

Open afdev82 opened this issue 2 years ago • 11 comments

I'm using the i18n-js gem/package to use the translations also in JS and till now I don't really understand which patterns are recognized by the scanner, I was always getting unused translations. There are several possibilities to specify the translation in JS (and interpolate strings too). To avoid to interpolate strings like i18n.t("some.scope." + key) or i18n.t(`some.scope.${key}`), I'm just using the scope option in the following way (see also here):

i18n.t(key, { scope: 'some.scope' });

I think this syntax is very similar to the one used in Ruby.

Could you clarify that aspect? I haven't found anything special about JS in the docs. I'm using the scope option when the key is dynamic, but also a simple string sometimes is not recognized. Few examples:

$(".modal-body", this.alertProductModal).html(I18n.t('configurations.check_background.your_product_doesnt_fit'));
$(".modal-footer", this.alertProductModal).append($("<a id='ignore_alert_background' class='conf-btn btn btn-outline-dark btn-secondary'>" + I18n.t('global.ignore') + "</a>"));

Thank you for your support!

afdev82 avatar Apr 25 '22 14:04 afdev82

@afdev82 Are these translations in a html file or a javascript file?

davidwessman avatar May 15 '22 18:05 davidwessman

It's javascript

afdev82 avatar May 15 '22 20:05 afdev82

Then I think you would have to implement a CustomScanner to handle a case like this.

davidwessman avatar May 16 '22 06:05 davidwessman

Ah ok,

in the Usage search section of the README I read:

i18n-tasks uses an AST scanner for .rb and .html.erb files, and a regexp scanner for all other files.

I thought that the javascript files were supported by the regexp scanner and it was not needed to implement a custom one. If I need to implement one for the javascript files it's also fine, could it be clarified in the README which files are supported out of the box? Thank you!

afdev82 avatar May 16 '22 06:05 afdev82

@afdev82 Ah, that makes sense to document. The existing one probably works for a lot of syntaxes, but I do not think it can handle the Javascript object as parameter.

davidwessman avatar May 16 '22 06:05 davidwessman

Could it be worth to improve the existing one? I think many users could benefit from it. First I will try to have a look at the custom scanner to fix my issue, I think if I find a solution, maybe it could be integrated later.

afdev82 avatar May 16 '22 07:05 afdev82

@afdev82 Yes, that would probably be good 🙂 Could you write some test cases?

davidwessman avatar May 16 '22 07:05 davidwessman

I'll try to write some when I will work on that again. For the moment, thank you!

afdev82 avatar May 16 '22 07:05 afdev82

One approach for the JS scanner is using JS AST parser like @babel/parser and @babel/traverse to traverse and find the items. Example here:

let fs = require('fs')
let parser = require("@babel/parser")
let traverse = require("@babel/traverse")

function collectCalls(filepath) {
  let results = []

  let code = fs.readFileSync(filepath).toString()
  let ast = parser.parse(code, {
    // parse in strict mode and allow module declarations
    sourceType: "module",

    plugins: [
      // enable jsx and flow syntax
      "jsx",
    ],
  });

  traverse.default(ast, {
    CallExpression(path) {
      //console.log(path.node)
      let { loc, start, end } = path.node
      let { type, object: objectNode, property: propertyNode } = path.node.callee

      if (type == 'MemberExpression' && objectNode.name == 'I18n' && (propertyNode.name == 't' || propertyNode.name == 'translate')) {
        let [ { value: rawKey }, defaultArg ] = path.node.arguments

        let h = {
          path: filepath,
          pos: start,
          line_num: loc.start.line,
          line_pos: loc.start.column,
          line: code.substring(start, end),
          raw_key: rawKey || null,
          default_arg: null,
        }

        if (defaultArg && defaultArg.type == 'ObjectExpression') {
          let node = defaultArg.properties.find(node => node.key.name == 'defaultValue')
          if (node) {
            if (node.value.type == 'StringLiteral') {
              h.default_arg = node.value.value
            } else if (node.value.type == 'ObjectExpression') {
              h.default_arg = node.value.properties.reduce((obj, property) => {
                obj[property.key.name] = property.value.value
                return obj
              }, {})
            }
          }
        }

        results.push(h)
      }
    },
  })
  return results
}

cantin avatar Aug 04 '22 02:08 cantin

Or a simple fix would be to update the pattern to allow a lowercase i18n.t.

https://github.com/glebm/i18n-tasks/blob/a2b06e3cf8cebb10cf47f680e82779f353839815/lib/i18n/tasks/scanners/pattern_scanner.rb#L15

Something like:

-    TRANSLATE_CALL_RE = /(?<=^|[^\w'\-.]|[^\w'\-]I18n\.|I18n\.)t(?:ranslate)?/
+   TRANSLATE_CALL_RE = /(?<=^|[^\w'\-.]|[^\w'\-](?:I|i)18n\.|(?:I|i)18n\.)t(?:ranslate)?/

Or:

-    TRANSLATE_CALL_RE = /(?<=^|[^\w'\-.]|[^\w'\-]I18n\.|I18n\.)t(?:ranslate)?/
+   TRANSLATE_CALL_RE = /(?<=^|[^\w'\-.]|[^\w'\-][Ii]18n\.|[Ii]18n\.)t(?:ranslate)?/

Either one of these patches is working for me.

JohnRDOrazio avatar Jan 04 '23 17:01 JohnRDOrazio