github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/dm/ui/plugins/vite-plugin-i18next-scanner/index.js (about)

     1  // src/context.ts
     2  import fs2 from 'fs'
     3  import { Parser } from 'i18next-scanner'
     4  import debug from 'debug'
     5  import workerpool from 'workerpool'
     6  
     7  // src/options.ts
     8  import path from 'path'
     9  import { defaults, defaultsDeep } from 'lodash'
    10  var defaultOptions = {
    11    input: ['src/**/*.{js,jsx,ts,tsx}'],
    12    output: './',
    13    options: {
    14      debug: false,
    15      removeUnusedKeys: true,
    16      sort: true,
    17      attr: {
    18        list: ['data-i18n'],
    19        extensions: ['.html', '.htm'],
    20      },
    21      func: {
    22        list: ['t', 'i18next.t', 'i18n.t'],
    23        extensions: ['.ts', '.tsx', '.js', '.jsx'],
    24      },
    25      trans: {
    26        component: 'Trans',
    27        i18nKey: 'i18nKey',
    28        defaultsKey: 'defaults',
    29        extensions: [],
    30        fallbackKey: false,
    31      },
    32      lngs: ['en'],
    33      defaultLng: 'en',
    34      defaultValue: function (_, __, key) {
    35        return key
    36      },
    37      resource: {
    38        loadPath: './locales/{{lng}}.json',
    39        savePath: './locales/{{lng}}.json',
    40        jsonIndent: 2,
    41        lineEnding: '\n',
    42      },
    43      nsSeparator: ':',
    44      keySeparator: '.',
    45      pluralSeparator: '_',
    46      contextSeparator: '_',
    47      contextDefaultValues: [],
    48      interpolation: {
    49        prefix: '{{',
    50        suffix: '}}',
    51      },
    52    },
    53  }
    54  var defaultPluginOptions = {
    55    langs: ['en'],
    56    outDir: 'locales',
    57    includes: ['src/**/*.{js,jsx,ts,tsx}'],
    58  }
    59  function mergePluginOptionToScannerOption(a, b) {
    60    const o = defaults(b, defaultPluginOptions)
    61    a.input = o.includes
    62    a.options.lngs = o.langs
    63    a.options.resource.savePath = path.join(o.outDir, '{{lng}}.json')
    64    a.options.resource.loadPath = path.join(o.outDir, '{{lng}}.json')
    65    return a
    66  }
    67  function normalizeOptions(o = {}) {
    68    const options = defaultsDeep({}, defaultOptions)
    69    return mergePluginOptionToScannerOption(options, o)
    70  }
    71  
    72  // src/context.ts
    73  import path2 from 'path'
    74  
    75  // src/fs.ts
    76  import fs from 'fs'
    77  
    78  // node_modules/.pnpm/detect-indent@7.0.0/node_modules/detect-indent/index.js
    79  var INDENT_REGEX = /^(?:( )+|\t+)/
    80  var INDENT_TYPE_SPACE = 'space'
    81  var INDENT_TYPE_TAB = 'tab'
    82  function makeIndentsMap(string, ignoreSingleSpaces) {
    83    const indents = new Map()
    84    let previousSize = 0
    85    let previousIndentType
    86    let key
    87    for (const line of string.split(/\n/g)) {
    88      if (!line) {
    89        continue
    90      }
    91      let indent
    92      let indentType
    93      let weight
    94      let entry
    95      const matches = line.match(INDENT_REGEX)
    96      if (matches === null) {
    97        previousSize = 0
    98        previousIndentType = ''
    99      } else {
   100        indent = matches[0].length
   101        indentType = matches[1] ? INDENT_TYPE_SPACE : INDENT_TYPE_TAB
   102        if (
   103          ignoreSingleSpaces &&
   104          indentType === INDENT_TYPE_SPACE &&
   105          indent === 1
   106        ) {
   107          continue
   108        }
   109        if (indentType !== previousIndentType) {
   110          previousSize = 0
   111        }
   112        previousIndentType = indentType
   113        weight = 0
   114        const indentDifference = indent - previousSize
   115        previousSize = indent
   116        if (indentDifference === 0) {
   117          weight++
   118        } else {
   119          const absoluteIndentDifference =
   120            indentDifference > 0 ? indentDifference : -indentDifference
   121          key = encodeIndentsKey(indentType, absoluteIndentDifference)
   122        }
   123        entry = indents.get(key)
   124        entry = entry === void 0 ? [1, 0] : [++entry[0], entry[1] + weight]
   125        indents.set(key, entry)
   126      }
   127    }
   128    return indents
   129  }
   130  function encodeIndentsKey(indentType, indentAmount) {
   131    const typeCharacter = indentType === INDENT_TYPE_SPACE ? 's' : 't'
   132    return typeCharacter + String(indentAmount)
   133  }
   134  function decodeIndentsKey(indentsKey) {
   135    const keyHasTypeSpace = indentsKey[0] === 's'
   136    const type = keyHasTypeSpace ? INDENT_TYPE_SPACE : INDENT_TYPE_TAB
   137    const amount = Number(indentsKey.slice(1))
   138    return { type, amount }
   139  }
   140  function getMostUsedKey(indents) {
   141    let result
   142    let maxUsed = 0
   143    let maxWeight = 0
   144    for (const [key, [usedCount, weight]] of indents) {
   145      if (usedCount > maxUsed || (usedCount === maxUsed && weight > maxWeight)) {
   146        maxUsed = usedCount
   147        maxWeight = weight
   148        result = key
   149      }
   150    }
   151    return result
   152  }
   153  function makeIndentString(type, amount) {
   154    const indentCharacter = type === INDENT_TYPE_SPACE ? ' ' : '	'
   155    return indentCharacter.repeat(amount)
   156  }
   157  function detectIndent(string) {
   158    if (typeof string !== 'string') {
   159      throw new TypeError('Expected a string')
   160    }
   161    let indents = makeIndentsMap(string, true)
   162    if (indents.size === 0) {
   163      indents = makeIndentsMap(string, false)
   164    }
   165    const keyOfMostUsedIndent = getMostUsedKey(indents)
   166    let type
   167    let amount = 0
   168    let indent = ''
   169    if (keyOfMostUsedIndent !== void 0) {
   170      ;({ type, amount } = decodeIndentsKey(keyOfMostUsedIndent))
   171      indent = makeIndentString(type, amount)
   172    }
   173    return {
   174      amount,
   175      type,
   176      indent,
   177    }
   178  }
   179  
   180  // src/fs.ts
   181  var DEFAULT_INDENT = '  '
   182  function readJsonFile(path3) {
   183    const file = fs.readFileSync(path3, 'utf8') || '{}'
   184    const indent = detectIndent(path3).indent || DEFAULT_INDENT
   185    return {
   186      path: path3,
   187      json: JSON.parse(file),
   188      indent,
   189    }
   190  }
   191  
   192  // src/context.ts
   193  var dbg = debug('vite-plugin-i18next-scanner:context')
   194  var Context = class {
   195    constructor(options = {}) {
   196      this.server = null
   197      this.pool = null
   198      this.pluginOptions = options
   199      this.scannerOptions = normalizeOptions(options)
   200      dbg('scannerOptions: %o', this.scannerOptions)
   201    }
   202    async startScanner(server) {
   203      if (this.server === server) {
   204        return
   205      }
   206      if (this.pool) {
   207        await this.pool.terminate()
   208      }
   209      this.server = server
   210      this.pool = workerpool.pool(__dirname + '/worker.js', {
   211        minWorkers: 'max',
   212        maxWorkers: 1,
   213      })
   214      await this.scanAll()
   215      this.watch(server.watcher)
   216    }
   217    watch(watcher) {
   218      watcher.on('change', p => this.handleFileChange(p))
   219      watcher.on('unlink', p => this.handleFileUnlink(p))
   220    }
   221    passExtensionCheck(p) {
   222      const extname = path2.extname(p)
   223      return (
   224        this.scannerOptions.options.func.extensions.includes(extname) ||
   225        this.scannerOptions.options.attr.extensions.includes(extname) ||
   226        this.scannerOptions.options.trans.extensions.includes(extname)
   227      )
   228    }
   229    async handleFileUnlink(p) {
   230      if (this.passExtensionCheck(p)) {
   231        await this.scanAll()
   232      }
   233    }
   234    async handleFileChange(p) {
   235      dbg(`scanning ${p}`)
   236      if (!this.passExtensionCheck(p)) {
   237        return
   238      }
   239      const content = fs2.readFileSync(p, 'utf8')
   240      const parser = new Parser(this.scannerOptions.options)
   241      if (!content) {
   242        return
   243      }
   244      parser.parseFuncFromString(content)
   245      const translations = parser.get()
   246      const resourceFromFile = Object.keys(translations).reduce((acc, key) => {
   247        acc[key] = translations[key].translation
   248        return acc
   249      }, {})
   250      dbg('resource from file: %o', resourceFromFile)
   251      const hasKey = Object.keys(resourceFromFile).some(lang => {
   252        return Object.keys(resourceFromFile[lang]).length > 0
   253      })
   254      if (!hasKey) {
   255        dbg('no key found')
   256        return
   257      }
   258      let shouldScanAll = false
   259      Object.keys(resourceFromFile).forEach(lang => {
   260        const languageResource = path2.resolve(
   261          this.scannerOptions.options.resource.savePath.replace('{{lng}}', lang)
   262        )
   263        const { json } = readJsonFile(languageResource)
   264        if (Object.keys(resourceFromFile[lang]).some(key => !(key in json))) {
   265          shouldScanAll = true
   266        }
   267      })
   268      if (shouldScanAll) {
   269        await this.scanAll()
   270      } else {
   271        dbg('no need to scan all')
   272      }
   273    }
   274    async scanAll() {
   275      if (!this.pool) {
   276        return
   277      }
   278      dbg('scanning and regenerating all resources...')
   279      const worker = await this.pool.proxy()
   280      await worker.scanAndGenerateResource(
   281        this.scannerOptions.input,
   282        this.scannerOptions.output,
   283        this.pluginOptions
   284      )
   285      dbg('done scanning and regenerating all resources')
   286    }
   287  }
   288  
   289  // src/index.ts
   290  function i18nextScanner(options) {
   291    const ctx = new Context(options)
   292    return {
   293      name: 'vite-plugin-i18next-scanner',
   294      apply: 'serve',
   295      async configureServer(server) {
   296        await ctx.startScanner(server)
   297      },
   298    }
   299  }
   300  export { i18nextScanner }