demux
demux

Reputation: 4654

Hoist all expressions of a certain type to top scope using babel transform

At work, we have a custom solution for translations. The implementation is as follows:

I'm currently in the process of rewriting parts of the application using React.js, and I'm implementing a javascript version of get_string (calling it getString).

I need a way...

What I think would be a perfect solution is to create a babel transform that moves all getString calls to the top scope, leaving a variable as reference. This would allow me to solve both problems with relative ease.

import React from 'react'
import {getString} from 'translate'

export default class TestComponent extends React.Component {
  render() {
    const translatedString = getString('unique_string_identifier_1', 'Default string 1')
    
    return <div>
     {getString('unique_string_identifier_2', 'Default string 2')}
    </div>
  }
}

Would become something like:

import React from 'react'
import {getString} from 'translate'

const _getStringRef0 = getString('unique_string_identifier_1', 'Default string 1')
const _getStringRef1 = getString('unique_string_identifier_2', 'Default string 2')

export default class TestComponent extends React.Component {
  render() {
    const translatedString = _getStringRef0
    
    return <div>
     {_getStringRef1}
    </div>
  }
}

How would I go about doing this?

Upvotes: 1

Views: 360

Answers (1)

demux
demux

Reputation: 4654

I've changed the requirements slightly, so...

import React from 'react'
import {getString, makeGetString} from 'translate'

const _ = makeGetString({
  prefix: 'unique_prefix'
})

export default class TestComponent extends React.Component {
  render() {
    const translatedString = getString('unique_string_identifier_1', 'Default string 1 %s', dynamic1, dynamic2)

    return <div>
     {getString('unique_string_identifier_2', 'Default string 2')}
     {_('string_identifier_3')}
    </div>
  }
}

becomes...

import React from 'react'
import {getString, makeGetString} from 'translate'

const _getString = getString('unique_string_identifier_1', 'Default string 1 %s');
const _getString2 = getString('unique_string_identifier_2', 'Default string 2');

const _ = makeGetString({
  prefix: 'unique_prefix'
})

const _ref = _('string_identifier_3');

export default class TestComponent extends React.Component {
  render() {
    const translatedString = _getString(dynamic1, dynamic2)

    return <div>
     {_getString2()}
     {_ref()}
    </div>
  }
}

This is actually what I have:

module.exports = function(babel) {
  const {types: t} = babel

  const origFnNames = [
    'getString',
    'makeGetString',
  ]

  const getStringVisitor = {
    CallExpression(path) {
      const callee = path.get('callee')
      if(callee && callee.node && this.fnMap[callee.node.name]) {
        this.replacePaths.push(path)
      }
    }
  }

  const makeGetStringVisitor = {
    VariableDeclaration(path) {
      path.node.declarations.forEach((decl) => {
        if(!(decl.init && decl.init.callee && !decl.parent)) {
          return
        }

        const fnInfo = this.fnMap[decl.init.callee.name]

        if(fnInfo && fnInfo.name === 'makeGetString') {
          this.fnMap[decl.id.name] = {
            name: decl.id.name,
            path
          }
        }
      })
    }
  }

  return {
    visitor: {
      ImportDeclaration(path) {
        if(path.node.source.value === 'translate') {
          const fnMap = {}

          path.node.specifiers.forEach((s) => {
            if(origFnNames.indexOf(s.imported.name) !== -1) {
              fnMap[s.local.name] = {
                name: s.imported.name,
                path
              }
            }
          })

          path.parentPath.traverse(makeGetStringVisitor, {fnMap})

          const replacePaths = []

          path.parentPath.traverse(getStringVisitor, {fnMap, replacePaths})

          delete fnMap.makeGetString

          Object.keys(fnMap).map((k) => {
            const fnInfo = fnMap[k]

            const paths = replacePaths.filter((p) => p.get('callee').node.name === fnInfo.name)

            const expressions = paths.map((rPath) => {
              const id = rPath.scope.generateUidIdentifierBasedOnNode(rPath.node)
              const args = rPath.node.arguments

              rPath.replaceWith(t.callExpression(id, args.slice(2)))

              const expr = t.callExpression(t.identifier(fnInfo.name), args.slice(0, 2))

              return t.variableDeclaration('const', [t.variableDeclarator(id, expr)])
            })

            fnInfo.path.insertAfter(expressions)
          })
        }
      }
    }
  }
}

Upvotes: 1

Related Questions