通过 babel 国际化

421 阅读2分钟

看到一些老的系统通过手动配置语言的json文件,正好学习babel,所以通过babel去生成语言文件。

整体思路

通过babel配置使用的语言,然后从代码中晒出字符串并生成文件,接第三方或者手动配置语言文件(项目如果手动需要执行俩次打包,一次生成文件,第二次去应用)

通过babel.config 去配置使用的语言文件 配置了zh-CN

module.exports = {
  "plugins": [
    ["./src/i18n/i18n.plugin.js", {locale: 'zh-CN'}],
    "@babel/syntax-jsx" //配置jsx语法支持
  ]
}

接下来在遍历ast之前生成转换函数文件
转换函数文件在这块我是写了固定的路径,在项目中可以通过webpack配置别名如 :@:'src',引入可以改成import xx from '@/util/xxx.js',也可以通过externals配置去像node_modules一样去引用,这样就可以在项目中每个js文件都通用这个转换函数文件了,下面只是demo 所以转换函数文件放在了code同级目录,通过from './xxx'引用

pre(file) {
  // 语言包的文件路径 此处locale是options传进来的
  let localePath = `./${locale}.json`
  // 判断转换函数的文件是否存在
  if(!fs.pathExistsSync(process.cwd()+'/src/i18n/i18n-util.js')) {
    let transformCode = `
        import locale from '${localePath}'
        export default function transform(key,...args) {
            return locale[key].replace(/\{placeholder\}/, () => args[index++])
        }
    `
    fs.outputFileSync(process.cwd()+'/src/i18n/i18n-util.js', transformCode)
  }
  file.localeId = 0  // 唯一标识的key
  file.outputFile = {} // 最终输出的locale
}

接下来是操作ast 首先要把转换函数文件引入,如果import了就不需要再引入,否则要引入

  Program: {
    enter(path, state) {
      path.traverse({
          ImportDeclaration(curpath) {
              let requirePath = curpath.get('source')
              let requireValue = requirePath.node.value
              if(requireValue === './i18n-util.js') {
                  let importPath = curpath.get('specifiers')[0].get('local')
                  let importIdentifier = importPath.node.name
                  state.importIdentifier = importIdentifier
                  curpath.stop()
              }
          }
      })
      if(!state.importIdentifier) {
          state.importIdentifier  = importModule.addDefault(path, './i18n-util.js',{
              nameHint: path.scope.generateUid('./i18n-util.js')
          }).name
      }
    }
  }

string 与模板字符串处理,要注意模板字符串的插值与jsx属性 jsx属性做了属性名称+标识的key

StringLiteral(path, state) {
  if(isStep(path)) return
  // 能找到说明是jsx 的 attr
  let parent = path.findParent(p => p.isJSXAttribute())
  let localeId = ++state.file.localeId
  // 统计到输出json的对象中
  state.file.outputFile[`${parent?parent.node.name.name:''}${localeId}`] = path.node.value
  let replaceExpression = api.template.ast(`${state.importIdentifier}('${parent?parent.node.name.name:''}${localeId}')`).expression
  // jsx attr 的值要加{}
  if (path.findParent(p => p.isJSXAttribute()) && !path.findParent(p=> p.isJSXExpressionContainer())) {
      replaceExpression = api.types.JSXExpressionContainer(replaceExpression);
  }
  path.replaceWith(replaceExpression)
  path.skip()
},
TemplateLiteral(path, state) {
  if(isStep(path)) return
  // 能找到说明是jsx 的 attr
  let parent = path.findParent(p => p.isJSXAttribute())
  // ${xxx + xxx} 转为code
  let localeId = ++state.file.localeId
  state.file.outputFile[`${parent?parent.node.name.name:''}${localeId}`] = path.get('quasis').map(item => item.node.value.raw).join('{placeholder}')
  const value = path.node.expressions.map(item => generate(item).code)
  let replaceExpression = api.template.ast(`${state.importIdentifier}('${parent?parent.node.name.name:''}${localeId}',${value.join(',')})`).expression
  if (path.findParent(p => p.isJSXAttribute()) && !path.findParent(p=> p.isJSXExpressionContainer())) {
      replaceExpression = api.types.JSXExpressionContainer(replaceExpression);
  }
  path.replaceWith(replaceExpression)
  path.skip()
}

语言文件的输出

post(file) {
    let {
        outputFile,
    } = file
    let localeZhPath = `./src/i18n/zh-CN.json`
    let localeEnPath = `./src/i18n/en-US.json`
    fs.outputFileSync(path.join(process.cwd(), localeZhPath), JSON.stringify(outputFile, null, 2))
    fs.outputFileSync(path.join(process.cwd(), localeEnPath), JSON.stringify(outputFile, null, 2))
}

最终输出

code

import _i18nUtilJs2 from "./i18n-util.js";

function App() {
  const title = _i18nUtilJs2('1');

  const desc = _i18nUtilJs2('2');

  const desc2 =
  /*i18n-disable*/
  `desc`;

  const desc3 = _i18nUtilJs2('3', title + desc, desc2);

  return <div className={_i18nUtilJs2('className4')} title={_i18nUtilJs2('title5')}>
        <img src={Logo} />
        <h1>{title}</h1>
        <p>{desc}</p>  
        <div>
        {
      /*i18n-disable*/
      '中文'}
        </div>
      </div>;
}

util

import locale from './i18n/zh-CN.json'
export default function transform(key,...args) {
    return locale[key].replace(/{placeholder}/, () => args[index++])
}

locale

{
  "1": "title",
  "2": "desc",
  "3": "aaa {placeholder} bbb {placeholder} ccc",
  "className4": "app",
  "title5": "测试"
}

(代码仓库):[gitee.com/summarize/b…]