jcloud/dashboard/vite-plugin-translate.mjs

189 lines
4.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
/**
* Vite 插件:构建时直接读取 CSV替换代码中的 t('xxx') 为翻译文本
* 无需生成任何中间文件,直接从 CSV 读取并替换
* @param {object} options - 插件选项
* @param {string} options.locale - 目标语言代码,从 vite.config.ts 传递
*/
function vitePluginTranslate(options = {}) {
let translations = {};
let locale = options.locale || 'en';
return {
name: 'vite-plugin-translate',
enforce: 'pre', // 在 Vue 插件之前执行,处理原始模板
configResolved(config) {
// 直接读取 CSV 翻译文件(与后端共享同一数据源)
if (locale && locale !== 'en') {
const csvPath = path.resolve(__dirname, `../jcloud/translations/${locale}.csv`);
if (fs.existsSync(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) {
// 只处理 .vue, .js, .ts 文件
if (!/\.(vue|js|ts|jsx|tsx)$/.test(id)) {
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;
}
let replaced = code;
const isVueFile = id.endsWith('.vue');
if (isVueFile) {
// 分离 <template> 和 <script> 部分,分别处理
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('翻译')
replaced = replaced.replace(
/(<script[^>]*>)([\s\S]*?)(<\/script>)/gi,
(scriptMatch, openTag, scriptContent, closeTag) => {
// 在脚本中替换 $t('xxx') 为 $t('翻译')
const replacedContent = scriptContent.replace(
/\$t\((['"])([^'"]+)\1\)/g,
(match, quote, key) => {
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 {
// 非 Vue 文件:替换 t('xxx') 为 '翻译'
replaced = replaced.replace(
/\bt\((['"])([^'"]+)\1\)/g,
(match, quote, key) => {
const translation = translations[key];
if (translation) {
const escaped = translation
.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
.replace(/"/g, '\\"');
return quote + escaped + quote;
}
return match;
}
);
}
if (replaced !== code) {
return {
code: replaced,
map: null
};
}
return null;
}
};
}
/**
* 解析 CSV 文件
*/
function parseCSV(csvPath) {
if (!fs.existsSync(csvPath)) {
return {};
}
const content = fs.readFileSync(csvPath, 'utf-8');
const lines = content.split('\n');
const translations = {};
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const parts = [];
let current = '';
let inQuotes = false;
for (let i = 0; i < trimmed.length; i++) {
const char = trimmed[i];
if (char === '"') {
inQuotes = !inQuotes;
} else if (char === ',' && !inQuotes) {
parts.push(current);
current = '';
} else {
current += char;
}
}
parts.push(current);
if (parts.length >= 2 && parts[0] && parts[1]) {
const key = parts[0].trim();
const translation = parts[1].trim();
const context = parts[2] ? parts[2].trim() : '';
if (context) {
translations[`${key}:${context}`] = translation;
} else {
translations[key] = translation;
}
}
}
return translations;
}
/**
* HTML 转义函数
*/
function escapeHtml(text) {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
export default vitePluginTranslate;