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 }