monaco-editor 配置中文语言并制作npm包
在vite6.0+vue3的项目中使用monaco-editor开发一个在线编辑器,如何配置中文语言,并且结合Ai开发一个vite插件发布到npm
依赖版本及运行环境
json
{
"monaco-editor": "^0.52.2",
"vite": "^6.3.5",
"vue": "^3.5.17",
"node": "22.14.0",
}
如何配置中文
1、下载中文语言文件包
首先,去VS Code的语言包仓库下载对应的语言包:vscode-loc/i18n/vscode-language-pack-zh-hans,里面的translations/main.i18n.json就是中文的语言包。
2、应用语言包
参考vite-plugin-monaco-editor-nls实现语言转换,它的原理是将汉化文本注入到monaco源码的nls.js文件中。
ts
import fs from 'node:fs';
import path from 'node:path';
import type { Plugin } from 'vite';
import MagicString from 'magic-string';
export enum Languages {
bg = 'bg',
cs = 'cs',
de = 'de',
en_gb = 'en-gb',
es = 'es',
fr = 'fr',
hu = 'hu',
id = 'id',
it = 'it',
ja = 'ja',
ko = 'ko',
nl = 'nl',
pl = 'pl',
ps = 'ps',
pt_br = 'pt-br',
ru = 'ru',
tr = 'tr',
uk = 'uk',
zh_hans = 'zh-hans',
zh_hant = 'zh-hant',
}
export interface Options {
locale: Languages;
localeData?: Record<string, any>;
}
/**
* 在vite中dev模式下会使用esbuild对node_modules进行预编译,导致找不到映射表中的filepath,
* 需要在预编译之前进行替换
* @param options 替换语言包
* @returns
*/
export function esbuildPluginMonacoEditorNls(options: Options) {
options = Object.assign({ locale: Languages.en_gb }, options);
const CURRENT_LOCALE_DATA = getLocalizeMapping(options.locale, options.localeData);
return {
name: 'esbuild-plugin-monaco-editor-nls',
setup(build) {
build.onLoad({ filter: /esm[/\\]vs[/\\]nls\.js/ }, async () => {
return {
contents: getLocalizeCode(CURRENT_LOCALE_DATA),
loader: 'js',
};
});
build.onLoad({ filter: /monaco-editor[/\\]esm[/\\]vs.+\.js/ }, async (args) => {
return {
contents: transformLocalizeFuncCode(args.path, CURRENT_LOCALE_DATA),
loader: 'js',
};
});
},
};
}
/**
* 使用了monaco-editor-nls的语言映射包,把原始localize(data, message)的方法,替换成了localize(path, data, defaultMessage)
* vite build 模式下,使用rollup处理
* @param options 替换语言包
* @returns
*/
export default function (options: Options): Plugin {
options = Object.assign({ locale: Languages.en_gb }, options);
const CURRENT_LOCALE_DATA = getLocalizeMapping(options.locale, options.localeData);
return {
name: 'rollup-plugin-monaco-editor-nls',
enforce: 'pre',
load(filepath) {
if (/esm[/\\]vs[/\\]nls\.js/.test(filepath)) {
return getLocalizeCode(CURRENT_LOCALE_DATA);
}
},
transform(code, filepath) {
if (
/monaco-editor[/\\]esm[/\\]vs.+\.js/.test(filepath) &&
!/esm[/\\]vs[/\\].*nls\.js/.test(filepath)
) {
const re = /monaco-editor[/\\]esm[/\\](.+)(?=\.js)/;
if (re.exec(filepath) && code.includes('localize(')) {
let path = RegExp.$1;
path = path.replace(/\\/g, '/');
code = code.replace(/localize\(/g, `localize('${path}', `);
return {
code: code,
/** 使用magic-string 生成 source map */
map: new MagicString(code).generateMap({
includeContent: true,
hires: true,
source: filepath,
}),
};
}
}
},
};
}
/**
* 替换调用方法接口参数,替换成相应语言包语言
* @param filepath 路径
* @param CURRENT_LOCALE_DATA 替换规则
* @returns
*/
function transformLocalizeFuncCode(filepath: string, _CURRENT_LOCALE_DATA: string) {
let code = fs.readFileSync(filepath, 'utf8');
const re = /monaco-editor[/\\]esm[/\\](.+)(?=\.js)/;
if (re.exec(filepath)) {
let path = RegExp.$1;
path = path.replace(/\\/g, '/');
// if (filepath.includes('contextmenu')) {
// console.log(filepath);
// console.log(JSON.parse(CURRENT_LOCALE_DATA)[path]);
// }
// console.log(path, JSON.parse(CURRENT_LOCALE_DATA)[path])
code = code.replace(/localize\(/g, `localize('${path}', `);
}
return code;
}
/**
* 获取语言包
* @param locale 语言
* @param localeData
* @returns
*/
function getLocalizeMapping(
locale: Languages,
localeData: Record<string, any> | undefined = undefined
) {
if (localeData) return JSON.stringify(localeData);
const locale_data_path = path.join(__dirname, `./locale/${locale}.json`);
return fs.readFileSync(locale_data_path) as unknown as string;
}
/**
* 替换代码
* @param CURRENT_LOCALE_DATA 语言包
* @returns
*/
function getLocalizeCode(CURRENT_LOCALE_DATA: string) {
return `
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// eslint-disable-next-line local/code-import-patterns
import { getNLSLanguage, getNLSMessages } from './nls.messages.js';
// eslint-disable-next-line local/code-import-patterns
export { getNLSLanguage, getNLSMessages } from './nls.messages.js';
const isPseudo = getNLSLanguage() === 'pseudo' || (typeof document !== 'undefined' && document.location && document.location.hash.indexOf('pseudo=true') >= 0);
function _format(message, args) {
let result;
if (args.length === 0) {
result = message;
}
else {
result = message.replace(/\\{(\\d+)\\}/g, (match, rest) => {
const index = rest[0];
const arg = args[index];
let result = match;
if (typeof arg === 'string') {
result = arg;
}
else if (typeof arg === 'number' || typeof arg === 'boolean' || arg === void 0 || arg === null) {
result = String(arg);
}
return result;
});
}
if (isPseudo) {
// FF3B and FF3D is the Unicode zenkaku representation for [ and ]
result = '\uFF3B' + result.replace(/[aouei]/g, '$&$&') + '\uFF3D';
}
return result;
}
/**
* @skipMangle
*/
// export function localize(data /* | number when built */, message /* | null when built */, ...args) {
// if (typeof data === 'number') {
// return _format(lookupMessage(data, message), args);
// }
// return _format(message, args);
// }
// ------------------------invoke----------------------------------------
export function localize(path, data, defaultMessage, ...args) {
if (typeof data === 'number') {
return _format(lookupMessage(data, message), args);
}
var key = typeof data === 'object' ? data.key : data;
var data = ${CURRENT_LOCALE_DATA} || {};
var message = (data[path] || data?.contents?.[path] || {})[key];
if (!message) {
message = defaultMessage;
}
return _format(message, args);
}
// ------------------------invoke----------------------------------------
/**
* Only used when built: Looks up the message in the global NLS table.
* This table is being made available as a global through bootstrapping
* depending on the target context.
*/
function lookupMessage(index, fallback) {
const message = getNLSMessages()?.[index];
if (typeof message !== 'string') {
if (typeof fallback === 'string') {
return fallback;
}
throw new Error(\`!!! NLS MISSING: \${index} !!!\`);
}
return message;
}
/**
* @skipMangle
*/
export function localize2(data /* | number when built */, originalMessage, ...args) {
let message;
if (typeof data === 'number') {
message = lookupMessage(data, originalMessage);
}
else {
message = originalMessage;
}
const value = _format(message, args);
return {
value,
original: originalMessage === message ? value : _format(originalMessage, args)
};
}
`;
}
3、配置vite.config.ts
ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import UnoCSS from 'unocss/vite';
import nlsPlugin, { Languages, esbuildPluginMonacoEditorNls } from './nls/index.ts';
import zh_hans from './nls/zh-hans.json';
// 注意只在生产环境下添加rollup插件,开发模式下会报错
const dev_plugins = [];
if (process.env.NODE_ENV !== 'development') {
dev_plugins.push(
nlsPlugin({
locale: Languages.zh_hans,
localeData: zh_hans,
})
);
}
// https://vite.dev/config/
export default defineConfig({
optimizeDeps: {
esbuildOptions: {
plugins: [
// 开发环境下通过esbuild插件进行汉化
esbuildPluginMonacoEditorNls({
locale: Languages.zh_hans,
localeData: zh_hans,
}),
],
},
},
plugins: [vue(), UnoCSS(), ...dev_plugins],
resolve: {
alias: {
'@': '/src',
},
},
});
如何开发vite插件并发布npm包
到现在已经实现monaco-editor汉化功能,但是如何将其作为vite插件发布呢? 在不具备开发插件的技能的情况下,借助通义灵码实现这个工程。
1、明确插件功能
当前的插件是用于 Monaco Editor 的国际化支持,通过 Vite 插件机制在开发和构建阶段替换 Monaco Editor 的 nls.js 文件并注入本地化语言包。
主要功能包括:
在 vite dev 模式下使用 esbuild 替换语言资源 在 vite build 模式下使用 rollup 注入语言逻辑 支持多种语言(如 zh-hans, en-gb 等)
2、项目结构
bash
vite-plugin-monaco-editor-i18n/
├── src/
│ ├── index.ts ← 主逻辑
│ └── locales/ ← 所有语言包
│ └── zh-hans.json
├── dist/ ← 构建输出目录
│ ├── index.js
│ └── index.d.ts ← 类型定义文件
│ └── locales/ ← 所有语言包
│ └── zh-hans.json
├── package.json
├── tsconfig.json
└── README.md
3、配置 TypeScript 编译
json
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"lib": ["DOM", "ESNext"],
"declaration": true,
"declarationDir": "./dist",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"moduleResolution": "node",
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}
4、添加package.json
json
{
"name": "vite-plugin-monaco-editor-i18n",
"version": "1.0.3",
"description": "为 Monaco Editor 提供多语言支持的 Vite 插件",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist/**/*"
],
"scripts": {
"build": "tsc --build && copyfiles -u 1 src/locales/**/* dist"
},
"keywords": [
"monaco",
"monaco-editor",
"i18n",
"vite-plugin",
"language",
"localization"
],
"author": "important489",
"license": "MIT",
"peerDependencies": {
"monaco-editor": "*",
"vite": "^6.3.5"
},
"devDependencies": {
"@types/node": "^24.0.10",
"copyfiles": "^2.4.1",
"magic-string": "^0.30.17",
"typescript": "^5.0.0"
}
}
5、添加入口文件 index.ts
写入上文的内容,需要根据需要解决部分报错。
6、添加语言包
下载语言包,并保存在 src/locales 目录下。
7、发布npm
- 创建npm账号
- 登录npm:npm login
- 发布npm:npm publish --access public
注意:登录npm时,需要切换回官方 npm registry,
npm config set registry https://registry.npmjs.org/
8、确认结果并使用插件
bash
pnpm add vite-plugin-monaco-editor-i18n
根据*.md文档内容配置vite.config.ts,并使用插件。
思考总结
"files": ["dist/**/*"]
通过这个配置可以只发布dist目录下的文件,- 如何实现动态语言支持?