393 lines
13 KiB
TypeScript
393 lines
13 KiB
TypeScript
import fs from 'node:fs'
|
||
import path from 'node:path'
|
||
import type { Plugin } from 'vite'
|
||
import MagicString from 'magic-string'
|
||
|
||
export enum Languages {
|
||
bg = 'bg',
|
||
cs = 'cs',
|
||
de = 'de',
|
||
en_gb = 'en-gb',
|
||
es = 'es',
|
||
fr = 'fr',
|
||
hu = 'hu',
|
||
id = 'id',
|
||
it = 'it',
|
||
ja = 'ja',
|
||
ko = 'ko',
|
||
nl = 'nl',
|
||
pl = 'pl',
|
||
ps = 'ps',
|
||
pt_br = 'pt-br',
|
||
ru = 'ru',
|
||
tr = 'tr',
|
||
uk = 'uk',
|
||
zh_hans = 'zh-hans',
|
||
zh_hant = 'zh-hant',
|
||
}
|
||
|
||
export interface Options {
|
||
locale: Languages;
|
||
localeData?: Record<string, any>
|
||
}
|
||
|
||
/**
|
||
* 在vite中dev模式下会使用esbuild对node_modules进行预编译,导致找不到映射表中的filepath,
|
||
* 需要在预编译之前进行替换
|
||
* @param options 替换语言包
|
||
* @returns
|
||
*/
|
||
export function esbuildPluginMonacoEditorNls(options: Options): any {
|
||
options = Object.assign({ locale: Languages.en_gb }, options)
|
||
const CURRENT_LOCALE_DATA = getLocalizeMapping(options.locale, options.localeData)
|
||
|
||
return {
|
||
name: 'esbuild-plugin-monaco-editor-nls',
|
||
setup(build) {
|
||
build.onLoad({ filter: /esm[/\\]vs[/\\]nls\.js/ }, async () => {
|
||
return {
|
||
contents: getLocalizeCode(CURRENT_LOCALE_DATA),
|
||
loader: 'js',
|
||
}
|
||
})
|
||
|
||
build.onLoad(
|
||
{ filter: /monaco-editor[/\\]esm[/\\]vs.+\.js/ },
|
||
async args => {
|
||
return {
|
||
contents: transformLocalizeFuncCode(
|
||
args.path,
|
||
CURRENT_LOCALE_DATA,
|
||
),
|
||
loader: 'js',
|
||
}
|
||
},
|
||
)
|
||
},
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 使用了monaco-editor-nls的语言映射包,把原始localize(data, message)的方法,替换成了localize(path, data, defaultMessage)
|
||
* vite build 模式下,使用rollup处理
|
||
* @param options 替换语言包
|
||
* @returns
|
||
*/
|
||
export default function (options: Options): Plugin {
|
||
options = Object.assign({ locale: Languages.en_gb }, options)
|
||
const CURRENT_LOCALE_DATA = getLocalizeMapping(options.locale, options.localeData)
|
||
|
||
return {
|
||
name: 'rollup-plugin-monaco-editor-nls',
|
||
|
||
enforce: 'pre',
|
||
|
||
load(filepath) {
|
||
if (/esm[/\\]vs[/\\]nls\.js/.test(filepath)) {
|
||
|
||
return getLocalizeCode(CURRENT_LOCALE_DATA)
|
||
}
|
||
},
|
||
transform(code, filepath) {
|
||
if (
|
||
/monaco-editor[/\\]esm[/\\]vs.+\.js/.test(filepath)
|
||
&& !/esm[/\\]vs[/\\].*nls\.js/.test(filepath)
|
||
) {
|
||
const re = /monaco-editor[/\\]esm[/\\](.+)(?=\.js)/
|
||
if (re.exec(filepath) && code.includes('localize(')) {
|
||
let path = RegExp.$1
|
||
path = path.replaceAll('\\', '/')
|
||
code = code.replace(/localize\(/g, `localize('${path}', `)
|
||
|
||
return {
|
||
code: code,
|
||
|
||
/** 使用magic-string 生成 source map */
|
||
map: new MagicString(code).generateMap({
|
||
includeContent: true,
|
||
hires: true,
|
||
source: filepath,
|
||
}),
|
||
}
|
||
}
|
||
}
|
||
},
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 替换调用方法接口参数,替换成相应语言包语言
|
||
* @param filepath 路径
|
||
* @param CURRENT_LOCALE_DATA 替换规则
|
||
* @returns
|
||
*/
|
||
function transformLocalizeFuncCode(
|
||
filepath: string,
|
||
_CURRENT_LOCALE_DATA: string,
|
||
) {
|
||
let code = fs.readFileSync(filepath, 'utf8')
|
||
const re = /monaco-editor[/\\]esm[/\\](.+)(?=\.js)/
|
||
if (re.exec(filepath)) {
|
||
let path = RegExp.$1
|
||
path = path.replaceAll('\\', '/')
|
||
code = code.replace(/localize\(/g, `localize('${path}', `)
|
||
}
|
||
|
||
return code
|
||
}
|
||
|
||
/**
|
||
* 获取语言包
|
||
* @param locale 语言
|
||
* @param localeData
|
||
* @returns
|
||
*/
|
||
function getLocalizeMapping(locale: Languages, localeData: Record<string, any> | undefined = undefined) {
|
||
if (localeData) return JSON.stringify(localeData)
|
||
const locale_data_path = path.join(__dirname, `./locale/${locale}.json`)
|
||
|
||
return fs.readFileSync(locale_data_path) as unknown as string
|
||
}
|
||
|
||
/**
|
||
* 替换代码
|
||
* @param CURRENT_LOCALE_DATA 语言包
|
||
* @returns
|
||
*/
|
||
function getLocalizeCode(CURRENT_LOCALE_DATA: string) {
|
||
return `
|
||
/* ---------------------------------------------------------------------------------------------
|
||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||
*--------------------------------------------------------------------------------------------*/
|
||
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(/[aeiou]/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] || data?.contents?.[path] || {})[key];
|
||
if (!message) {
|
||
message = defaultMessage;
|
||
}
|
||
return _format(message, args);
|
||
}
|
||
// ------------------------invoke----------------------------------------
|
||
|
||
/**
|
||
* @skipMangle
|
||
*/
|
||
export function localize2(data, message, ...args) {
|
||
const original = _format(message, args)
|
||
|
||
return {
|
||
value: original,
|
||
original,
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @skipMangle
|
||
*/
|
||
export function getConfiguredDefaultLocale(_) {
|
||
|
||
// This returns undefined because this implementation isn't used and is overwritten by the loader
|
||
// when loaded.
|
||
return undefined
|
||
}
|
||
|
||
/**
|
||
* @skipMangle
|
||
*/
|
||
export function setPseudoTranslation(value) {
|
||
isPseudo = value
|
||
}
|
||
|
||
/**
|
||
* Invoked in a built product at run-time
|
||
* @skipMangle
|
||
*/
|
||
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
|
||
* @skipMangle
|
||
*/
|
||
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) {
|
||
|
||
// TODO: We need to give back the mangled names here
|
||
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)
|
||
})
|
||
}
|
||
}
|
||
`
|
||
}
|