189 lines
4.9 KiB
JavaScript
189 lines
4.9 KiB
JavaScript
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, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
|
||
export default vitePluginTranslate;
|
||
|