-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
1,724 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
useTabs: false | ||
tabWidth: 2 | ||
printWidth: 300 | ||
singleQuote: true | ||
semi: false | ||
trailingComma: 'all' | ||
arrowParens: 'always' | ||
bracketSpacing: true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,203 @@ | ||
import path from 'path' | ||
|
||
export function hasChinese(str = '') { | ||
if (str.includes('$t(')) return false | ||
return /[\u4E00-\u9FA5]+/g.test(str) | ||
} | ||
|
||
export function hasVueText(text = '') { | ||
if (text.includes('{{') && text.includes('}}')) return true | ||
return false | ||
} | ||
|
||
// 递归解析所有 Vue 表达式并生成数组 | ||
// ABC昵称{{ userInfo.A }}你好啊!{{ userInfo.B }}卧槽 | ||
// TO | ||
// ["ABC昵称","{{ userInfo.A }}","你好啊!","{{ userInfo.B }}",卧槽] | ||
export function htmlTextVueExpressParse(text = '') { | ||
const result: string[] = [] | ||
function inner(point: number): number { | ||
if (point > text.length - 1) return 0 | ||
const left = text.indexOf('{{', point) | ||
const right = text.indexOf('}}', left) | ||
if (right != -1 && left != -1) { | ||
result.push(text.slice(point, left)) | ||
const vueExpress = text.slice(left, right + 2) | ||
if (hasChinese(vueExpress)) throw new Error('The expression cannot contain Chinese characters') | ||
result.push(vueExpress) | ||
return inner(right + 2) | ||
} | ||
result.push(text.slice(point, text.length)) | ||
return 0 | ||
} | ||
inner(0) | ||
return result | ||
} | ||
|
||
export function hasVueAttr(attrName = '') { | ||
const keywords = ['v-', '@', ':'] | ||
for (const iterator of keywords) { | ||
if (attrName.indexOf(iterator) === 0) { | ||
return true | ||
} | ||
} | ||
return false | ||
} | ||
|
||
export function fillNumber(index = 0) { | ||
if (index < 10) return `00${index}` | ||
if (index < 100) return `0${index}` | ||
if (index < 1000) return `${index}` | ||
return String(index) | ||
} | ||
|
||
export function setObjectAttr(root: any, attrPath: string[], key: string, value: string) { | ||
let current = root | ||
for (let index = 0; index < attrPath.length; index++) { | ||
const element = attrPath[index] | ||
if (!current[element]) { | ||
current[element] = {} | ||
} | ||
current = current[element] | ||
} | ||
current[key] = value | ||
} | ||
|
||
export function getObjectAttr(root: ObjectMap, attrPath: string[]): JsonMap | null { | ||
let current = root | ||
for (let index = 0; index < attrPath.length; index++) { | ||
const element = attrPath[index] | ||
if (!current[element]) return null | ||
current = current[element] | ||
} | ||
return current | ||
} | ||
|
||
export function includeCommonLang(commonLang: JsonMap, text: string) { | ||
const prefix = 'CommonText' | ||
let i = 1 | ||
for (const key in commonLang) { | ||
const cText = commonLang[key] | ||
i++ | ||
if (cText === text) { | ||
return `${prefix}.${key}` | ||
} | ||
} | ||
const fullNumber = fillNumber(i) | ||
commonLang[fullNumber] = text | ||
return `${prefix}.${fullNumber}` | ||
} | ||
|
||
export function isExistsTextForLang(prefix: string, map: JsonMap, text: string): string { | ||
for (const key in map) { | ||
const element = map[key] | ||
if (element === text) { | ||
return `${prefix}.${key}` | ||
} | ||
} | ||
return '' | ||
} | ||
|
||
export function includeLangMap(filePrefix: string, index: number, text: string, commonLang: JsonMap, outputLanguageConfig: JsonMap) { | ||
let isBreak = false | ||
let tFnKey = `${filePrefix}.${fillNumber(index)}` | ||
if (text.length <= 4) { | ||
tFnKey = includeCommonLang(commonLang, text) | ||
isBreak = true | ||
} else if (isExistsTextForLang(filePrefix, outputLanguageConfig, text) !== '') { | ||
tFnKey = isExistsTextForLang(filePrefix, outputLanguageConfig, text) | ||
isBreak = true | ||
} else { | ||
outputLanguageConfig[fillNumber(index)] = text | ||
} | ||
return { | ||
tFnKey, | ||
isBreak, | ||
} | ||
} | ||
|
||
// 寻找文件中最大的编码序号,并同时忽略 CommonText 内容 | ||
// $t 不能换行 | ||
export function findMax$tNumber(code = '') { | ||
const lines = code.split('\n') | ||
let max = 0 | ||
lines.forEach((line) => { | ||
function inner(start = 0) { | ||
const sp = line.indexOf('$t(', start) | ||
const commonTextP = line.indexOf('CommonText', start) | ||
const ep = line.indexOf(')', sp) | ||
if (sp > 0 && ep > 0 && (commonTextP == -1 || commonTextP > ep)) { | ||
let keyExpress = line.slice(sp + 3, ep) | ||
const keyExpressArr = keyExpress.replace(/\'/gim, '').split('') | ||
let n = '' | ||
let find = false | ||
for (const ch of keyExpressArr) { | ||
if (!isNaN(Number(ch))) { | ||
find = true | ||
n += String(ch) | ||
} else if (find) { | ||
break | ||
} | ||
} | ||
if (Number(n) > max) max = Number(n) | ||
inner(ep + 1) | ||
} | ||
} | ||
inner() | ||
}) | ||
return max | ||
} | ||
|
||
// 寻找文件中最大的编码序号,并同时忽略 CommonText 内容 | ||
// $t 不能换行 | ||
export function findMax$tNumberByLangFile(language: ObjectMap, prefix: string) { | ||
let max = 0 | ||
const keyList = getObjectAttr(language, prefix.split('.')) | ||
for (const key in keyList) { | ||
if (!isNaN(Number(key)) && Number(key) > max) { | ||
max = Number(key) | ||
} | ||
} | ||
return max | ||
} | ||
|
||
// 取路径最后两项作为语言后缀 | ||
// /Users/wangkun/Documents/SupportProject/weplay-admin-huafu-project/src/view/Censor/components/guardList.vue | ||
export function buildLangPrefix(targetPath: string) { | ||
let prefix1 = '' | ||
let prefix2 = '' | ||
const prefix3 = targetPath.split('/').slice(-2) | ||
|
||
let tmpFlag = false | ||
for (const iterator of targetPath.split('/')) { | ||
if (iterator === 'view') continue | ||
if (iterator === 'src') { | ||
tmpFlag = true | ||
continue | ||
} | ||
if (tmpFlag) { | ||
prefix1 = iterator | ||
break | ||
} | ||
} | ||
if (prefix3[0] === prefix1) prefix3.shift() | ||
prefix2 = prefix3.join('_').replace(path.extname(prefix3.join('')), '') | ||
|
||
// 语言文件最终前缀 | ||
let filePrefix = [prefix1, prefix2].join('.') | ||
return filePrefix | ||
} | ||
|
||
export interface ReturnValue { | ||
index: number | ||
output: string | ||
lang: { [key: string]: any } | ||
} | ||
|
||
export interface JsonMap { | ||
[key: string]: string | ||
} | ||
|
||
export interface ObjectMap { | ||
[key: string]: any | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
import fs from 'fs' | ||
import * as htmlparser2 from 'htmlparser2' | ||
import { render } from 'dom-serializer' | ||
import { fillNumber, hasChinese, hasVueAttr, hasVueText, htmlTextVueExpressParse, includeCommonLang, includeLangMap, isExistsTextForLang, JsonMap, ReturnValue, setObjectAttr } from './common' | ||
|
||
function htmlTextFilter(html: string) { | ||
html = html.replace(/\n/gim, '') | ||
html = html.trim() | ||
return html | ||
} | ||
|
||
export function generatorHTML(html: string, filePrefix: string, index: number, commonLang: JsonMap): ReturnValue { | ||
// DOM 树生成 | ||
const dom = htmlparser2.parseDocument(html, { | ||
xmlMode: true, | ||
lowerCaseTags: false, | ||
lowerCaseAttributeNames: false, | ||
}) | ||
|
||
const outputLanguageConfig: { | ||
[key: string]: any | ||
} = {} | ||
|
||
// HTML 属性转换 | ||
function forEachAttr(node: any) { | ||
for (const key in node.attribs) { | ||
const text = node.attribs[key] | ||
if (!hasChinese(text)) continue | ||
if (hasVueAttr(key)) continue // 忽略 Vue 动态属性 | ||
|
||
const { tFnKey, isBreak } = includeLangMap(filePrefix, index, text, commonLang, outputLanguageConfig) | ||
const textVueTemplate = `$t('${tFnKey}')` | ||
|
||
// 删除旧的 HTML 属性,新增i18n绑定 | ||
delete node.attribs[key] | ||
node.attribs[`:${key}`] = textVueTemplate | ||
|
||
console.log('HTML 属性转换:', node.name, key, text, '->', textVueTemplate) | ||
|
||
if (!isBreak) index += 1 | ||
} | ||
} | ||
|
||
// 文本常量转换 | ||
function constDefineOutput(keyword: string) { | ||
if (hasVueText(keyword) || !hasChinese(keyword)) return keyword | ||
const { tFnKey, isBreak } = includeLangMap(filePrefix, index, keyword, commonLang, outputLanguageConfig) | ||
const textVueTemplate = `{{ $t('${tFnKey}') }}` | ||
console.log('HTML 文本转换:', keyword, '->', textVueTemplate) | ||
if (!isBreak) index += 1 | ||
return textVueTemplate | ||
} | ||
|
||
// Vue 表达式文本转换 | ||
function expressDefineOutput(keyword: string) { | ||
let expressArray: string[] = [] | ||
try { | ||
expressArray = htmlTextVueExpressParse(keyword) | ||
} catch (error) { | ||
return keyword | ||
} | ||
const vueParams: string[] = [] | ||
let paramsIndex = 0 | ||
for (let index = 0; index < expressArray.length; index++) { | ||
const element = expressArray[index] | ||
if (element.includes('{{') && element.includes('}}')) { | ||
vueParams.push(element.slice(2, -2)) | ||
expressArray[index] = `{${paramsIndex}}` | ||
paramsIndex++ | ||
} | ||
} | ||
const $tExpress = expressArray.join('') | ||
const $tParams = JSON.stringify(vueParams).replace(/\"/gim, '') | ||
const { tFnKey, isBreak } = includeLangMap(filePrefix, index, $tExpress, commonLang, outputLanguageConfig) | ||
const textVueTemplate = `{{ $t('${tFnKey}', ${$tParams}) }}` | ||
console.log('HTML 表达式转换:', keyword, '->', textVueTemplate) | ||
if (!isBreak) index += 1 | ||
return textVueTemplate | ||
} | ||
|
||
// 递归DOM树,根据其特点批量转换所有节点 | ||
function walk(dom: any): any { | ||
for (const node of dom.children) { | ||
forEachAttr(node) | ||
if (node.children?.length > 0) { | ||
walk(node) | ||
continue | ||
} | ||
if (node.type === 'text') { | ||
const text = htmlTextFilter(node.data) | ||
if (!hasChinese(text)) continue | ||
|
||
if (!hasVueText(text)) { | ||
node.data = constDefineOutput(text) | ||
} else { | ||
node.data = expressDefineOutput(text) | ||
} | ||
} | ||
} | ||
} | ||
|
||
walk(dom) | ||
|
||
// DOM 树生成 HTML | ||
const outputHtml = render(dom, { | ||
encodeEntities: false, | ||
decodeEntities: false, | ||
xmlMode: false, | ||
selfClosingTags: false, | ||
}) | ||
|
||
return { | ||
index, | ||
output: outputHtml, | ||
lang: outputLanguageConfig, | ||
} | ||
} | ||
|
||
// Node 类型 | ||
// Document { | ||
// parent: null, | ||
// prev: null, | ||
// next: null, | ||
// startIndex: null, | ||
// endIndex: null, | ||
// children: [ | ||
// Element { | ||
// parent: [Circular *1], | ||
// prev: null, | ||
// next: [Text], | ||
// startIndex: null, | ||
// endIndex: null, | ||
// children: [Array], | ||
// name: 'template', | ||
// attribs: {}, | ||
// type: 'tag' | ||
// }, | ||
// Text { | ||
// parent: [Circular *1], | ||
// prev: [Element], | ||
// next: null, | ||
// startIndex: null, | ||
// endIndex: null, | ||
// data: '\n', | ||
// type: 'text' | ||
// } | ||
// ], | ||
// type: 'root' | ||
// } |
Oops, something went wrong.