构建时实现多语言翻译
This commit is contained in:
parent
738225a28d
commit
62cda250ae
@ -1,50 +1,12 @@
|
||||
import { ref } from 'vue';
|
||||
import { jingrowRequest } from 'jingrow-ui';
|
||||
|
||||
// 从构建时配置获取语言,默认为英文
|
||||
const buildLocale = import.meta.env.DASHBOARD_LOCALE || 'en';
|
||||
|
||||
// 当前语言(从构建配置获取,不可切换)
|
||||
const currentLocale = ref(buildLocale);
|
||||
// 翻译数据:构建时 Vite 插件已直接替换静态文本,这里只处理动态翻译
|
||||
const translations = ref({});
|
||||
const isLoading = ref(false);
|
||||
const initPromise = ref(null);
|
||||
|
||||
/**
|
||||
* 从后端加载翻译
|
||||
* @param {string} locale - 语言代码,默认 'en'
|
||||
*/
|
||||
export async function loadTranslations(locale = 'en') {
|
||||
// 如果正在加载或已加载相同语言,直接返回
|
||||
if (isLoading.value) {
|
||||
return initPromise.value;
|
||||
}
|
||||
|
||||
if (currentLocale.value === locale && Object.keys(translations.value).length > 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
|
||||
const promise = jingrowRequest({
|
||||
url: '/api/action/jingrow.translate.get_app_translations',
|
||||
method: 'GET'
|
||||
})
|
||||
.then((response) => {
|
||||
translations.value = response || {};
|
||||
currentLocale.value = locale;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to load translations:', error);
|
||||
translations.value = {};
|
||||
})
|
||||
.finally(() => {
|
||||
isLoading.value = false;
|
||||
});
|
||||
|
||||
initPromise.value = promise;
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译函数
|
||||
@ -83,10 +45,9 @@ export function getLocale() {
|
||||
*/
|
||||
export async function initI18n() {
|
||||
const locale = buildLocale.split('-')[0] || 'en'; // 处理 'en-US' -> 'en'
|
||||
await loadTranslations(locale);
|
||||
document.documentElement.lang = locale;
|
||||
}
|
||||
|
||||
// 导出响应式状态(供 composable 使用)
|
||||
export { currentLocale, isLoading };
|
||||
export { currentLocale };
|
||||
|
||||
|
||||
188
dashboard/vite-plugin-translate.mjs
Normal file
188
dashboard/vite-plugin-translate.mjs
Normal file
@ -0,0 +1,188 @@
|
||||
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;
|
||||
|
||||
@ -8,15 +8,17 @@ import Components from 'unplugin-vue-components/vite';
|
||||
import Icons from 'unplugin-icons/vite';
|
||||
import IconsResolver from 'unplugin-icons/resolver';
|
||||
import { sentryVitePlugin } from '@sentry/vite-plugin';
|
||||
import vitePluginTranslate from './vite-plugin-translate.mjs';
|
||||
|
||||
// 语言配置:通过环境变量 DASHBOARD_LOCALE 设置,默认为 'en'
|
||||
const locale = process.env.DASHBOARD_LOCALE || 'en';
|
||||
// 语言配置:设置目标语言,默认为 'en'(英文),可设置为 'zh'(中文)等
|
||||
const locale = process.env.DASHBOARD_LOCALE || 'zh';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
vueJsx(),
|
||||
pluginRewriteAll(),
|
||||
vitePluginTranslate({ locale }), // 传递语言配置
|
||||
jingrowui(),
|
||||
Components({
|
||||
dirs: [
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user