vite-plugin-monaco-editor-nls 最新版踩坑笔记和修复方法

2,983 阅读4分钟

背景

最近心血来潮想要给Monaco Editor进行汉化,但是由于我使用的构建工具是Vite,众所周知,Webpack对于Moncao Editor来说是一等公民,因此Vite往往可能遇到问题。 由于部分热心群众的不断PR,Monaco Editor本身在进行Vite构建时已经没什么使用上的困难,主要的困难还是汉化。

说明

本文以最新稳定Monaco Editor版本(0.48.0)、Vite 5、Vue 3为例讲讲目前可以怎么做Monaco Editor的汉化。

遇到的问题

1.默认安装的版本不对

如果使用 yarn add -D vite-plugin-monaco-editor-nls 进行安装时,默认安装的是2.0.2版本,该版本是有问题的。

缺东西,一定要安装2.0.1版本。

2.报错 MonacoEditorNlsPlugin is not a function

目前建议这样修复:

export default defineConfig({
    plugins: [MonacoEditorNlsPlugin.default({
        locale: Languages.zh_hans,
    }), vue(), AutoImport({
        imports: ['vue', 'vue-router']
    })], optimizeDeps: {
        /** vite >= 2.3.0 */
        esbuildOptions: {
            plugins: [
                esbuildPluginMonacoEditorNls({
                    locale: Languages.zh_hans,
                }),
            ],
        },
    },
})

增加.default,除此之外好像只能改代码了。

3.配置成功后,发现无效果,且控制台报错

Uncaught TypeError: nls.getConfiguredDefaultLocale is not a function

原因:代码没有对齐,需要参考下node_modules/monaco-editor/esm/vs/nls.js和vite-plugin-monaco-editor-nls插件内的方法getLocalizeCode,确保两个地方能对齐。

主要修改的点,通过Diff工具配置就可以看到了。

4.修复问题3后,发现右键菜单打不开,且伴随如下报错:

Cannot read properties of undefined (reading 'localeCompare')

原因:

新版本增加了一个新的本地化函数localize2,返回的是一个Object,这个函数需要在vite-plugin-monaco-editor-nls内的transformLocalizeFuncCode函数进行注册,写法如下:

