抽取国际化自动化脚本(多模块)
发布于 4 个月前 作者 qjzd 266 次浏览 来自 码农

抽取国际化自动化脚本

模块抽取

抽离modules目录下的模块下的文件的国际化文本,但不包含子模块的文件。 存放在模块的zh-CN.json和en.json文件中

组件抽取

抽离组件的国际化(key为componentIntl的参数,@componentIntl(‘国际化key’)) 存放在src/static/locale/comp目录下

其他

ae约定所有模块中有且仅有一个index.jsx,故根据模块的index.jsx确定模块的位置 模块的目录必须是数字开头,一是可以排序,二是为了区分模块下的子模块目录与模块依赖目录。如果不以数字开头,抽取国际化文本,会被当做模块的依赖目录,从而父模块会多出这些多余的国际化文本。 如果原来国际化文件中的key不再被引用,就被删除,除非配置了KEY_RULES 如果已经原来的国际化文件中的key已存在,则该key和对应的value会被保留下来。、

核心源码

'use strict'
var jsonfile = require('jsonfile')
const utils = require('./utils')
const vfs = require('vinyl-fs')
const map = require('map-stream')
const { I18N_TYPES, CMP_KEEP_KEY_RULES, MODULE_KEEP_KEY_RULES } = require('./common')
const path = require('path')
const uniq = require('lodash/uniq')

const pathResolve = utils.pathResolve
const fsExistsSync = utils.fsExistsSync
const filterObjByKeyRules = utils.filterObjByKeyRules

