🔧 自动翻译插件中的智能字符串切割方案详解
问题背景
在开发 auto-i18n-translation-plugins
时遇到核心问题:如何处理包含HTML标签和文本的混合字符串?
比如Vue3对于静态节点就是不会编译的,当你得到一个带有html的代码块时,如果拿去进行翻译api的翻译就会使得翻译过程非常不可控,甚至导致语法错误渲染异常
javascript
// 问题示例:这种字符串如何精准切割?
"欢迎来到<strong>官方网站</strong>!请点击<button>登录</button>继续。"
传统AST分析无法区分哪些需要翻译,哪些是代码结构。
核心思路:三阶段渐进式切割
设计理念:从粗到细的渐进式处理,确保语义完整性和结构保护
scss
原始字符串 → 粗分割(最细粒度) → 语义重组 → 结构合并 → AST模板字符串
第一阶段:粗分割(最细粒度分解)
目标
将字符串按照目标语言字符和标点符号拆分成最小的语义单元。
代码逻辑
typescript
// 定义标点符号的正则表达式
const punctuationRegex = /[,。?!《》,..:!?""'';'"、0-9\n\r\t\v\f]/
// 创建组合正则:目标语言 OR 标点符号
const splitRegex = new RegExp(
`(${separatorRegex.source}|${punctuationRegex.source})`,
separatorRegex.flags
)
// 执行分割
const splitArr = str.split(splitRegex).filter(Boolean)
实际效果演示
输入字符串:
css
"欢迎来到<strong>官方网站</strong>!请点击<button>登录</button>继续。"
粗分割结果:
javascript
[
'欢', '迎', '来', '到', '<strong>', '官', '方', '网', '站',
'</strong>', '!', '请', '点', '击', '<button>', '登', '录',
'</button>', '继', '续', '。'
]
// 共21个片段,每个汉字、标点、HTML标签都被拆成最小单元
关键特点:
- 每个汉字单独分离:
'欢', '迎', '来', '到'
- 标点符号独立:
'!', '。'
- HTML标签完整保留:
'<strong>', '</strong>'
第二阶段:语义重组(智能连接)
目标
将语义相关的片段重新连接,形成有意义的翻译单元。
代码逻辑
typescript
// 定义连接规则:目标语言字符 OR 特定标点符号
const connectPunctuationRegex = /[,。?!《》,..:!?;'"、0-9]/
const connectRegex = new RegExp(
`(${separatorRegex.source}|${connectPunctuationRegex.source})`,
separatorRegex.flags
)
// 智能连接逻辑
for (const item of splitArr) {
if (connectRegex.test(item)) {
// 符合连接条件,加入当前片段
currentMatch += item
} else {
// 不符合连接条件,结束当前片段
if (currentMatch) {
result.push(currentMatch)
currentMatch = ''
}
result.push(item) // HTML标签等独立推入
}
}
实际效果演示
处理过程详解:
javascript
// 遍历粗分割结果:
'欢' → 中文字符 → 连接 → currentMatch = '欢'
'迎' → 中文字符 → 连接 → currentMatch = '欢迎'
'来' → 中文字符 → 连接 → currentMatch = '欢迎来'
'到' → 中文字符 → 连接 → currentMatch = '欢迎来到'
'<strong>' → 非连接条件 → 推出'欢迎来到',独立推入'<strong>'
'官' → 中文字符 → 连接 → currentMatch = '官'
'方' → 中文字符 → 连接 → currentMatch = '官方'
'网' → 中文字符 → 连接 → currentMatch = '官方网'
'站' → 中文字符 → 连接 → currentMatch = '官方网站'
'</strong>' → 非连接条件 → 推出'官方网站',独立推入'</strong>'
'!' → 标点符号 → 连接 → currentMatch = '!'
'请' → 中文字符 → 连接 → currentMatch = '!请'
'点' → 中文字符 → 连接 → currentMatch = '!请点'
'击' → 中文字符 → 连接 → currentMatch = '!请点击'
'<button>' → 非连接条件 → 推出'!请点击',独立推入'<button>'
// ... 继续处理
语义重组结果:
javascript
[
'欢迎来到', '<strong>', '官方网站', '</strong>',
'!请点击', '<button>', '登录', '</button>', '继续。'
]
关键变化:
- 连续汉字合并:
'欢迎来到'
、'官方网站'
、'继续'
- 标点与文字合并 :
'!请点击'
、'继续。'
- HTML标签保持独立:
'<strong>'
、'</strong>'
第三阶段:结构合并(代码结构保护)
目标
将不包含目标语言的相邻片段合并,保护代码结构的完整性。
代码逻辑
typescript
for (let i = 0; i < result.length; i++) {
const item = result[i]
if (separatorRegex.test(item)) {
// 包含目标语言,需要翻译
if (tempStr) {
finalResult.push(tempStr) // 先推入积累的代码结构
tempStr = ''
}
finalResult.push(item) // 推入翻译文本
} else {
// 不包含目标语言,是代码结构
tempStr += item
// 如果下一项是翻译内容或已到末尾,结束积累
if (i === result.length - 1 || separatorRegex.test(result[i + 1])) {
finalResult.push(tempStr)
tempStr = ''
}
}
}
实际效果演示
处理过程:
javascript
'欢迎来到' → 包含中文 → 直接推入 → ['欢迎来到']
'<strong>' → 不包含中文 → 积累 → tempStr = '<strong>'
'官方网站' → 包含中文 → 先推入tempStr,再推入当前项 → ['欢迎来到', '<strong>', '官方网站']
'</strong>' → 不包含中文 → 积累 → tempStr = '</strong>'
'!请点击' → 包含中文 → 先推入tempStr,再推入当前项 → ['欢迎来到', '<strong>', '官方网站', '</strong>', '!请点击']
'<button>' → 不包含中文 → 积累 → tempStr = '<button>'
'登录' → 包含中文 → 先推入tempStr,再推入当前项 → [..., '<button>', '登录']
'</button>' → 不包含中文 → 积累 → tempStr = '</button>'
'继续。' → 包含中文 → 先推入tempStr,再推入当前项 → [..., '</button>', '继续。']
最终结构合并结果:
javascript
[
'欢迎来到', // 翻译单元
'<strong>', // 代码结构
'官方网站', // 翻译单元
'</strong>', // 代码结构
'!请点击', // 翻译单元(标点+文字)
'<button>', // 代码结构
'登录', // 翻译单元
'</button>', // 代码结构
'继续。' // 翻译单元(文字+标点)
]
第四阶段:AST模板字符串生成
目标
将切割结果转换为Babel AST模板字符串节点,用于代码生成。
代码逻辑
typescript
strArray.forEach((str, index) => {
if (getOriginRegex().test(str)) {
// 包含目标语言,生成翻译表达式
expressions.push(
baseUtils.createI18nTranslator({
value: str,
isExpression: true,
insertOption: option
})
)
} else {
// 不包含目标语言,作为静态部分
quasis.push(types.templateElement({ raw: str, cooked: str }, false))
}
})
最终生成结果
模板字符串:
javascript
$deepScan`${$t('欢迎来到')}<strong>${$t('官方网站')}</strong>${$t('!请点击')}<button>${$t('登录')}</button>${$t('继续。')}`
关键特点:
- 翻译单元:
${$t('欢迎来到')}
、${$t('!请点击')}
、${$t('继续。')}
- 结构保护:
<strong>
、</strong>
、<button>
、</button>
保持原样 - 语义完整:
'!请点击'
和'继续。'
作为完整的翻译单元
核心价值总结
- 渐进式处理:从最细粒度到语义单元的渐进式合并
- 智能识别:准确区分翻译内容和代码结构
- 语义保护:确保标点符号与相关文字保持在同一翻译单元
- 结构完整:严格保护HTML标签等代码结构不被破坏
- 自动化:全程无需人工干预,自动生成可用的国际化代码
这个方案已成功应用于 auto-i18n-translation-plugins
的 Webpack、Vite、Rsbuild 三个插件中,解决了混合字符串的自动翻译难题。