mirror of
https://gitee.com/mirrors/AllinSSL.git
synced 2026-03-13 10:00:53 +08:00
【初始化】前端工程项目
This commit is contained in:
607
frontend/packages/vite/plugin/i18n.cjs
Normal file
607
frontend/packages/vite/plugin/i18n.cjs
Normal file
@@ -0,0 +1,607 @@
|
||||
const { Plugin } = require("vite");
|
||||
const { glob } = require("glob");
|
||||
const fs = require("node:fs/promises");
|
||||
const path = require("node:path");
|
||||
|
||||
/**
|
||||
* 支持的语言类型列表
|
||||
* 每种语言包含:
|
||||
* - lang: 语言代码
|
||||
* - content: 语言名称
|
||||
*/
|
||||
const SUPPORTED_LANGUAGES = [
|
||||
{ lang: "zh", content: "中文" },
|
||||
{ lang: "zh-CN", content: "中文-繁体" },
|
||||
{ lang: "en", content: "English" },
|
||||
{ lang: "fr", content: "Français" },
|
||||
{ lang: "de", content: "Deutsch" },
|
||||
{ lang: "es", content: "Español" },
|
||||
{ lang: "it", content: "Italiano" },
|
||||
{ lang: "ja", content: "日本語" },
|
||||
];
|
||||
|
||||
/**
|
||||
* 默认插件配置
|
||||
*/
|
||||
const defaultConfig = {
|
||||
scanDirs: ["src"],
|
||||
fileTypes: [".vue", ".tsx", ".jsx", ".ts", ".js"],
|
||||
targetLanguages: ["en", "zh"],
|
||||
outputDir: "src/locales",
|
||||
enableInDev: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* 扫描器类 - 负责扫描文件并提取需要翻译的文本
|
||||
*/
|
||||
class Scanner {
|
||||
constructor(config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描所有匹配的文件
|
||||
* @returns {Promise<Array>} 扫描结果数组
|
||||
*/
|
||||
async scanFiles() {
|
||||
const results = [];
|
||||
|
||||
// 遍历所有需要扫描的目录
|
||||
for (const dir of this.config.scanDirs) {
|
||||
// 构建 glob 模式,统一使用正斜杠
|
||||
const pattern = path.join(dir, "**/*").replace(/\\/g, "/");
|
||||
|
||||
// 使用 glob 获取所有匹配的文件
|
||||
const files = await glob(pattern, {
|
||||
ignore: ["**/node_modules/**"],
|
||||
nodir: true,
|
||||
absolute: true,
|
||||
});
|
||||
|
||||
// 过滤出指定类型的文件
|
||||
const matchedFiles = files.filter((file) => {
|
||||
const ext = path.extname(file);
|
||||
return this.config.fileTypes.includes(ext);
|
||||
});
|
||||
|
||||
// 扫描每个匹配的文件
|
||||
for (const file of matchedFiles) {
|
||||
const result = await this.scanFile(file);
|
||||
if (result.matches.length > 0) {
|
||||
results.push(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描单个文件
|
||||
* @param {string} filePath - 文件路径
|
||||
* @returns {Promise<Object>} 文件的扫描结果
|
||||
*/
|
||||
async scanFile(filePath) {
|
||||
// 读取文件内容
|
||||
const content = await fs.readFile(filePath, "utf-8");
|
||||
const lines = content.split("\n");
|
||||
const matches = [];
|
||||
|
||||
// 匹配 t("文本") 或 t('文本') 格式的国际化标记
|
||||
const pattern = /t\(['"](.+?)['"]\)/g;
|
||||
|
||||
// 遍历每一行查找匹配
|
||||
lines.forEach((line, index) => {
|
||||
let match;
|
||||
while ((match = pattern.exec(line)) !== null) {
|
||||
matches.push({
|
||||
content: match[1],
|
||||
line: index + 1,
|
||||
column: match.index,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
filePath,
|
||||
matches,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译器类
|
||||
*/
|
||||
class Translator {
|
||||
constructor(config) {
|
||||
this.config = config;
|
||||
this.cache = new Map();
|
||||
this.batchSize = 10; // 批量翻译大小
|
||||
}
|
||||
|
||||
async translate(text, targetLang) {
|
||||
const langCache = this.cache.get(text);
|
||||
if (langCache?.has(targetLang)) {
|
||||
return langCache.get(targetLang);
|
||||
}
|
||||
|
||||
let translatedText;
|
||||
|
||||
if (this.config.customTranslator) {
|
||||
translatedText = await this.config.customTranslator(text, targetLang);
|
||||
} else {
|
||||
const results = await this.translateWithGLM([text], targetLang);
|
||||
translatedText = results[0];
|
||||
}
|
||||
|
||||
if (!this.cache.has(text)) {
|
||||
this.cache.set(text, new Map());
|
||||
}
|
||||
this.cache.get(text).set(targetLang, translatedText);
|
||||
|
||||
return translatedText;
|
||||
}
|
||||
|
||||
async translateWithGLM(texts, targetLang) {
|
||||
// 检查 API 密钥是否配置
|
||||
if (!this.config.glmConfig?.apiKey) {
|
||||
throw new Error("GLM API key is required for translation");
|
||||
}
|
||||
|
||||
// 获取 API 端点,如果未配置则使用默认端点
|
||||
const endpoint =
|
||||
this.config.glmConfig.apiEndpoint ||
|
||||
"https://open.bigmodel.cn/api/paas/v4/chat/completions";
|
||||
|
||||
try {
|
||||
// 生成翻译提示词
|
||||
const prompt = this.generateTranslationPrompt(texts, targetLang);
|
||||
|
||||
// 调用智谱 AI 的 API
|
||||
const response = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${this.config.glmConfig.apiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: "glm-4-flash",
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content:
|
||||
"你是一个专业的翻译助手,请将用户提供的文本准确翻译成目标语言。保持专业性和准确性,同时确保翻译后的文本通顺易懂。",
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: prompt,
|
||||
},
|
||||
],
|
||||
temperature: 0.3,
|
||||
top_p: 0.7,
|
||||
response_format: { type: "json" },
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Translation failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
const translatedContent = JSON.parse(result.choices[0].message.content);
|
||||
return translatedContent.translations;
|
||||
} catch (error) {
|
||||
console.error("Translation error:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
generateTranslationPrompt(texts, targetLang) {
|
||||
return `请将以下文本翻译成${targetLang}语言。请以JSON格式返回,格式为:{"translations": ["翻译1", "翻译2", ...]}
|
||||
|
||||
源文本:
|
||||
${texts.map((text, index) => `${index + 1}. ${text}`).join("\n")}
|
||||
|
||||
要求:
|
||||
1. 保持专业性和准确性
|
||||
2. 确保翻译后的文本通顺易懂
|
||||
3. 严格按照JSON格式返回
|
||||
4. 保持原文的语气和风格
|
||||
5. 专业术语使用对应语言的标准翻译`;
|
||||
}
|
||||
|
||||
async translateBatch(texts, targetLang) {
|
||||
const results = [];
|
||||
const uncachedTexts = [];
|
||||
|
||||
texts.forEach((text, index) => {
|
||||
const langCache = this.cache.get(text);
|
||||
if (langCache?.has(targetLang)) {
|
||||
results[index] = {
|
||||
text,
|
||||
targetLang,
|
||||
translatedText: langCache.get(targetLang),
|
||||
};
|
||||
} else {
|
||||
uncachedTexts.push({ text, index });
|
||||
}
|
||||
});
|
||||
|
||||
if (uncachedTexts.length > 0) {
|
||||
for (let i = 0; i < uncachedTexts.length; i += this.batchSize) {
|
||||
const batch = uncachedTexts.slice(i, i + this.batchSize);
|
||||
const batchTexts = batch.map((item) => item.text);
|
||||
|
||||
let translatedTexts;
|
||||
if (this.config.customTranslator) {
|
||||
translatedTexts = await Promise.all(
|
||||
batchTexts.map((text) =>
|
||||
this.config.customTranslator(text, targetLang),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
translatedTexts = await this.translateWithGLM(batchTexts, targetLang);
|
||||
}
|
||||
|
||||
batch.forEach((item, batchIndex) => {
|
||||
const translatedText = translatedTexts[batchIndex];
|
||||
|
||||
if (!this.cache.has(item.text)) {
|
||||
this.cache.set(item.text, new Map());
|
||||
}
|
||||
this.cache.get(item.text).set(targetLang, translatedText);
|
||||
|
||||
results[item.index] = {
|
||||
text: item.text,
|
||||
targetLang,
|
||||
translatedText,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译队列类 - 管理翻译任务的队列
|
||||
*/
|
||||
class TranslationQueue {
|
||||
constructor() {
|
||||
this.queue = new Map();
|
||||
this.processed = new Set();
|
||||
}
|
||||
|
||||
add(item) {
|
||||
const key = this.generateKey(item);
|
||||
if (!this.queue.has(key)) {
|
||||
this.queue.set(key, item);
|
||||
}
|
||||
}
|
||||
|
||||
getUnprocessed() {
|
||||
return Array.from(this.queue.values()).filter(
|
||||
(item) => !this.processed.has(this.generateKey(item)),
|
||||
);
|
||||
}
|
||||
|
||||
markAsProcessed(item) {
|
||||
this.processed.add(this.generateKey(item));
|
||||
}
|
||||
|
||||
exists(item) {
|
||||
return this.queue.has(this.generateKey(item));
|
||||
}
|
||||
|
||||
getAll() {
|
||||
return Array.from(this.queue.values());
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.queue.clear();
|
||||
this.processed.clear();
|
||||
}
|
||||
|
||||
generateKey(item) {
|
||||
return `${item.path}:${item.content}`;
|
||||
}
|
||||
|
||||
generateI18nId(item) {
|
||||
const dir = path.dirname(item.path);
|
||||
const components = dir.split(path.sep).filter(Boolean);
|
||||
const lastComponent = components[components.length - 1] || "root";
|
||||
const queueArray = Array.from(this.queue.values());
|
||||
const index = queueArray.findIndex((i) => i.path === item.path);
|
||||
return `${lastComponent}.${index >= 0 ? index : 0}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件管理器类 - 负责生成和管理国际化文件
|
||||
*/
|
||||
class FileManager {
|
||||
constructor(config) {
|
||||
this.config = config;
|
||||
this.contentCache = {};
|
||||
}
|
||||
|
||||
async generateI18nFiles(translations) {
|
||||
const i18nContent = {};
|
||||
|
||||
for (const item of translations) {
|
||||
i18nContent[item.i18n] = item.lang;
|
||||
}
|
||||
|
||||
await this.loadExistingContent();
|
||||
Object.assign(this.contentCache, i18nContent);
|
||||
|
||||
for (const lang of this.config.targetLanguages) {
|
||||
const langContent = {};
|
||||
|
||||
for (const [key, translations] of Object.entries(this.contentCache)) {
|
||||
if (translations[lang]) {
|
||||
langContent[key] = translations[lang];
|
||||
}
|
||||
}
|
||||
|
||||
await this.writeLanguageFile(lang, langContent);
|
||||
}
|
||||
|
||||
await this.generateTypeDefinition();
|
||||
}
|
||||
|
||||
async loadExistingContent() {
|
||||
for (const lang of this.config.targetLanguages) {
|
||||
const filePath = path.join(this.config.outputDir, `${lang}.json`);
|
||||
try {
|
||||
const content = await fs.readFile(filePath, "utf-8");
|
||||
const langContent = JSON.parse(content);
|
||||
|
||||
for (const [key, value] of Object.entries(langContent)) {
|
||||
if (!this.contentCache[key]) {
|
||||
this.contentCache[key] = {};
|
||||
}
|
||||
this.contentCache[key][lang] = value;
|
||||
}
|
||||
} catch (error) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async writeLanguageFile(lang, content) {
|
||||
const outputDir = this.config.outputDir;
|
||||
await fs.mkdir(outputDir, { recursive: true });
|
||||
|
||||
const filePath = path.join(outputDir, `${lang}.json`);
|
||||
await fs.writeFile(filePath, JSON.stringify(content, null, 2));
|
||||
}
|
||||
|
||||
async generateTypeDefinition() {
|
||||
const keys = Object.keys(this.contentCache);
|
||||
|
||||
const typeContent = `// This file is auto-generated. DO NOT EDIT.
|
||||
export type I18nKey = ${keys.map((key) => `'${key}'`).join(" | ")}
|
||||
|
||||
export interface I18nMessages {
|
||||
[key in I18nKey]: string
|
||||
}
|
||||
`;
|
||||
const typePath = path.join(this.config.outputDir, "i18n-types.ts");
|
||||
await fs.writeFile(typePath, typeContent);
|
||||
}
|
||||
|
||||
async fileExists(filePath) {
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateConfig(options) {
|
||||
const config = { ...defaultConfig, ...options };
|
||||
|
||||
if (!Array.isArray(config.scanDirs) || config.scanDirs.length === 0) {
|
||||
throw new Error("scanDirs must be a non-empty array");
|
||||
}
|
||||
|
||||
if (!Array.isArray(config.fileTypes) || config.fileTypes.length === 0) {
|
||||
throw new Error("fileTypes must be a non-empty array");
|
||||
}
|
||||
|
||||
if (
|
||||
!Array.isArray(config.targetLanguages) ||
|
||||
config.targetLanguages.length === 0
|
||||
) {
|
||||
throw new Error("targetLanguages must be a non-empty array");
|
||||
}
|
||||
|
||||
if (!config.outputDir) {
|
||||
throw new Error("outputDir is required");
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接处理国际化翻译
|
||||
* @param {Object} options - 国际化插件配置
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function processI18n(options = {}) {
|
||||
const config = validateConfig(options);
|
||||
const scanner = new Scanner(config);
|
||||
const translator = new Translator(config);
|
||||
const queue = new TranslationQueue();
|
||||
const fileManager = new FileManager(config);
|
||||
|
||||
console.log("Starting i18n scanning...");
|
||||
|
||||
try {
|
||||
const scanResults = await scanner.scanFiles();
|
||||
|
||||
for (const result of scanResults) {
|
||||
for (const match of result.matches) {
|
||||
queue.add({
|
||||
path: result.filePath,
|
||||
i18n: queue.generateI18nId({
|
||||
path: result.filePath,
|
||||
content: match.content,
|
||||
i18n: "",
|
||||
lang: {},
|
||||
}),
|
||||
content: match.content,
|
||||
lang: {},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const unprocessed = queue.getUnprocessed();
|
||||
const total = unprocessed.length * config.targetLanguages.length;
|
||||
let processed = 0;
|
||||
|
||||
for (const item of unprocessed) {
|
||||
for (const targetLang of config.targetLanguages) {
|
||||
const translatedText = await translator.translate(
|
||||
item.content,
|
||||
targetLang,
|
||||
);
|
||||
item.lang[targetLang] = translatedText;
|
||||
processed++;
|
||||
console.log(
|
||||
`Processing translations: ${processed}/${total} (${Math.round((processed / total) * 100)}%)`,
|
||||
);
|
||||
}
|
||||
queue.markAsProcessed(item);
|
||||
}
|
||||
|
||||
await fileManager.generateI18nFiles(queue.getAll());
|
||||
console.log("I18n processing completed successfully");
|
||||
} catch (error) {
|
||||
console.error("Error in i18n processing:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vite 插件主函数
|
||||
* @param {Object} options - 插件配置选项
|
||||
* @returns {Object} Vite 插件对象
|
||||
*/
|
||||
function viteI18nPlugin(options = {}) {
|
||||
const config = validateConfig(options);
|
||||
const scanner = new Scanner(config);
|
||||
const translator = new Translator(config);
|
||||
const queue = new TranslationQueue();
|
||||
const fileManager = new FileManager(config);
|
||||
|
||||
return {
|
||||
name: "vite-plugin-i18n-auto",
|
||||
|
||||
async configResolved() {
|
||||
console.log("Starting i18n scanning...");
|
||||
|
||||
try {
|
||||
const scanResults = await scanner.scanFiles();
|
||||
|
||||
for (const result of scanResults) {
|
||||
for (const match of result.matches) {
|
||||
queue.add({
|
||||
path: result.filePath,
|
||||
i18n: queue.generateI18nId({
|
||||
path: result.filePath,
|
||||
content: match.content,
|
||||
i18n: "",
|
||||
lang: {},
|
||||
}),
|
||||
content: match.content,
|
||||
lang: {},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const unprocessed = queue.getUnprocessed();
|
||||
for (const item of unprocessed) {
|
||||
for (const targetLang of config.targetLanguages) {
|
||||
const translatedText = await translator.translate(
|
||||
item.content,
|
||||
targetLang,
|
||||
);
|
||||
item.lang[targetLang] = translatedText;
|
||||
}
|
||||
queue.markAsProcessed(item);
|
||||
}
|
||||
|
||||
await fileManager.generateI18nFiles(queue.getAll());
|
||||
console.log("I18n processing completed successfully");
|
||||
} catch (error) {
|
||||
console.error("Error in i18n plugin:", error);
|
||||
}
|
||||
},
|
||||
|
||||
configureServer(server) {
|
||||
if (!config.enableInDev) return;
|
||||
|
||||
console.log("I18n plugin server configured");
|
||||
|
||||
server.watcher.on("change", async (filePath) => {
|
||||
const ext = filePath.split(".").pop();
|
||||
if (!ext || !config.fileTypes.includes(`.${ext}`)) return;
|
||||
|
||||
try {
|
||||
const scanResult = await scanner.scanFile(filePath);
|
||||
let hasChanges = false;
|
||||
|
||||
for (const match of scanResult.matches) {
|
||||
const item = {
|
||||
path: filePath,
|
||||
i18n: queue.generateI18nId({
|
||||
path: filePath,
|
||||
content: match.content,
|
||||
i18n: "",
|
||||
lang: {},
|
||||
}),
|
||||
content: match.content,
|
||||
lang: {},
|
||||
};
|
||||
|
||||
if (!queue.exists(item)) {
|
||||
hasChanges = true;
|
||||
queue.add(item);
|
||||
|
||||
for (const targetLang of config.targetLanguages) {
|
||||
const translatedText = await translator.translate(
|
||||
item.content,
|
||||
targetLang,
|
||||
);
|
||||
item.lang[targetLang] = translatedText;
|
||||
}
|
||||
queue.markAsProcessed(item);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasChanges) {
|
||||
await fileManager.generateI18nFiles(queue.getAll());
|
||||
console.log(`Updated i18n files for changes in ${filePath}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error processing file ${filePath}:`, error);
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
SUPPORTED_LANGUAGES,
|
||||
Scanner,
|
||||
Translator,
|
||||
TranslationQueue,
|
||||
FileManager,
|
||||
processI18n,
|
||||
viteI18nPlugin,
|
||||
};
|
||||
27
frontend/packages/vite/plugin/index.ts
Normal file
27
frontend/packages/vite/plugin/index.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
// 如果你想自定义翻译处理
|
||||
import { Scanner, Translator, TranslationQueue, FileManager } from "./i18n";
|
||||
|
||||
async function customTranslation() {
|
||||
const config = {
|
||||
scanDirs: ["src"], // 需要扫描的目录
|
||||
fileTypes: [".vue", ".tsx", ".jsx", ".ts", ".js"], // 支持的文件类型
|
||||
targetLanguages: ["en", "zh"], // 目标语言
|
||||
outputDir: "src/locales", // 输出目录
|
||||
glmConfig: {
|
||||
apiKey: "a160afdbea1644e68de5e5b014bea0f7.zZuSidvDSYOD7oJT", // 你的智谱 AI API 密钥
|
||||
apiEndpoint: "https://open.bigmodel.cn/api/paas/v4/chat/completions", // 可选,API 端点
|
||||
},
|
||||
};
|
||||
|
||||
const scanner = new Scanner(config);
|
||||
const translator = new Translator(config);
|
||||
const queue = new TranslationQueue();
|
||||
const fileManager = new FileManager(config);
|
||||
|
||||
// 自定义扫描和翻译逻辑
|
||||
const results = await scanner.scanFiles();
|
||||
// ... 处理翻译
|
||||
await fileManager.generateI18nFiles(queue.getAll());
|
||||
}
|
||||
|
||||
customTranslation();
|
||||
41
frontend/packages/vite/plugin/readme.md
Normal file
41
frontend/packages/vite/plugin/readme.md
Normal file
@@ -0,0 +1,41 @@
|
||||
国际化匹配,基于路由和文件拆分,生成国际化文件
|
||||
1、vite插件编写,仅项目运行前扫描一次,并处理国际化文件
|
||||
2、定义配置参数
|
||||
-- 需要扫描的目录
|
||||
-- 需要扫描的文件类型
|
||||
-- 需要扫描的文件名称
|
||||
3、基于正则进行匹配,提取正则表达式中的翻译内容,进入翻译队列和路径匹配数据。
|
||||
4、进入翻译队列后,要进行去重处理,如果内容相同,则不进行翻译,直接调用队列表。
|
||||
4、基于GLM-4-Plus模型进行翻译
|
||||
5、将重复的翻译内容进行合并,生成队列表
|
||||
6、队列表拆分成国际化文件
|
||||
|
||||
项目模块划分:
|
||||
1、翻译模块-用于翻译内容
|
||||
2、扫描模块-扫描文件信息,进行匹配,支持国际化常用的几种方式
|
||||
3、队列模块-用于存储翻译内容
|
||||
4、文件模块-用于生成国际化文件和相关的配置文件
|
||||
5、插件模块-用于处理vite插件项目周期
|
||||
6、文件对比模块-用于对比文件内容
|
||||
|
||||
队列表格式如下:
|
||||
[{
|
||||
path: '/src/views/user/index.vue',
|
||||
i18n: 'user.components.0', // 生成国际化id,"路由_当前下标"
|
||||
content: 't("用户",{ name: "zhang san" })'
|
||||
lang:{
|
||||
zh: '用户管理',
|
||||
en: 'User Management'
|
||||
}
|
||||
}]
|
||||
|
||||
语言翻译列表如下:
|
||||
[
|
||||
{lang: 'zh', content: '中文'}, // 中文
|
||||
{lang: 'en', content: 'English'}, // 英文
|
||||
{lang: 'fr', content: 'Français'}, // 法语
|
||||
{lang: 'de', content: 'Deutsch'}, // 德语
|
||||
{lang: 'es', content: 'Español'}, // 西班牙语
|
||||
{lang: 'it', content: 'Italiano'}, // 意大利语
|
||||
{lang: 'ja', content: '日本語'}, // 日语
|
||||
]
|
||||
17
frontend/packages/vite/plugin/tsconfig.json
Normal file
17
frontend/packages/vite/plugin/tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020"],
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": ["./**/*.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user