发布一个monaco-editor 汉化包

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

  1. 创建npm账号
  2. 登录npm:npm login
  3. 发布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,并使用插件。

思考总结

  1. "files": ["dist/**/*"]通过这个配置可以只发布dist目录下的文件,
  2. 如何实现动态语言支持?

参考资料: 用Tauri开发一个EPUB编辑器(二) Monaco Editor的汉化、代码高亮、设置主题、代码补全

相关推荐
zwjapple1 小时前
docker-compose一键部署全栈项目。springboot后端,react前端
前端·spring boot·docker
像风一样自由20203 小时前
HTML与JavaScript:构建动态交互式Web页面的基石
前端·javascript·html
aiprtem4 小时前
基于Flutter的web登录设计
前端·flutter
浪裡遊4 小时前
React Hooks全面解析:从基础到高级的实用指南
开发语言·前端·javascript·react.js·node.js·ecmascript·php
why技术4 小时前
Stack Overflow,轰然倒下!
前端·人工智能·后端
GISer_Jing4 小时前
0704-0706上海,又聚上了
前端·新浪微博
止观止5 小时前
深入探索 pnpm:高效磁盘利用与灵活的包管理解决方案
前端·pnpm·前端工程化·包管理器
whale fall5 小时前
npm install安装的node_modules是什么
前端·npm·node.js
烛阴5 小时前
简单入门Python装饰器
前端·python
袁煦丞5 小时前
数据库设计神器DrawDB:cpolar内网穿透实验室第595个成功挑战
前端·程序员·远程工作