修复vite翻译插件正则匹配问题
This commit is contained in:
parent
62cda250ae
commit
12401e9742
@ -7,10 +7,9 @@ const __filename = fileURLToPath(import.meta.url);
|
|||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vite 插件:构建时直接读取 CSV,替换代码中的 t('xxx') 为翻译文本
|
* Vite 插件:构建时直接读取 CSV,替换代码中的 $t('xxx') 为翻译文本
|
||||||
* 无需生成任何中间文件,直接从 CSV 读取并替换
|
|
||||||
* @param {object} options - 插件选项
|
* @param {object} options - 插件选项
|
||||||
* @param {string} options.locale - 目标语言代码,从 vite.config.ts 传递
|
* @param {string} options.locale - 目标语言代码
|
||||||
*/
|
*/
|
||||||
function vitePluginTranslate(options = {}) {
|
function vitePluginTranslate(options = {}) {
|
||||||
let translations = {};
|
let translations = {};
|
||||||
@ -18,111 +17,173 @@ function vitePluginTranslate(options = {}) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
name: 'vite-plugin-translate',
|
name: 'vite-plugin-translate',
|
||||||
enforce: 'pre', // 在 Vue 插件之前执行,处理原始模板
|
enforce: 'pre',
|
||||||
configResolved(config) {
|
configResolved(config) {
|
||||||
// 直接读取 CSV 翻译文件(与后端共享同一数据源)
|
|
||||||
if (locale && locale !== 'en') {
|
if (locale && locale !== 'en') {
|
||||||
const csvPath = path.resolve(__dirname, `../jcloud/translations/${locale}.csv`);
|
const csvPath = path.resolve(__dirname, `../jcloud/translations/${locale}.csv`);
|
||||||
if (fs.existsSync(csvPath)) {
|
if (fs.existsSync(csvPath)) {
|
||||||
translations = parseCSV(csvPath);
|
translations = parseCSV(csvPath);
|
||||||
console.log(`[translate] Loaded ${Object.keys(translations).length} translations from ${csvPath}`);
|
|
||||||
} else {
|
|
||||||
console.warn(`[translate] CSV file not found: ${csvPath}, translations will not be replaced`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
transform(code, id) {
|
transform(code, id) {
|
||||||
// 只处理 .vue, .js, .ts 文件
|
const fileMatches = /\.(vue|js|ts|jsx|tsx)(\?.*)?$/.test(id);
|
||||||
if (!/\.(vue|js|ts|jsx|tsx)$/.test(id)) {
|
if (!fileMatches || locale === 'en' || Object.keys(translations).length === 0) {
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果是英文,不替换
|
|
||||||
if (locale === 'en') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果翻译为空,警告但不阻止(可能 CSV 文件不存在,使用原文)
|
|
||||||
if (Object.keys(translations).length === 0) {
|
|
||||||
console.warn(`[translate] No translations loaded for locale: ${locale}, skipping replacement`);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let replaced = code;
|
let replaced = code;
|
||||||
const isVueFile = id.endsWith('.vue');
|
const isVueFile = id.includes('.vue') && !id.includes('?vue&type=style');
|
||||||
|
|
||||||
if (isVueFile) {
|
if (isVueFile) {
|
||||||
// 分离 <template> 和 <script> 部分,分别处理
|
const isTemplateModule = id.includes('?vue&type=template');
|
||||||
replaced = replaced.replace(
|
|
||||||
/(<template[^>]*>)([\s\S]*?)(<\/template>)/gi,
|
|
||||||
(templateMatch, openTag, templateContent, closeTag) => {
|
|
||||||
// 在模板中替换 {{ $t('xxx') }} 为静态文本
|
|
||||||
const replacedContent = templateContent.replace(
|
|
||||||
/\{\{\s*\$t\((['"])([^'"]+)\1\)\s*\}\}/g,
|
|
||||||
(match, quote, key) => {
|
|
||||||
const translation = translations[key];
|
|
||||||
if (translation) {
|
|
||||||
return translation;
|
|
||||||
}
|
|
||||||
return match;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return openTag + replacedContent + closeTag;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 替换 <script> 中的 this.$t('xxx') 为 this.$t('翻译')
|
if (isTemplateModule) {
|
||||||
replaced = replaced.replace(
|
// Vue 编译后的模板模块
|
||||||
/(<script[^>]*>)([\s\S]*?)(<\/script>)/gi,
|
replaced = replaceInterpolations(replaced, translations);
|
||||||
(scriptMatch, openTag, scriptContent, closeTag) => {
|
replaced = replaceCompiledFormat(replaced, translations);
|
||||||
// 在脚本中替换 $t('xxx') 为 $t('翻译')
|
replaced = replaceAttributeBindings(replaced, translations);
|
||||||
const replacedContent = scriptContent.replace(
|
} else {
|
||||||
/\$t\((['"])([^'"]+)\1\)/g,
|
// 原始 Vue SFC 文件
|
||||||
(match, quote, key) => {
|
replaced = processVueSFC(replaced, translations);
|
||||||
const translation = translations[key];
|
}
|
||||||
if (translation) {
|
|
||||||
const escaped = translation
|
|
||||||
.replace(/\\/g, '\\\\')
|
|
||||||
.replace(/'/g, "\\'")
|
|
||||||
.replace(/"/g, '\\"');
|
|
||||||
return `$t(${quote}${escaped}${quote})`;
|
|
||||||
}
|
|
||||||
return match;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return openTag + replacedContent + closeTag;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
// 非 Vue 文件:替换 t('xxx') 为 '翻译'
|
// 非 Vue 文件
|
||||||
replaced = replaced.replace(
|
replaced = replaced.replace(
|
||||||
/\bt\((['"])([^'"]+)\1\)/g,
|
/\bt\((['"])([^'"]+)\1\)/g,
|
||||||
(match, quote, key) => {
|
(match, quote, key) => {
|
||||||
const translation = translations[key];
|
const translation = translations[key];
|
||||||
if (translation) {
|
return translation ? quote + escapeString(translation, quote) + quote : match;
|
||||||
const escaped = translation
|
|
||||||
.replace(/\\/g, '\\\\')
|
|
||||||
.replace(/'/g, "\\'")
|
|
||||||
.replace(/"/g, '\\"');
|
|
||||||
return quote + escaped + quote;
|
|
||||||
}
|
|
||||||
return match;
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (replaced !== code) {
|
return replaced !== code ? { code: replaced, map: null } : null;
|
||||||
return {
|
|
||||||
code: replaced,
|
|
||||||
map: null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理 Vue SFC 文件
|
||||||
|
*/
|
||||||
|
function processVueSFC(code, translations) {
|
||||||
|
// 处理 <template> 部分
|
||||||
|
const templateRegex = /<template[^>]*>/gi;
|
||||||
|
let templateMatch;
|
||||||
|
|
||||||
|
while ((templateMatch = templateRegex.exec(code)) !== null) {
|
||||||
|
const startIndex = templateMatch.index;
|
||||||
|
const openTag = templateMatch[0];
|
||||||
|
|
||||||
|
// 查找对应的 </template>,处理嵌套
|
||||||
|
let depth = 1;
|
||||||
|
let pos = templateRegex.lastIndex;
|
||||||
|
let templateContent = '';
|
||||||
|
|
||||||
|
while (depth > 0 && pos < code.length) {
|
||||||
|
const nextOpen = code.indexOf('<template', pos);
|
||||||
|
const nextClose = code.indexOf('</template>', pos);
|
||||||
|
|
||||||
|
if (nextClose === -1) break;
|
||||||
|
|
||||||
|
if (nextOpen !== -1 && nextOpen < nextClose) {
|
||||||
|
depth++;
|
||||||
|
pos = nextOpen + 9;
|
||||||
|
} else {
|
||||||
|
depth--;
|
||||||
|
if (depth === 0) {
|
||||||
|
templateContent = code.substring(templateRegex.lastIndex, nextClose);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
pos = nextClose + 11;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (depth === 0 && templateContent) {
|
||||||
|
let replacedContent = templateContent;
|
||||||
|
replacedContent = replaceInterpolations(replacedContent, translations);
|
||||||
|
replacedContent = replaceAttributeBindings(replacedContent, translations);
|
||||||
|
|
||||||
|
code = code.substring(0, startIndex) + openTag + replacedContent + '</template>' +
|
||||||
|
code.substring(startIndex + openTag.length + templateContent.length + 11);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 <script> 部分
|
||||||
|
code = code.replace(
|
||||||
|
/(<script[^>]*>)([\s\S]*?)(<\/script>)/gi,
|
||||||
|
(match, openTag, scriptContent, closeTag) => {
|
||||||
|
const replacedContent = scriptContent.replace(
|
||||||
|
/\$t\((['"])([^'"]+)\1\)/g,
|
||||||
|
(match, quote, key) => {
|
||||||
|
const translation = translations[key];
|
||||||
|
return translation ? `$t(${quote}${escapeString(translation, quote)}${quote})` : match;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return openTag + replacedContent + closeTag;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 替换插值表达式 {{ $t('xxx') }}
|
||||||
|
*/
|
||||||
|
function replaceInterpolations(code, translations) {
|
||||||
|
return code.replace(
|
||||||
|
/\{\{\s*[\$]t\s*\(\s*(['"])([^'"]+)\1\s*\)\s*\}\}/g,
|
||||||
|
(match, quote, key) => translations[key] || match
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 替换 Vue 编译后的格式:_ctx.$t('xxx') 或 $t('xxx')
|
||||||
|
*/
|
||||||
|
function replaceCompiledFormat(code, translations) {
|
||||||
|
return code.replace(
|
||||||
|
/(_ctx\.)?[\$]t\((['"])([^'"]+)\2\)/g,
|
||||||
|
(match, ctxPrefix, quote, key) => {
|
||||||
|
const translation = translations[key];
|
||||||
|
return translation ? quote + escapeString(translation, quote) + quote : match;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 替换属性绑定中的 $t('xxx')
|
||||||
|
*/
|
||||||
|
function replaceAttributeBindings(code, translations) {
|
||||||
|
return code.replace(
|
||||||
|
/([:@]|v-bind:|v-on:)([a-zA-Z0-9_-]+)=(["'])[\$]?t\((['"])([^'"]+)\4\)\3/g,
|
||||||
|
(match, prefix, attrName, outerQuote, innerQuote, key) => {
|
||||||
|
const translation = translations[key];
|
||||||
|
if (!translation || !translation.trim()) {
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
|
||||||
|
let escaped = escapeString(translation, outerQuote);
|
||||||
|
if (outerQuote === '"') {
|
||||||
|
escaped = escaped.replace(/'/g, "\\'");
|
||||||
|
return `${prefix}${attrName}="'${escaped}'"`;
|
||||||
|
} else {
|
||||||
|
escaped = escaped.replace(/"/g, '\\"');
|
||||||
|
return `${prefix}${attrName}='"${escaped}"'`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转义字符串
|
||||||
|
*/
|
||||||
|
function escapeString(text, quote) {
|
||||||
|
return text
|
||||||
|
.replace(/\\/g, '\\\\')
|
||||||
|
.replace(quote === '"' ? /"/g : /'/g, quote === '"' ? '\\"' : "\\'");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析 CSV 文件
|
* 解析 CSV 文件
|
||||||
*/
|
*/
|
||||||
@ -172,17 +233,4 @@ function parseCSV(csvPath) {
|
|||||||
return translations;
|
return translations;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* HTML 转义函数
|
|
||||||
*/
|
|
||||||
function escapeHtml(text) {
|
|
||||||
return text
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/"/g, '"')
|
|
||||||
.replace(/'/g, ''');
|
|
||||||
}
|
|
||||||
|
|
||||||
export default vitePluginTranslate;
|
export default vitePluginTranslate;
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user