Skip to content
索引

monaco-editor的vite汉化插件vite-plugin-monaco-editor-nls

vite-plugin-monaco-editor-nls原理是通过vite插件钩子,在开发和打包时修改了monaco-editor里的一些代码。

而修改的这部分monaco-editor代码,又来自monaco-editor在开发环境通过npm的方式引入的monaco-editor-core包:monaco-editor-core包是基于vscode仓库中的代码打包的编辑器核心代码。

所以最终来看,vite汉化插件修改的其实是vscode仓库中一部分代码,这部分代码在/src/vs/nls.ts文件中,所以可以找到vscode仓库中的/src/vs/nls.ts文件,查看其原始代码。

插件需要修改代码

插件本身两年未维护了,直接使用会有问题,但是最近有提交,参考最新贡献代码复制代码直接使用

https://github.com/shaoerkuai/vite-plugin-monaco-editor-nls/tree/master

https://juejin.cn/post/7370132224711098395 参考这篇掘金文章,应该同贡献者是一人

修改后的完整插件代码

js
import fs from 'fs'
import path from 'path'
import { Plugin } from 'vite'
import MagicString from 'magic-string'
import { Plugin as EsbuildPlugin } from 'esbuild'

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 = { locale: Languages.en_gb }
): EsbuildPlugin {
  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 MonacoEditorNlsPlugin(
  options: Options = { locale: Languages.en_gb }
): Plugin {
  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)
      ) {
        CURRENT_LOCALE_DATA
        const re = /(?:monaco-editor[/\\]esm[/\\])(.+)(?=\.js)/
        if (re.exec(filepath) && code.includes('localize(')) {
          let path = RegExp.$1
          path = path.replaceAll('\\', '/')
          if (JSON.parse(CURRENT_LOCALE_DATA)[path]) {
            code = code.replace(/localize\(/g, `localize("${path}", `)
            code = code.replace(/localize2\(/g, `localize2('${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.replaceAll('\\', '/')
    code = code.replace(/localize\(/g, `localize('${path}', `)
    code = code.replace(/localize2\(/g, `localize2('${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.
 *--------------------------------------------------------------------------------------------*/

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);
        });
    }
}
    `
}
import fs from 'fs'
import path from 'path'
import { Plugin } from 'vite'
import MagicString from 'magic-string'
import { Plugin as EsbuildPlugin } from 'esbuild'

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 = { locale: Languages.en_gb }
): EsbuildPlugin {
  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 MonacoEditorNlsPlugin(
  options: Options = { locale: Languages.en_gb }
): Plugin {
  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)
      ) {
        CURRENT_LOCALE_DATA
        const re = /(?:monaco-editor[/\\]esm[/\\])(.+)(?=\.js)/
        if (re.exec(filepath) && code.includes('localize(')) {
          let path = RegExp.$1
          path = path.replaceAll('\\', '/')
          if (JSON.parse(CURRENT_LOCALE_DATA)[path]) {
            code = code.replace(/localize\(/g, `localize("${path}", `)
            code = code.replace(/localize2\(/g, `localize2('${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.replaceAll('\\', '/')
    code = code.replace(/localize\(/g, `localize('${path}', `)
    code = code.replace(/localize2\(/g, `localize2('${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.
 *--------------------------------------------------------------------------------------------*/

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);
        });
    }
}
    `
}

更新插件的中文json

https://github.com/microsoft/vscode-loc/blob/main/i18n/vscode-language-pack-zh-hans/translations/main.i18n.json

取出contents中部分

找到vscode仓库中vscode/src/vs/nls.ts文件

插件中替换的代码来自vscode中。

为了正常运行,需要更改ts为js,去除所有类型;

为了转成字符串后通过插件替换,字符串转义字符前要加\转义;

做完这两个前置操作,我们就可以开始修改vscode/src/vs/nls.ts文件代码来实现汉化了

ts
/*---------------------------------------------------------------------------------------------
 *  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';

interface INLSPluginConfig {
	availableLanguages?: INLSPluginConfigAvailableLanguages;
	loadBundle?: BundleLoader;
	translationServiceUrl?: string;
}

export interface INLSPluginConfigAvailableLanguages {
	'*'?: string;
	[module: string]: string | undefined;
}

interface BundleLoader {
	(bundle: string, locale: string | null, cb: (err: Error, messages: string[] | IBundledStrings) => void): void;
}

interface IBundledStrings {
	[moduleId: string]: string[];
}

export interface ILocalizeInfo {
	key: string;
	comment: string[];
}

export interface ILocalizedString {
	original: string;
	value: string;
}

interface ILocalizeFunc {
	(info: ILocalizeInfo, message: string, ...args: (string | number | boolean | undefined | null)[]): string;
	(key: string, message: string, ...args: (string | number | boolean | undefined | null)[]): string;
}

interface IBoundLocalizeFunc {
	(idx: number, defaultValue: null): string;
}

interface ILocalize2Func {
	(info: ILocalizeInfo, message: string, ...args: (string | number | boolean | undefined | null)[]): ILocalizedString;
	(key: string, message: string, ...args: (string | number | boolean | undefined | null)[]): ILocalizedString;
}

interface IBoundLocalize2Func {
	(idx: number, defaultValue: string): ILocalizedString;
}

interface IConsumerAPI {
	localize: ILocalizeFunc | IBoundLocalizeFunc;
	localize2: ILocalize2Func | IBoundLocalize2Func;
	getConfiguredDefaultLocale(stringFromLocalizeCall: string): string | undefined;
}

function _format(message: string, args: (string | number | boolean | undefined | null)[]): string {
	let result: string;

	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: INLSPluginConfigAvailableLanguages, name: string) {
	let result = config[name];
	if (result) {
		return result;
	}
	result = config['*'];
	if (result) {
		return result;
	}
	return null;
}

function endWithSlash(path: string): string {
	if (path.charAt(path.length - 1) === '/') {
		return path;
	}
	return path + '/';
}

async function getMessagesFromTranslationsService(translationServiceUrl: string, language: string, name: string): Promise<string[] | IBundledStrings> {
	const url = endWithSlash(translationServiceUrl) + endWithSlash(language) + 'vscode/' + endWithSlash(name);
	const res = await fetch(url);
	if (res.ok) {
		const messages = await res.json() as string[] | IBundledStrings;
		return messages;
	}
	throw new Error(`${res.status} - ${res.statusText}`);
}

function createScopedLocalize(scope: string[]): IBoundLocalizeFunc {
	return function (idx: number, defaultValue: null) {
		const restArgs = Array.prototype.slice.call(arguments, 2);
		return _format(scope[idx], restArgs);
	};
}

function createScopedLocalize2(scope: string[]): IBoundLocalize2Func {
	return (idx: number, defaultValue: string, ...args) => ({
		value: _format(scope[idx], args),
		original: _format(defaultValue, args)
	});
}

/**
 * Marks a string to be localized. Returns the localized string.
 *
 * @param info The {@linkcode ILocalizeInfo} which describes the id and comments associated with the localized string.
 * @param message The string to localize
 * @param args The arguments to the string
 *
 * @note `message` can contain `{n}` notation where it is replaced by the nth value in `...args`
 * @example `localize({ key: 'sayHello', comment: ['Welcomes user'] }, 'hello {0}', name)`
 *
 * @returns string The localized string.
 */
export function localize(info: ILocalizeInfo, message: string, ...args: (string | number | boolean | undefined | null)[]): string;

/**
 * Marks a string to be localized. Returns the localized string.
 *
 * @param key The key to use for localizing the string
 * @param message The string to localize
 * @param args The arguments to the string
 *
 * @note `message` can contain `{n}` notation where it is replaced by the nth value in `...args`
 * @example For example, `localize('sayHello', 'hello {0}', name)`
 *
 * @returns string The localized string.
 */
export function localize(key: string, message: string, ...args: (string | number | boolean | undefined | null)[]): string;

/**
 * @skipMangle
 */
export function localize(data: ILocalizeInfo | string, message: string, ...args: (string | number | boolean | undefined | null)[]): string {
	return _format(message, args);
}

/**
 * Marks a string to be localized. Returns an {@linkcode ILocalizedString}
 * which contains the localized string and the original string.
 *
 * @param info The {@linkcode ILocalizeInfo} which describes the id and comments associated with the localized string.
 * @param message The string to localize
 * @param args The arguments to the string
 *
 * @note `message` can contain `{n}` notation where it is replaced by the nth value in `...args`
 * @example `localize2({ key: 'sayHello', comment: ['Welcomes user'] }, 'hello {0}', name)`
 *
 * @returns ILocalizedString which contains the localized string and the original string.
 */
export function localize2(info: ILocalizeInfo, message: string, ...args: (string | number | boolean | undefined | null)[]): ILocalizedString;

/**
 * Marks a string to be localized. Returns an {@linkcode ILocalizedString}
 * which contains the localized string and the original string.
 *
 * @param key The key to use for localizing the string
 * @param message The string to localize
 * @param args The arguments to the string
 *
 * @note `message` can contain `{n}` notation where it is replaced by the nth value in `...args`
 * @example `localize('sayHello', 'hello {0}', name)`
 *
 * @returns ILocalizedString which contains the localized string and the original string.
 */
export function localize2(key: string, message: string, ...args: (string | number | boolean | undefined | null)[]): ILocalizedString;

/**
 * @skipMangle
 */
export function localize2(data: ILocalizeInfo | string, message: string, ...args: (string | number | boolean | undefined | null)[]): ILocalizedString {
	const original = _format(message, args);
	return {
		value: original,
		original
	};
}

/**
 *
 * @param stringFromLocalizeCall You must pass in a string that was returned from a `nls.localize()` call
 * in order to ensure the loader plugin has been initialized before this function is called.
 */
export function getConfiguredDefaultLocale(stringFromLocalizeCall: string): string | undefined;
/**
 * @skipMangle
 */
export function getConfiguredDefaultLocale(_: string): string | undefined {
	// This returns undefined because this implementation isn't used and is overwritten by the loader
	// when loaded.
	return undefined;
}

/**
 * @skipMangle
 */
export function setPseudoTranslation(value: boolean) {
	isPseudo = value;
}

/**
 * Invoked in a built product at run-time
 * @skipMangle
 */
export function create(key: string, data: IBundledStrings & IConsumerAPI): IConsumerAPI {
	return {
		localize: createScopedLocalize(data[key]),
		localize2: createScopedLocalize2(data[key]),
		getConfiguredDefaultLocale: data.getConfiguredDefaultLocale ?? ((_: string) => undefined)
	};
}

/**
 * Invoked by the loader at run-time
 * @skipMangle
 */
export function load(name: string, req: AMDLoader.IRelativeRequire, load: AMDLoader.IPluginLoadCallback, config: AMDLoader.IConfigurationOptions): void {
	const pluginConfig: INLSPluginConfig = config['vs/nls'] ?? {};
	if (!name || name.length === 0) {
		// TODO: We need to give back the mangled names here
		return load({
			localize: localize,
			localize2: localize2,
			getConfiguredDefaultLocale: () => pluginConfig.availableLanguages?.['*']
		} as IConsumerAPI);
	}
	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: string[] | IBundledStrings) => {
		if (Array.isArray(messages)) {
			(messages as any as IConsumerAPI).localize = createScopedLocalize(messages);
			(messages as any as IConsumerAPI).localize2 = createScopedLocalize2(messages);
		} else {
			(messages as any as IConsumerAPI).localize = createScopedLocalize(messages[name]);
			(messages as any as IConsumerAPI).localize2 = createScopedLocalize2(messages[name]);
		}
		(messages as any as IConsumerAPI).getConfiguredDefaultLocale = () => pluginConfig.availableLanguages?.['*'];
		load(messages);
	};
	if (typeof pluginConfig.loadBundle === 'function') {
		(pluginConfig.loadBundle as BundleLoader)(name, language, (err: Error, 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 () => {
			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.
					pluginConfig.availableLanguages ??= {};
					pluginConfig.availableLanguages['*'] = genericLanguage;
					return messagesLoaded(messages);
				} catch (err) {
					console.error(err);
					return req([name + '.nls'], messagesLoaded);
				}
			}
		})();
	} else {
		req([name + suffix], messagesLoaded, (err: Error) => {
			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);
		});
	}
}
/*---------------------------------------------------------------------------------------------
 *  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';

interface INLSPluginConfig {
	availableLanguages?: INLSPluginConfigAvailableLanguages;
	loadBundle?: BundleLoader;
	translationServiceUrl?: string;
}

export interface INLSPluginConfigAvailableLanguages {
	'*'?: string;
	[module: string]: string | undefined;
}

interface BundleLoader {
	(bundle: string, locale: string | null, cb: (err: Error, messages: string[] | IBundledStrings) => void): void;
}

interface IBundledStrings {
	[moduleId: string]: string[];
}

export interface ILocalizeInfo {
	key: string;
	comment: string[];
}

export interface ILocalizedString {
	original: string;
	value: string;
}

interface ILocalizeFunc {
	(info: ILocalizeInfo, message: string, ...args: (string | number | boolean | undefined | null)[]): string;
	(key: string, message: string, ...args: (string | number | boolean | undefined | null)[]): string;
}

interface IBoundLocalizeFunc {
	(idx: number, defaultValue: null): string;
}

interface ILocalize2Func {
	(info: ILocalizeInfo, message: string, ...args: (string | number | boolean | undefined | null)[]): ILocalizedString;
	(key: string, message: string, ...args: (string | number | boolean | undefined | null)[]): ILocalizedString;
}

interface IBoundLocalize2Func {
	(idx: number, defaultValue: string): ILocalizedString;
}

interface IConsumerAPI {
	localize: ILocalizeFunc | IBoundLocalizeFunc;
	localize2: ILocalize2Func | IBoundLocalize2Func;
	getConfiguredDefaultLocale(stringFromLocalizeCall: string): string | undefined;
}

function _format(message: string, args: (string | number | boolean | undefined | null)[]): string {
	let result: string;

	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: INLSPluginConfigAvailableLanguages, name: string) {
	let result = config[name];
	if (result) {
		return result;
	}
	result = config['*'];
	if (result) {
		return result;
	}
	return null;
}

function endWithSlash(path: string): string {
	if (path.charAt(path.length - 1) === '/') {
		return path;
	}
	return path + '/';
}

async function getMessagesFromTranslationsService(translationServiceUrl: string, language: string, name: string): Promise<string[] | IBundledStrings> {
	const url = endWithSlash(translationServiceUrl) + endWithSlash(language) + 'vscode/' + endWithSlash(name);
	const res = await fetch(url);
	if (res.ok) {
		const messages = await res.json() as string[] | IBundledStrings;
		return messages;
	}
	throw new Error(`${res.status} - ${res.statusText}`);
}

function createScopedLocalize(scope: string[]): IBoundLocalizeFunc {
	return function (idx: number, defaultValue: null) {
		const restArgs = Array.prototype.slice.call(arguments, 2);
		return _format(scope[idx], restArgs);
	};
}

function createScopedLocalize2(scope: string[]): IBoundLocalize2Func {
	return (idx: number, defaultValue: string, ...args) => ({
		value: _format(scope[idx], args),
		original: _format(defaultValue, args)
	});
}

/**
 * Marks a string to be localized. Returns the localized string.
 *
 * @param info The {@linkcode ILocalizeInfo} which describes the id and comments associated with the localized string.
 * @param message The string to localize
 * @param args The arguments to the string
 *
 * @note `message` can contain `{n}` notation where it is replaced by the nth value in `...args`
 * @example `localize({ key: 'sayHello', comment: ['Welcomes user'] }, 'hello {0}', name)`
 *
 * @returns string The localized string.
 */
export function localize(info: ILocalizeInfo, message: string, ...args: (string | number | boolean | undefined | null)[]): string;

/**
 * Marks a string to be localized. Returns the localized string.
 *
 * @param key The key to use for localizing the string
 * @param message The string to localize
 * @param args The arguments to the string
 *
 * @note `message` can contain `{n}` notation where it is replaced by the nth value in `...args`
 * @example For example, `localize('sayHello', 'hello {0}', name)`
 *
 * @returns string The localized string.
 */
export function localize(key: string, message: string, ...args: (string | number | boolean | undefined | null)[]): string;

/**
 * @skipMangle
 */
export function localize(data: ILocalizeInfo | string, message: string, ...args: (string | number | boolean | undefined | null)[]): string {
	return _format(message, args);
}

/**
 * Marks a string to be localized. Returns an {@linkcode ILocalizedString}
 * which contains the localized string and the original string.
 *
 * @param info The {@linkcode ILocalizeInfo} which describes the id and comments associated with the localized string.
 * @param message The string to localize
 * @param args The arguments to the string
 *
 * @note `message` can contain `{n}` notation where it is replaced by the nth value in `...args`
 * @example `localize2({ key: 'sayHello', comment: ['Welcomes user'] }, 'hello {0}', name)`
 *
 * @returns ILocalizedString which contains the localized string and the original string.
 */
export function localize2(info: ILocalizeInfo, message: string, ...args: (string | number | boolean | undefined | null)[]): ILocalizedString;

/**
 * Marks a string to be localized. Returns an {@linkcode ILocalizedString}
 * which contains the localized string and the original string.
 *
 * @param key The key to use for localizing the string
 * @param message The string to localize
 * @param args The arguments to the string
 *
 * @note `message` can contain `{n}` notation where it is replaced by the nth value in `...args`
 * @example `localize('sayHello', 'hello {0}', name)`
 *
 * @returns ILocalizedString which contains the localized string and the original string.
 */
export function localize2(key: string, message: string, ...args: (string | number | boolean | undefined | null)[]): ILocalizedString;

/**
 * @skipMangle
 */
export function localize2(data: ILocalizeInfo | string, message: string, ...args: (string | number | boolean | undefined | null)[]): ILocalizedString {
	const original = _format(message, args);
	return {
		value: original,
		original
	};
}

/**
 *
 * @param stringFromLocalizeCall You must pass in a string that was returned from a `nls.localize()` call
 * in order to ensure the loader plugin has been initialized before this function is called.
 */
export function getConfiguredDefaultLocale(stringFromLocalizeCall: string): string | undefined;
/**
 * @skipMangle
 */
export function getConfiguredDefaultLocale(_: string): string | undefined {
	// This returns undefined because this implementation isn't used and is overwritten by the loader
	// when loaded.
	return undefined;
}

/**
 * @skipMangle
 */
export function setPseudoTranslation(value: boolean) {
	isPseudo = value;
}

/**
 * Invoked in a built product at run-time
 * @skipMangle
 */
export function create(key: string, data: IBundledStrings & IConsumerAPI): IConsumerAPI {
	return {
		localize: createScopedLocalize(data[key]),
		localize2: createScopedLocalize2(data[key]),
		getConfiguredDefaultLocale: data.getConfiguredDefaultLocale ?? ((_: string) => undefined)
	};
}

/**
 * Invoked by the loader at run-time
 * @skipMangle
 */
export function load(name: string, req: AMDLoader.IRelativeRequire, load: AMDLoader.IPluginLoadCallback, config: AMDLoader.IConfigurationOptions): void {
	const pluginConfig: INLSPluginConfig = config['vs/nls'] ?? {};
	if (!name || name.length === 0) {
		// TODO: We need to give back the mangled names here
		return load({
			localize: localize,
			localize2: localize2,
			getConfiguredDefaultLocale: () => pluginConfig.availableLanguages?.['*']
		} as IConsumerAPI);
	}
	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: string[] | IBundledStrings) => {
		if (Array.isArray(messages)) {
			(messages as any as IConsumerAPI).localize = createScopedLocalize(messages);
			(messages as any as IConsumerAPI).localize2 = createScopedLocalize2(messages);
		} else {
			(messages as any as IConsumerAPI).localize = createScopedLocalize(messages[name]);
			(messages as any as IConsumerAPI).localize2 = createScopedLocalize2(messages[name]);
		}
		(messages as any as IConsumerAPI).getConfiguredDefaultLocale = () => pluginConfig.availableLanguages?.['*'];
		load(messages);
	};
	if (typeof pluginConfig.loadBundle === 'function') {
		(pluginConfig.loadBundle as BundleLoader)(name, language, (err: Error, 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 () => {
			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.
					pluginConfig.availableLanguages ??= {};
					pluginConfig.availableLanguages['*'] = genericLanguage;
					return messagesLoaded(messages);
				} catch (err) {
					console.error(err);
					return req([name + '.nls'], messagesLoaded);
				}
			}
		})();
	} else {
		req([name + suffix], messagesLoaded, (err: Error) => {
			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);
		});
	}
}

Released under the MIT License.