// 铁路图地址: https://jex.im/regulex/
const regexps = {
  regI18n: /__\(['"](.+?)['"]/g,
  regCmpIntl: /[@componentIntl](/user/componentIntl)\(['"](.+?)['"]/g
}

// 组件:文件中存在“[@componentIntl](/user/componentIntl)”关键字都属于组件
// 模块:modules目录下的模块下的文件,但不包含子模块的文件。

// C下存放组件的国际化(key为componentIntl的参数,[@componentIntl](/user/componentIntl)('国际化key'))
// M下存在模块的国际化(key为模块路径)
// G下存放不属于C和M的
const i18nData = { 'C': {}, 'M': {}, 'G': [] }
const tmpRegData = {}

// 从文件中提取[@componentIntl](/user/componentIntl),用来判断是否是一个组件文件
function getCmpIntlKey(fileContent) {
  tmpRegData.matches = new RegExp(regexps.regCmpIntl).exec(fileContent)
  return tmpRegData.matches && tmpRegData.matches[1]
}

// 从文件中提取模块的的国际化KEY
function getModuleI18nData (modulePath, fileContent) {
  const regI18n = new RegExp(regexps.regI18n)
  while ((tmpRegData.matches = regI18n.exec(fileContent))) {
    if (!i18nData.M[modulePath]) {
      i18nData.M[modulePath] = []
    }
    i18nData.M[modulePath].push(tmpRegData.matches[1])
  }
}

// 从文件中提取组件的的国际化KEY
function getComI18nData(cmpKey, fileContent) {
  const regI18n = new RegExp(regexps.regI18n)
  while ((tmpRegData.matches = regI18n.exec(fileContent))) {
    if (!i18nData.C[cmpKey]) {
      i18nData.C[cmpKey] = []
    }
    i18nData.C[cmpKey].push(tmpRegData.matches[1])
  }
}

// 从文件中提取不是的模块也不是组件的国际化数据
function getGlobalI18nData(fileContent) {
  const regI18n = new RegExp(regexps.regI18n)
  while ((tmpRegData.matches = regI18n.exec(fileContent))) {
    i18nData.G.push(tmpRegData.matches[1])
  }
}

// 删除重复的key,并排序方便git比对
function normalizeI18nData () {
  const moduleKeys = Object.keys(i18nData.M)
  moduleKeys.forEach(key => {
    i18nData.M[key] = uniq(i18nData.M[key]).sort()
  })

  const cmpKeys = Object.keys(i18nData.C)
  cmpKeys.forEach(key => {
    i18nData.C[key] = uniq(i18nData.C[key]).sort()
  })

  i18nData.G = uniq(i18nData.G).sort()
}

// 保存国际化文件
function saveI18nFile({
  dirPath,
  makeNewData
} = {}) {

  function defaultMakeNewData(originData) {
    return originData
  }

  I18N_TYPES.forEach(item => {
    const langFilePath = path.resolve(dirPath, item + '.json')
    if (!fsExistsSync(langFilePath)) {
      jsonfile.writeFileSync(langFilePath, {}, { spaces: 2, EOL: '\n' })
    }

    const originData = jsonfile.readFileSync(langFilePath) || {}
    makeNewData = makeNewData || defaultMakeNewData
    const newData = makeNewData(originData)

    jsonfile.writeFile(langFilePath, newData, { spaces: 2, EOL: '\n' }, err => {
      if (err) return console.log('提取失败' + langFilePath + '\n' + err)
      console.log('提取完成' + langFilePath)
    })
  })
}

// 保存模块的I18n文件
function saveModuleI18nFile() {
  const moduleKeys = Object.keys(i18nData.M)
  moduleKeys.forEach(key => {
    saveI18nFile({
      dirPath: key,
      makeNewData (originData) {
        const newData = filterObjByKeyRules(originData, MODULE_KEEP_KEY_RULES) // 根据配置保留一些keys值,保证即使在项目中不被引用也能保存下来
        i18nData.M[key].forEach(key => {
          if (originData.hasOwnProperty(key)) {
            newData[key] = originData[key]
          } else {
            newData[key] = key
          }
        })
        return newData
      }
    })
  })
}

// 保存组件的的I18n文件
function saveCmpI18nFile() {
  const path = pathResolve.src('static/locale/comp')
  saveI18nFile({
    dirPath: path,
    makeNewData (originData) {
      const cmpKeys = Object.keys(i18nData.C)
      const newData = filterObjByKeyRules(originData, CMP_KEEP_KEY_RULES) // 根据配置保留一些keys值,保证即使在项目中不被引用也能保存下来
      cmpKeys.forEach(cmpKey => {
        if (!newData[cmpKey]) newData[cmpKey] = {}
        i18nData.C[cmpKey].forEach(key => {
          if (originData[cmpKey] && originData[cmpKey].hasOwnProperty(key)) {
            newData[cmpKey][key] = originData[cmpKey][key]
          } else {
            newData[cmpKey][key] = key
          }
        })
      })
      return newData
    }
  })
}

// 保存全局的文件
function saveGlobalI18nFile() {
  // 暂无
}

vfs.src([
  pathResolve.src('!(modules)/**/*.+(jsx|js)')
], { dot: false }).pipe(map((file, cb) => {
  const contents = file.contents.toString()
  const cmpKey = getCmpIntlKey(contents)
  if (cmpKey) {
    getComI18nData(cmpKey, contents)
  } else {
    getGlobalI18nData(contents)
  }
  cb(null)
})).on('end', () => {
  vfs.src([
    pathResolve.src('**/modules/**/index.jsx') // ae约定所有模块中有且仅有一个index.jsx,故根据模块的index.jsx确定模块的位置
  ], {
    dot: false
  }).pipe(map((file, cb) => {
    const modulePath = path.resolve(file.path, '..')
    vfs.src([
      path.resolve(modulePath, '!(modules|[0123456789]*)/**/*.+(jsx|js)'), // 非模块目录下文件,遍历底下所有的js和jsx文件
      path.resolve(modulePath, '*.+(jsx|js)') // 当前模块下直接的的js和jsx文件,子目录不会遍历
    ], {dot: false}).pipe(map((file, cb) => {
      const contents = file.contents.toString()
      const cmpKey = getCmpIntlKey(contents)
      if (cmpKey) {
        getComI18nData(cmpKey, contents)
      } else {
        getModuleI18nData(modulePath, contents)
      }
      cb(null)
    })).on('end', () => {
      cb(null)
    })
  })).on('end', () => {
    normalizeI18nData()

    saveModuleI18nFile()
    saveCmpI18nFile()
    saveGlobalI18nFile()
  })
})

附上另外依赖的文件代码

common.js

const I18N_TYPES = [
  'zh-CN',
  'en'
]

// 组件的国际化的json文件需要被保留下的key,即使这些组件在项目中没有被引用
// 比如重写AE的内部组件的国际化文件
// key可以是一个字符串,正则,或者是函数
const CMP_KEEP_KEY_RULES = [
  /^AE/
]

// 模块的国际化的json文件需要被保留下的key,即使这些组件在项目中没有被引用
// 这里做预留,暂时无使用场景
// key可以是一个字符串,正则,或者是函数
const MODULE_KEEP_KEY_RULES = [
]

module.exports = {
  I18N_TYPES,
  CMP_KEEP_KEY_RULES,
  MODULE_KEEP_KEY_RULES
}

utils.js

'use strict'
const path = require('path')
const fs = require('fs')

const rootPathResolve = function () {
  return path.resolve.apply(path.resolve, [__dirname, '../../'].concat(Array.prototype.slice.call(arguments)))
}

const pathResolve = {
  root: rootPathResolve,
  src: function () {
    return rootPathResolve.apply(rootPathResolve, ['src'].concat(Array.prototype.slice.call(arguments)))
  },
  dist: function () {
    return rootPathResolve.apply(rootPathResolve, ['../webapp'].concat(Array.prototype.slice.call(arguments)))
  }
}

exports.pathResolve = pathResolve

// 判断文件是否存在
function fsExistsSync(path) {
  try{
    fs.accessSync(path, fs.F_OK)
  }catch(e){
    return false;
  }
  return true;
}

exports.fsExistsSync = fsExistsSync

// 过滤出满足规则的key,规则可以是一个字符串,正则或者函数
function filterObjByKeyRules(obj = {}, keyRules = []) {
  const newObj = {}
  if (keyRules.length === 0) {
    return newObj
  }
  const keys = Object.keys(obj)
  keys.forEach(key => {
    for (let i = 0; i < keyRules.length; i++) {
      const keyRole = keyRules[i]
      if ((Object.prototype.toString.call(keyRole) === '[object RegExp]' && keyRole.test(key))
        || (Object.prototype.toString.call(keyRole) === '[object Function]' && keyRole(key))
        || keyRole === key) {
        newObj[key] = obj[key]
        break
      }
    }
  })
  return newObj
}

exports.filterObjByKeyRules = filterObjByKeyRules
回到顶部