dev #3
@ -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 ? `${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 ? quote + escapeString(translation, quote) + quote : match;
|
||||
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 === '"') {
|
||||
inQuotes = !inQuotes;
|
||||
// 处理转义的引号 ""
|
||||
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();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user