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
取出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);
});
}
}