function transformLocalizeFuncCode(filepath, CURRENT_LOCALE_DATA) {
    let code = fs_1.default.readFileSync(filepath, 'utf8');
    const re = /(?:monaco-editor[\/]esm[\/])(.+)(?=.js)/;
    if (re.exec(filepath)) {
        let path = RegExp.$1;
        path = path.replaceAll('\', '/');
        // 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}', `);
        code = code.replace(/localize2(/g, `localize2('${path}', `);
    }
    return code;
}

在nls.js中写法如下:

export function localize2(path, data, defaultMessage, ...args) {
            var key = typeof data === 'object' ? data.key : data;
            var data = ${CURRENT_LOCALE_DATA} || {};
            var message = (data[path] || {})[key];
            if (!message) {
                message = defaultMessage;
            }
            const original = _format(message, args);
               return {
                value: original,
        original
    };
}

最终执行的步骤是:

插件index.js修改transformLocalizeFuncCode函数、修改getLocalizeCode方法,替换返回的JS脚本为:

    function getLocalizeCode(CURRENT_LOCALE_DATA) {
        return `
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

let isPseudo = (typeof document !== 'undefined' && document.location && document.location.hash.indexOf('pseudo=true') >= 0);
const DEFAULT_TAG = 'i-default';
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;
}
function findLanguageForModule(config, name) {
    let result = config[name];
    if (result) {
        return result;
    }
    result = config['*'];
    if (result) {
        return result;
    }
    return null;
}
function endWithSlash(path) {
    if (path.charAt(path.length - 1) === '/') {
        return path;
    }
    return path + '/';
}
async function getMessagesFromTranslationsService(translationServiceUrl, language, name) {
    const url = endWithSlash(translationServiceUrl) + endWithSlash(language) + 'vscode/' + endWithSlash(name);
    const res = await fetch(url);
    if (res.ok) {
        const messages = await res.json();
        return messages;
    }
    throw new Error(`${res.status} - ${res.statusText}`);
}
function createScopedLocalize(scope) {
    return function (idx, defaultValue) {
        const restArgs = Array.prototype.slice.call(arguments, 2);
        return _format(scope[idx], restArgs);
    };
}
function createScopedLocalize2(scope) {
    return (idx, defaultValue, ...args) => ({
        value: _format(scope[idx], args),
        original: _format(defaultValue, args)
    });
}
// export function localize(data, message, ...args) {
//     return _format(message, args);
// }
// ------------------------invoke----------------------------------------
        export function localize(path, data, defaultMessage, ...args) {
            var key = typeof data === 'object' ? data.key : data;
            var data = ${CURRENT_LOCALE_DATA} || {};
            var message = (data[path] || {})[key];
            if (!message) {
                message = defaultMessage;
            }
            return _format(message, args);
        }
// ------------------------invoke----------------------------------------

export function localize2(path, data, defaultMessage, ...args) {
            var key = typeof data === 'object' ? data.key : data;
            var data = ${CURRENT_LOCALE_DATA} || {};
            var message = (data[path] || {})[key];
            if (!message) {
                message = defaultMessage;
            }
            const original = _format(message, args);
               return {
                value: original,
        original
    };
}
export function getConfiguredDefaultLocale(_) {
    // This returns undefined because this implementation isn't used and is overwritten by the loader
    // when loaded.
    return undefined;
}
export function setPseudoTranslation(value) {
    isPseudo = value;
}
/**
 * Invoked in a built product at run-time
 */
export function create(key, data) {
    var _a;
    return {
        localize: createScopedLocalize(data[key]),
        localize2: createScopedLocalize2(data[key]),
        getConfiguredDefaultLocale: (_a = data.getConfiguredDefaultLocale) !== null && _a !== void 0 ? _a : ((_) => undefined)
    };
}
/**
 * Invoked by the loader at run-time
 */
export function load(name, req, load, config) {
    var _a;
    const pluginConfig = (_a = config['vs/nls']) !== null && _a !== void 0 ? _a : {};
    if (!name || name.length === 0) {
        return load({
            localize: localize,
            localize2: localize2,
            getConfiguredDefaultLocale: () => { var _a; return (_a = pluginConfig.availableLanguages) === null || _a === void 0 ? void 0 : _a['*']; }
        });
    }
    const language = pluginConfig.availableLanguages ? findLanguageForModule(pluginConfig.availableLanguages, name) : null;
    const useDefaultLanguage = language === null || language === DEFAULT_TAG;
    let suffix = '.nls';
    if (!useDefaultLanguage) {
        suffix = suffix + '.' + language;
    }
    const messagesLoaded = (messages) => {
        if (Array.isArray(messages)) {
            messages.localize = createScopedLocalize(messages);
            messages.localize2 = createScopedLocalize2(messages);
        }
        else {
            messages.localize = createScopedLocalize(messages[name]);
            messages.localize2 = createScopedLocalize2(messages[name]);
        }
        messages.getConfiguredDefaultLocale = () => { var _a; return (_a = pluginConfig.availableLanguages) === null || _a === void 0 ? void 0 : _a['*']; };
        load(messages);
    };
    if (typeof pluginConfig.loadBundle === 'function') {
        pluginConfig.loadBundle(name, language, (err, messages) => {
            // We have an error. Load the English default strings to not fail
            if (err) {
                req([name + '.nls'], messagesLoaded);
            }
            else {
                messagesLoaded(messages);
            }
        });
    }
    else if (pluginConfig.translationServiceUrl && !useDefaultLanguage) {
        (async () => {
            var _a;
            try {
                const messages = await getMessagesFromTranslationsService(pluginConfig.translationServiceUrl, language, name);
                return messagesLoaded(messages);
            }
            catch (err) {
                // Language is already as generic as it gets, so require default messages
                if (!language.includes('-')) {
                    console.error(err);
                    return req([name + '.nls'], messagesLoaded);
                }
                try {
                    // Since there is a dash, the language configured is a specific sub-language of the same generic language.
                    // Since we were unable to load the specific language, try to load the generic language. Ex. we failed to find a
                    // Swiss German (de-CH), so try to load the generic German (de) messages instead.
                    const genericLanguage = language.split('-')[0];
                    const messages = await getMessagesFromTranslationsService(pluginConfig.translationServiceUrl, genericLanguage, name);
                    // We got some messages, so we configure the configuration to use the generic language for this session.
                    (_a = pluginConfig.availableLanguages) !== null && _a !== void 0 ? _a : (pluginConfig.availableLanguages = {});
                    pluginConfig.availableLanguages['*'] = genericLanguage;
                    return messagesLoaded(messages);
                }
                catch (err) {
                    console.error(err);
                    return req([name + '.nls'], messagesLoaded);
                }
            }
        })();
    }
    else {
        req([name + suffix], messagesLoaded, (err) => {
            if (suffix === '.nls') {
                console.error('Failed trying to load default language strings', err);
                return;
            }
            console.error(`Failed to load message bundle for language ${language}. Falling back to the default language:`, err);
            req([name + '.nls'], messagesLoaded);
        });
    }
}
    `;
    }
});

最后,执行vite --force重新npm run dev即可。

效果

image.png

由于我使用的是默认这个插件带的汉化包,是不完整的。