preface

Recently, I have been studying how to automate the internationalization of an English project without manual internationalization. I have written a script. Although the internationalization has not been accurately completed, it has achieved semi-automation.

Procedure for developing CLI plug-ins

  • Initialize a projectnpm init -y
  • Create a project directory

  • Add bin to package.json file

 "bin": {
    "translate-cli": "./bin/cli.js"
  },
Copy the code
  • performnpm linkThe translate-cli is inserted into the environment variable
  • translate-cliThe script is ready to execute.

Lingui.js – React internationalization library

It also supports javascript and typescript translation. Instead of writing internationalized static files manually, it can run commands to generate internationalized static files automatically. And the official documents are particularly detailed. Official document: Lingui.js.org/

Jscodeshift – Code automation refactoring tool

  • It can parse javascript, typescript code
  • Translate the code into AST syntax trees, and then batch query, manipulate, and output according to the nodes
  • Code -> astexplorer.net/
  • Apis are provided: find, replaceWith(), insertBefore (), and so on.

Internationalization plug-in solution

  • Batch file reading
  • Convert to an AST syntax tree using the jscodeshift plug-in
  • View the node rules that need to be internationalized according to the AST syntax tree
  • Find the node according to the node rules to determine whether to internationalize
  • Replace the value of the node to be internationalized with the internationalization code

Internationalization plug-in main logic

Configuration file translate-cli.js

module.exports = {
  pathRoot: 'public',
  regRules: [
    {
      reg: 'public/**/*[!.test].tsx',
      exten: 'tsx',
      rules: [
        'ExpressionStatement:StringLiteral',
        'JSXElement:StringLiteral',
        'JSXText',
        'ClassProperty:Literal',
        'TemplateLiteral',
        'ObjectExpression:Literal'
      ],
    },
    {
      reg: 'public/**/*.ts',
      exten: 'ts',
      rules: [
        'ExpressionStatement:StringLiteral',
        'TemplateLiteral',
        'ObjectExpression:Literal'
      ],
    },
  ],
};

Copy the code

1. Traverse regRules to read files according to regRules matching file path matching rules in configuration file translate-cli.js

For (value of regRules) {mapFiles(value); }Copy the code

2. Glob scans for files to be translated. After traversing the file, use fs.readfilesync () to read the file content, convert the content to AST syntax tree using jscodeshift plug-in, perform internationalization based on the configured nodes (nodes in the AST syntax tree) rules, traverse rules to perform internationalization.

/** * read the file, according to different rules, * @param {exten} file suffix * @param {rules} nodes that need to be internationalized */ const mapFiles = ({reg, exten, rules }) => { const j = jscodeshift.withParser(exten); const files = glob.sync(path.join(process.cwd(), reg)); For (value of files) {console.log(' ${value}... `); isFirstImport = false; const filedata = fs.readFileSync(value, 'utf-8'); const ast = j(filedata); For (rule of rules) {replaceIntl(j, ast, rule); } fs.writeFileSync(value, ast.toSource()); Console. log(' ${value} internationalization rule replaced successfully '); }};Copy the code

3. Then use ast.find() to extract nodes from the AST syntax tree and return true according to the value of the node and the summarized rules for whether to do internationalization (the rules to do internationalization are summarized from the project and strongly associated with the project, for example, when the value is string and the first letter is uppercase). False.

@param {*} rule = @param {*} rule = @param {*} value = @returns */ const replaceRule = (rule, item, j) => { const { value } = item.value; / / no need to be translated information directly to return the if (noIntl. Includes (value) | | item. The name = = = 'key' | | item?. ParentPath?. Value. The type = = = 'TaggedTemplateExpression' || item?.parentPath?.parentPath?.value?.callee?.name === 'navigationLogger' ) { return false;  } // Const reg = typeof value === 'string'? /[A-Z]/.test( value.replace(/\n/g, '').replace(/\r/g, '').trim().slice(0, 1) ) : false; switch (rule) { case 'JSXText': case 'ExpressionStatement:StringLiteral': case 'JSXElement:StringLiteral': case 'ClassProperty:Literal': case 'ObjectExpression:Literal': return reg; case 'TemplateLiteral': const { raw } = j(item).find(j.TemplateElement).at(0).get().value.value; console.log(rule, raw); return raw ? /[A-Z]/.test( raw.replace('\n', '').replace('\r', '').trim().slice(0, 1) ) : false; default: return false; }};Copy the code

3. When true is returned, return the object to internationalized code using ast.replacewith (). Internationalized code has different rules, and return internationalized templates (e.g. {} is needed for tags, string templates are used, and values are passed dynamically).

/** * if there are single quotes in the value, * @param {*} value */ const templateScheme = (value) => {return 't\' ${value.replace (/ '/g, '\\`') .replace(/{{/g, "'{{") .replace(/}}/g, "}}'") .replace(/{}/g, "'{}'")}\``; }; @param {*} value value * @returns */ const template = (rule, p, j) => { const value = p? .value? .value? .replace(/\n/g, '').replace(/\r/g, '').trim(); const { type } = p.parentPath.value; switch (type) { case 'JSXAttribute': return `{${templateScheme(value)}}`; // <div rule='Demo' ></div> } switch (rule) { case 'JSXText': // <div>Dddddd</div> return `{${templateScheme(value)}}`; Case 'TemplateLiteral': // Return templateTemplateLiteral(p, j); Case 'ExpressionStatement: StringLiteral: / / function in the case of internationalization' JSXElement: StringLiteral ': // <div>{Demo}</div> case 'ClassProperty:Literal': return `${templateScheme(value)}`; }}; * @param {*} p */ const templateTemplateLiteral = (p, j) => {let tel = "; const templateElements = j(p).find(j.TemplateElement); const identifiers = j(p).find(j.Identifier); templateElements.forEach((templateElement, index) => { const value = identifiers.at(index).length ? identifiers.at(index).get().value.name : ''; tel = `${tel}${templateElement.value.value.raw}${ value ? ` \${${value}} ` : '' }`; }); return templateScheme(tel); };Copy the code

Import {t} from “@lingui/macro” import {t} from “@lingui/macro” import {t} from “@lingui/macro”

/ const insertIntl = (j, ast) => {if (! ast .find(j.ImportDeclaration) .find(j.Identifier) .filter((item) => { return item.value.name === 't'; }).length && ! isFirstImport ) { isFirstImport = true; ast.get().node.program.body.unshift("import { t } from '@lingui/macro';" ); Console. log('import {t} from "@lingui/macro" insert succeeded '); }};Copy the code

reference

  • Plugin code address: github.com/lkxing/tran…
  • When doing research, I refer to an article on how the byte front end does international refactoring based on AST, which provides the main idea.

conclusion

The internationalization plug-ins in this article may not be applicable to other projects and are for reference only.