修复vite翻译插件不支持参数的问题

This commit is contained in:
jingrow 2025-12-29 03:51:14 +08:00
parent 12401e9742
commit 50bb70f80b

View File

@ -129,26 +129,175 @@ function processVueSFC(code, translations) {
}
/**
* 替换插值表达式 {{ $t('xxx') }}
* 替换插值表达式 {{ $t('xxx') }} {{ $t('xxx', { ... }) }}
*/
function replaceInterpolations(code, translations) {
return code.replace(
// 先处理不带参数的调用:{{ $t('xxx') }}
code = code.replace(
/\{\{\s*[\$]t\s*\(\s*(['"])([^'"]+)\1\s*\)\s*\}\}/g,
(match, quote, key) => translations[key] || match
);
// 处理带参数的调用:{{ $t('key', { params }) }}
// 使用更健壮的方法匹配平衡的括号
const matches = [];
const pattern = /\{\{\s*[\$]t\s*\(\s*(['"])([^'"]+)\1\s*,\s*/g;
let match;
// 先收集所有匹配位置(从后往前处理,避免索引错乱)
while ((match = pattern.exec(code)) !== null) {
const startIndex = match.index;
const quote = match[1];
const key = match[2];
const afterKeyIndex = match.index + match[0].length;
// 找到参数部分的结束位置(匹配平衡的括号)
let depth = 1; // 已经有一个开括号
let pos = afterKeyIndex;
let paramsEnd = -1;
while (pos < code.length && depth > 0) {
const char = code[pos];
if (char === '(') depth++;
else if (char === ')') {
depth--;
if (depth === 0) {
paramsEnd = pos;
break;
}
}
pos++;
}
// 找到整个 {{ $t(...) }} 的结束位置
let fullEnd = paramsEnd + 1;
// 跳过空格
while (fullEnd < code.length && code[fullEnd] === ' ') {
fullEnd++;
}
// 跳过 }}
if (fullEnd < code.length && code[fullEnd] === '}') {
fullEnd++;
if (fullEnd < code.length && code[fullEnd] === '}') {
fullEnd++;
}
}
if (paramsEnd !== -1) {
matches.push({
startIndex,
quote,
key,
afterKeyIndex,
paramsEnd,
fullEnd
});
}
}
// 从后往前替换,避免索引错乱
for (let i = matches.length - 1; i >= 0; i--) {
const m = matches[i];
let paramsPart = code.substring(m.afterKeyIndex, m.paramsEnd);
// 先处理参数部分中的 $t() 调用(递归处理嵌套的翻译)
paramsPart = paramsPart.replace(
/\{\{\s*[\$]t\s*\(\s*(['"])([^'"]+)\1\s*\)\s*\}\}/g,
(match, quote, key) => {
const translation = translations[key];
return translation || match;
}
);
const translation = translations[m.key];
if (translation) {
// 替换键为翻译文本,保留参数部分让运行时处理
const replacement = `{{ $t(${m.quote}${escapeString(translation, m.quote)}${m.quote}, ${paramsPart}) }}`;
code = code.substring(0, m.startIndex) + replacement + code.substring(m.fullEnd);
}
}
return code;
}
/**
* 替换 Vue 编译后的格式_ctx.$t('xxx') $t('xxx')
* 替换 Vue 编译后的格式_ctx.$t('xxx') $t('xxx')支持带参数
*/
function replaceCompiledFormat(code, translations) {
return code.replace(
/(_ctx\.)?[\$]t\((['"])([^'"]+)\2\)/g,
// 先处理带参数的调用_ctx.$t('key', params) 或 $t('key', params) 或 t.$t('key', params)
const matches = [];
const pattern = /([a-zA-Z_$][a-zA-Z0-9_$]*\.)?[\$]t\((['"])([^'"]+)\2\s*,\s*/g;
let match;
// 收集所有带参数的调用
while ((match = pattern.exec(code)) !== null) {
const startIndex = match.index;
const ctxPrefix = match[1] || '';
const quote = match[2];
const key = match[3];
const afterKeyIndex = match.index + match[0].length;
// 找到参数部分的结束位置(匹配平衡的括号)
let depth = 1;
let pos = afterKeyIndex;
let paramsEnd = -1;
while (pos < code.length && depth > 0) {
const char = code[pos];
if (char === '(') depth++;
else if (char === ')') {
depth--;
if (depth === 0) {
paramsEnd = pos;
break;
}
}
pos++;
}
if (paramsEnd !== -1) {
matches.push({
startIndex,
ctxPrefix,
quote,
key,
afterKeyIndex,
paramsEnd
});
}
}
// 从后往前替换
for (let i = matches.length - 1; i >= 0; i--) {
const m = matches[i];
let paramsPart = code.substring(m.afterKeyIndex, m.paramsEnd);
// 先处理参数部分中的 $t() 调用(递归处理嵌套的翻译)
paramsPart = paramsPart.replace(
/([a-zA-Z_$][a-zA-Z0-9_$]*\.)?[\$]t\((['"])([^'"]+)\2\)/g,
(match, ctxPrefix, quote, key) => {
const translation = translations[key];
return translation ? quote + escapeString(translation, quote) + quote : match;
return translation ? `${ctxPrefix || ''}$t(${quote}${escapeString(translation, quote)}${quote})` : match;
}
);
const translation = translations[m.key];
if (translation) {
const replacement = `${m.ctxPrefix}$t(${m.quote}${escapeString(translation, m.quote)}${m.quote}, ${paramsPart})`;
code = code.substring(0, m.startIndex) + replacement + code.substring(m.paramsEnd + 1);
}
}
// 处理不带参数的调用_ctx.$t('xxx') 或 $t('xxx') 或 t.$t('xxx')
code = code.replace(
/([a-zA-Z_$][a-zA-Z0-9_$]*\.)?[\$]t\((['"])([^'"]+)\2\)/g,
(match, ctxPrefix, quote, key) => {
const translation = translations[key];
return translation ? `${ctxPrefix || ''}$t(${quote}${escapeString(translation, quote)}${quote})` : match;
}
);
return code;
}
/**
@ -186,6 +335,9 @@ function escapeString(text, quote) {
/**
* 解析 CSV 文件
* 格式key,translation,context
* 注意如果 key translation 包含逗号需要用引号括起来
* 但为了兼容性也支持没有引号的情况通过限制分割次数
*/
function parseCSV(csvPath) {
if (!fs.existsSync(csvPath)) {
@ -200,6 +352,7 @@ function parseCSV(csvPath) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
// 先尝试用引号解析(标准 CSV 格式)
const parts = [];
let current = '';
let inQuotes = false;
@ -207,7 +360,13 @@ function parseCSV(csvPath) {
for (let i = 0; i < trimmed.length; i++) {
const char = trimmed[i];
if (char === '"') {
// 处理转义的引号 ""
if (i + 1 < trimmed.length && trimmed[i + 1] === '"') {
current += '"';
i++; // 跳过下一个引号
} else {
inQuotes = !inQuotes;
}
} else if (char === ',' && !inQuotes) {
parts.push(current);
current = '';
@ -217,6 +376,27 @@ function parseCSV(csvPath) {
}
parts.push(current);
// 如果解析出的部分超过2个可能是键中包含逗号但没有引号
// 尝试从右边开始合并,找到 translation通常包含非 ASCII 字符)
if (parts.length > 2) {
// 从右边开始,找到第一个看起来像翻译的部分(包含非 ASCII 字符)
let translationIndex = -1;
for (let i = parts.length - 1; i >= 0; i--) {
if (parts[i] && /[^\x00-\x7F]/.test(parts[i])) {
translationIndex = i;
break;
}
}
if (translationIndex > 0) {
// 合并 translationIndex 之前的所有部分作为 key
const key = parts.slice(0, translationIndex).join(',').trim();
const translation = parts[translationIndex].trim();
translations[key] = translation;
continue;
}
}
if (parts.length >= 2 && parts[0] && parts[1]) {
const key = parts[0].trim();
const translation = parts[1].trim();