mirror of
https://github.com/Microsoft/vscode
synced 2024-09-21 03:30:00 +00:00
Improvements to i18n (#163372)
* remove dead code from Transifex * use @vscode/l10n-dev for XLF operations for extensions * generated files * more generated files * remove dead code * move l10n-dev to where gulp is * generated
This commit is contained in:
parent
22ff985c19
commit
342aa9c59a
|
@ -4,19 +4,18 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.prepareIslFiles = exports.prepareI18nPackFiles = exports.prepareI18nFiles = exports.pullSetupXlfFiles = exports.findObsoleteResources = exports.pushXlfFiles = exports.createXlfFilesForIsl = exports.createXlfFilesForExtensions = exports.createXlfFilesForCoreBundle = exports.getResource = exports.processNlsFiles = exports.Limiter = exports.XLF = exports.Line = exports.externalExtensionsWithTranslations = exports.extraLanguages = exports.defaultLanguages = void 0;
|
||||
exports.prepareIslFiles = exports.prepareI18nPackFiles = exports.createXlfFilesForIsl = exports.createXlfFilesForExtensions = exports.createXlfFilesForCoreBundle = exports.getResource = exports.processNlsFiles = exports.XLF = exports.Line = exports.externalExtensionsWithTranslations = exports.extraLanguages = exports.defaultLanguages = void 0;
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const event_stream_1 = require("event-stream");
|
||||
const File = require("vinyl");
|
||||
const Is = require("is");
|
||||
const xml2js = require("xml2js");
|
||||
const https = require("https");
|
||||
const gulp = require("gulp");
|
||||
const fancyLog = require("fancy-log");
|
||||
const ansiColors = require("ansi-colors");
|
||||
const iconv = require("@vscode/iconv-lite-umd");
|
||||
const NUMBER_OF_CONCURRENT_DOWNLOADS = 4;
|
||||
const l10n_dev_1 = require("@vscode/l10n-dev");
|
||||
function log(message, ...rest) {
|
||||
fancyLog(ansiColors.green('[i18n]'), message, ...rest);
|
||||
}
|
||||
|
@ -63,19 +62,6 @@ var BundledFormat;
|
|||
}
|
||||
BundledFormat.is = is;
|
||||
})(BundledFormat || (BundledFormat = {}));
|
||||
var PackageJsonFormat;
|
||||
(function (PackageJsonFormat) {
|
||||
function is(value) {
|
||||
if (Is.undef(value) || !Is.object(value)) {
|
||||
return false;
|
||||
}
|
||||
return Object.keys(value).every(key => {
|
||||
const element = value[key];
|
||||
return Is.string(element) || (Is.object(element) && Is.defined(element.message) && Is.defined(element.comment));
|
||||
});
|
||||
}
|
||||
PackageJsonFormat.is = is;
|
||||
})(PackageJsonFormat || (PackageJsonFormat = {}));
|
||||
class Line {
|
||||
constructor(indent = 0) {
|
||||
this.buffer = [];
|
||||
|
@ -184,31 +170,6 @@ class XLF {
|
|||
}
|
||||
}
|
||||
exports.XLF = XLF;
|
||||
XLF.parsePseudo = function (xlfString) {
|
||||
return new Promise((resolve) => {
|
||||
const parser = new xml2js.Parser();
|
||||
const files = [];
|
||||
parser.parseString(xlfString, function (_err, result) {
|
||||
const fileNodes = result['xliff']['file'];
|
||||
fileNodes.forEach(file => {
|
||||
const originalFilePath = file.$.original;
|
||||
const messages = {};
|
||||
const transUnits = file.body[0]['trans-unit'];
|
||||
if (transUnits) {
|
||||
transUnits.forEach((unit) => {
|
||||
const key = unit.$.id;
|
||||
const val = pseudify(unit.source[0]['_'].toString());
|
||||
if (key && val) {
|
||||
messages[key] = decodeEntities(val);
|
||||
}
|
||||
});
|
||||
files.push({ messages: messages, originalFilePath: originalFilePath, language: 'ps' });
|
||||
}
|
||||
});
|
||||
resolve(files);
|
||||
});
|
||||
});
|
||||
};
|
||||
XLF.parse = function (xlfString) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const parser = new xml2js.Parser();
|
||||
|
@ -222,8 +183,8 @@ XLF.parse = function (xlfString) {
|
|||
reject(new Error(`XLF parsing error: XLIFF file does not contain "xliff" or "file" node(s) required for parsing.`));
|
||||
}
|
||||
fileNodes.forEach((file) => {
|
||||
const originalFilePath = file.$.original;
|
||||
if (!originalFilePath) {
|
||||
const name = file.$.original;
|
||||
if (!name) {
|
||||
reject(new Error(`XLF parsing error: XLIFF file node does not contain original attribute to determine the original location of the resource file.`));
|
||||
}
|
||||
const language = file.$['target-language'];
|
||||
|
@ -244,45 +205,18 @@ XLF.parse = function (xlfString) {
|
|||
val = val._ ? val._ : '';
|
||||
}
|
||||
if (!key) {
|
||||
reject(new Error(`XLF parsing error: trans-unit ${JSON.stringify(unit, undefined, 0)} defined in file ${originalFilePath} is missing the ID attribute.`));
|
||||
reject(new Error(`XLF parsing error: trans-unit ${JSON.stringify(unit, undefined, 0)} defined in file ${name} is missing the ID attribute.`));
|
||||
return;
|
||||
}
|
||||
messages[key] = decodeEntities(val);
|
||||
});
|
||||
files.push({ messages: messages, originalFilePath: originalFilePath, language: language.toLowerCase() });
|
||||
files.push({ messages, name, language: language.toLowerCase() });
|
||||
}
|
||||
});
|
||||
resolve(files);
|
||||
});
|
||||
});
|
||||
};
|
||||
class Limiter {
|
||||
constructor(maxDegreeOfParalellism) {
|
||||
this.maxDegreeOfParalellism = maxDegreeOfParalellism;
|
||||
this.outstandingPromises = [];
|
||||
this.runningPromises = 0;
|
||||
}
|
||||
queue(factory) {
|
||||
return new Promise((c, e) => {
|
||||
this.outstandingPromises.push({ factory, c, e });
|
||||
this.consume();
|
||||
});
|
||||
}
|
||||
consume() {
|
||||
while (this.outstandingPromises.length && this.runningPromises < this.maxDegreeOfParalellism) {
|
||||
const iLimitedTask = this.outstandingPromises.shift();
|
||||
this.runningPromises++;
|
||||
const promise = iLimitedTask.factory();
|
||||
promise.then(iLimitedTask.c).catch(iLimitedTask.e);
|
||||
promise.then(() => this.consumed()).catch(() => this.consumed());
|
||||
}
|
||||
}
|
||||
consumed() {
|
||||
this.runningPromises--;
|
||||
this.consume();
|
||||
}
|
||||
}
|
||||
exports.Limiter = Limiter;
|
||||
function sortLanguages(languages) {
|
||||
return languages.sort((a, b) => {
|
||||
return a.id < b.id ? -1 : (a.id > b.id ? 1 : 0);
|
||||
|
@ -589,12 +523,12 @@ function createXlfFilesForExtensions() {
|
|||
return;
|
||||
}
|
||||
counter++;
|
||||
let _xlf;
|
||||
function getXlf() {
|
||||
if (!_xlf) {
|
||||
_xlf = new XLF(extensionsProject);
|
||||
let _l10nMap;
|
||||
function getL10nMap() {
|
||||
if (!_l10nMap) {
|
||||
_l10nMap = new Map();
|
||||
}
|
||||
return _xlf;
|
||||
return _l10nMap;
|
||||
}
|
||||
gulp.src([`.build/extensions/${extensionName}/package.nls.json`, `.build/extensions/${extensionName}/**/nls.metadata.json`], { allowEmpty: true }).pipe((0, event_stream_1.through)(function (file) {
|
||||
if (file.isBuffer()) {
|
||||
|
@ -602,34 +536,22 @@ function createXlfFilesForExtensions() {
|
|||
const basename = path.basename(file.path);
|
||||
if (basename === 'package.nls.json') {
|
||||
const json = JSON.parse(buffer.toString('utf8'));
|
||||
const keys = [];
|
||||
const messages = [];
|
||||
Object.keys(json).forEach((key) => {
|
||||
const value = json[key];
|
||||
if (Is.string(value)) {
|
||||
keys.push(key);
|
||||
messages.push(value);
|
||||
}
|
||||
else if (value) {
|
||||
keys.push({
|
||||
key,
|
||||
comment: value.comment
|
||||
});
|
||||
messages.push(value.message);
|
||||
}
|
||||
else {
|
||||
keys.push(key);
|
||||
messages.push(`Unknown message for key: ${key}`);
|
||||
}
|
||||
});
|
||||
getXlf().addFile(`extensions/${extensionName}/package`, keys, messages);
|
||||
getL10nMap().set(`extensions/${extensionName}/package`, json);
|
||||
}
|
||||
else if (basename === 'nls.metadata.json') {
|
||||
const json = JSON.parse(buffer.toString('utf8'));
|
||||
const relPath = path.relative(`.build/extensions/${extensionName}`, path.dirname(file.path));
|
||||
for (const file in json) {
|
||||
const fileContent = json[file];
|
||||
getXlf().addFile(`extensions/${extensionName}/${relPath}/${file}`, fileContent.keys, fileContent.messages);
|
||||
const info = Object.create(null);
|
||||
for (let i = 0; i < fileContent.messages.length; i++) {
|
||||
const message = fileContent.messages[i];
|
||||
const { key, comment } = LocalizeInfo.is(fileContent.keys[i])
|
||||
? fileContent.keys[i]
|
||||
: { key: fileContent.keys[i], comment: undefined };
|
||||
info[key] = comment ? { message, comment } : message;
|
||||
}
|
||||
getL10nMap().set(`extensions/${extensionName}/${relPath}/${file}`, info);
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
@ -638,10 +560,10 @@ function createXlfFilesForExtensions() {
|
|||
}
|
||||
}
|
||||
}, function () {
|
||||
if (_xlf) {
|
||||
if (_l10nMap) {
|
||||
const xlfFile = new File({
|
||||
path: path.join(extensionsProject, extensionName + '.xlf'),
|
||||
contents: Buffer.from(_xlf.toString(), 'utf8')
|
||||
contents: Buffer.from((0, l10n_dev_1.getL10nXlf)(_l10nMap), 'utf8')
|
||||
});
|
||||
folderStream.queue(xlfFile);
|
||||
}
|
||||
|
@ -712,299 +634,7 @@ function createXlfFilesForIsl() {
|
|||
});
|
||||
}
|
||||
exports.createXlfFilesForIsl = createXlfFilesForIsl;
|
||||
function pushXlfFiles(apiHostname, username, password) {
|
||||
const tryGetPromises = [];
|
||||
const updateCreatePromises = [];
|
||||
return (0, event_stream_1.through)(function (file) {
|
||||
const project = path.dirname(file.relative);
|
||||
const fileName = path.basename(file.path);
|
||||
const slug = fileName.substr(0, fileName.length - '.xlf'.length);
|
||||
const credentials = `${username}:${password}`;
|
||||
// Check if resource already exists, if not, then create it.
|
||||
let promise = tryGetResource(project, slug, apiHostname, credentials);
|
||||
tryGetPromises.push(promise);
|
||||
promise.then(exists => {
|
||||
if (exists) {
|
||||
promise = updateResource(project, slug, file, apiHostname, credentials);
|
||||
}
|
||||
else {
|
||||
promise = createResource(project, slug, file, apiHostname, credentials);
|
||||
}
|
||||
updateCreatePromises.push(promise);
|
||||
});
|
||||
}, function () {
|
||||
// End the pipe only after all the communication with Transifex API happened
|
||||
Promise.all(tryGetPromises).then(() => {
|
||||
Promise.all(updateCreatePromises).then(() => {
|
||||
this.queue(null);
|
||||
}).catch((reason) => { throw new Error(reason); });
|
||||
}).catch((reason) => { throw new Error(reason); });
|
||||
});
|
||||
}
|
||||
exports.pushXlfFiles = pushXlfFiles;
|
||||
function getAllResources(project, apiHostname, username, password) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const credentials = `${username}:${password}`;
|
||||
const options = {
|
||||
hostname: apiHostname,
|
||||
path: `/api/2/project/${project}/resources`,
|
||||
auth: credentials,
|
||||
method: 'GET'
|
||||
};
|
||||
const request = https.request(options, (res) => {
|
||||
const buffer = [];
|
||||
res.on('data', (chunk) => buffer.push(chunk));
|
||||
res.on('end', () => {
|
||||
if (res.statusCode === 200) {
|
||||
const json = JSON.parse(Buffer.concat(buffer).toString());
|
||||
if (Array.isArray(json)) {
|
||||
resolve(json.map(o => o.slug));
|
||||
return;
|
||||
}
|
||||
reject(`Unexpected data format. Response code: ${res.statusCode}.`);
|
||||
}
|
||||
else {
|
||||
reject(`No resources in ${project} returned no data. Response code: ${res.statusCode}.`);
|
||||
}
|
||||
});
|
||||
});
|
||||
request.on('error', (err) => {
|
||||
reject(`Failed to query resources in ${project} with the following error: ${err}. ${options.path}`);
|
||||
});
|
||||
request.end();
|
||||
});
|
||||
}
|
||||
function findObsoleteResources(apiHostname, username, password) {
|
||||
const resourcesByProject = Object.create(null);
|
||||
resourcesByProject[extensionsProject] = [].concat(exports.externalExtensionsWithTranslations); // clone
|
||||
return (0, event_stream_1.through)(function (file) {
|
||||
const project = path.dirname(file.relative);
|
||||
const fileName = path.basename(file.path);
|
||||
const slug = fileName.substr(0, fileName.length - '.xlf'.length);
|
||||
let slugs = resourcesByProject[project];
|
||||
if (!slugs) {
|
||||
resourcesByProject[project] = slugs = [];
|
||||
}
|
||||
slugs.push(slug);
|
||||
this.push(file);
|
||||
}, function () {
|
||||
const json = JSON.parse(fs.readFileSync('./build/lib/i18n.resources.json', 'utf8'));
|
||||
const i18Resources = [...json.editor, ...json.workbench].map((r) => r.project + '/' + r.name.replace(/\//g, '_'));
|
||||
const extractedResources = [];
|
||||
for (const project of [workbenchProject, editorProject]) {
|
||||
for (const resource of resourcesByProject[project]) {
|
||||
if (resource !== 'setup_messages') {
|
||||
extractedResources.push(project + '/' + resource);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (i18Resources.length !== extractedResources.length) {
|
||||
console.log(`[i18n] Obsolete resources in file 'build/lib/i18n.resources.json': JSON.stringify(${i18Resources.filter(p => extractedResources.indexOf(p) === -1)})`);
|
||||
console.log(`[i18n] Missing resources in file 'build/lib/i18n.resources.json': JSON.stringify(${extractedResources.filter(p => i18Resources.indexOf(p) === -1)})`);
|
||||
}
|
||||
const promises = [];
|
||||
for (const project in resourcesByProject) {
|
||||
promises.push(getAllResources(project, apiHostname, username, password).then(resources => {
|
||||
const expectedResources = resourcesByProject[project];
|
||||
const unusedResources = resources.filter(resource => resource && expectedResources.indexOf(resource) === -1);
|
||||
if (unusedResources.length) {
|
||||
console.log(`[transifex] Obsolete resources in project '${project}': ${unusedResources.join(', ')}`);
|
||||
}
|
||||
}));
|
||||
}
|
||||
return Promise.all(promises).then(_ => {
|
||||
this.push(null);
|
||||
}).catch((reason) => { throw new Error(reason); });
|
||||
});
|
||||
}
|
||||
exports.findObsoleteResources = findObsoleteResources;
|
||||
function tryGetResource(project, slug, apiHostname, credentials) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const options = {
|
||||
hostname: apiHostname,
|
||||
path: `/api/2/project/${project}/resource/${slug}/?details`,
|
||||
auth: credentials,
|
||||
method: 'GET'
|
||||
};
|
||||
const request = https.request(options, (response) => {
|
||||
if (response.statusCode === 404) {
|
||||
resolve(false);
|
||||
}
|
||||
else if (response.statusCode === 200) {
|
||||
resolve(true);
|
||||
}
|
||||
else {
|
||||
reject(`Failed to query resource ${project}/${slug}. Response: ${response.statusCode} ${response.statusMessage}`);
|
||||
}
|
||||
});
|
||||
request.on('error', (err) => {
|
||||
reject(`Failed to get ${project}/${slug} on Transifex: ${err}`);
|
||||
});
|
||||
request.end();
|
||||
});
|
||||
}
|
||||
function createResource(project, slug, xlfFile, apiHostname, credentials) {
|
||||
return new Promise((_resolve, reject) => {
|
||||
const data = JSON.stringify({
|
||||
'content': xlfFile.contents.toString(),
|
||||
'name': slug,
|
||||
'slug': slug,
|
||||
'i18n_type': 'XLIFF'
|
||||
});
|
||||
const options = {
|
||||
hostname: apiHostname,
|
||||
path: `/api/2/project/${project}/resources`,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(data)
|
||||
},
|
||||
auth: credentials,
|
||||
method: 'POST'
|
||||
};
|
||||
const request = https.request(options, (res) => {
|
||||
if (res.statusCode === 201) {
|
||||
log(`Resource ${project}/${slug} successfully created on Transifex.`);
|
||||
}
|
||||
else {
|
||||
reject(`Something went wrong in the request creating ${slug} in ${project}. ${res.statusCode}`);
|
||||
}
|
||||
});
|
||||
request.on('error', (err) => {
|
||||
reject(`Failed to create ${project}/${slug} on Transifex: ${err}`);
|
||||
});
|
||||
request.write(data);
|
||||
request.end();
|
||||
});
|
||||
}
|
||||
/**
|
||||
* The following link provides information about how Transifex handles updates of a resource file:
|
||||
* https://dev.befoolish.co/tx-docs/public/projects/updating-content#what-happens-when-you-update-files
|
||||
*/
|
||||
function updateResource(project, slug, xlfFile, apiHostname, credentials) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const data = JSON.stringify({ content: xlfFile.contents.toString() });
|
||||
const options = {
|
||||
hostname: apiHostname,
|
||||
path: `/api/2/project/${project}/resource/${slug}/content`,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(data)
|
||||
},
|
||||
auth: credentials,
|
||||
method: 'PUT'
|
||||
};
|
||||
const request = https.request(options, (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
res.setEncoding('utf8');
|
||||
let responseBuffer = '';
|
||||
res.on('data', function (chunk) {
|
||||
responseBuffer += chunk;
|
||||
});
|
||||
res.on('end', () => {
|
||||
const response = JSON.parse(responseBuffer);
|
||||
log(`Resource ${project}/${slug} successfully updated on Transifex. Strings added: ${response.strings_added}, updated: ${response.strings_added}, deleted: ${response.strings_added}`);
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
else {
|
||||
reject(`Something went wrong in the request updating ${slug} in ${project}. ${res.statusCode}`);
|
||||
}
|
||||
});
|
||||
request.on('error', (err) => {
|
||||
reject(`Failed to update ${project}/${slug} on Transifex: ${err}`);
|
||||
});
|
||||
request.write(data);
|
||||
request.end();
|
||||
});
|
||||
}
|
||||
function pullSetupXlfFiles(apiHostname, username, password, language, includeDefault) {
|
||||
const setupResources = [{ name: 'setup_messages', project: workbenchProject }];
|
||||
if (includeDefault) {
|
||||
setupResources.push({ name: 'setup_default', project: setupProject });
|
||||
}
|
||||
return pullXlfFiles(apiHostname, username, password, language, setupResources);
|
||||
}
|
||||
exports.pullSetupXlfFiles = pullSetupXlfFiles;
|
||||
function pullXlfFiles(apiHostname, username, password, language, resources) {
|
||||
const credentials = `${username}:${password}`;
|
||||
const expectedTranslationsCount = resources.length;
|
||||
let translationsRetrieved = 0, called = false;
|
||||
return (0, event_stream_1.readable)(function (_count, callback) {
|
||||
// Mark end of stream when all resources were retrieved
|
||||
if (translationsRetrieved === expectedTranslationsCount) {
|
||||
return this.emit('end');
|
||||
}
|
||||
if (!called) {
|
||||
called = true;
|
||||
const stream = this;
|
||||
resources.map(function (resource) {
|
||||
retrieveResource(language, resource, apiHostname, credentials).then((file) => {
|
||||
if (file) {
|
||||
stream.emit('data', file);
|
||||
}
|
||||
translationsRetrieved++;
|
||||
}).catch(error => { throw new Error(error); });
|
||||
});
|
||||
}
|
||||
callback();
|
||||
});
|
||||
}
|
||||
const limiter = new Limiter(NUMBER_OF_CONCURRENT_DOWNLOADS);
|
||||
function retrieveResource(language, resource, apiHostname, credentials) {
|
||||
return limiter.queue(() => new Promise((resolve, reject) => {
|
||||
const slug = resource.name.replace(/\//g, '_');
|
||||
const project = resource.project;
|
||||
const transifexLanguageId = language.id === 'ps' ? 'en' : language.translationId || language.id;
|
||||
const options = {
|
||||
hostname: apiHostname,
|
||||
path: `/api/2/project/${project}/resource/${slug}/translation/${transifexLanguageId}?file&mode=onlyreviewed`,
|
||||
auth: credentials,
|
||||
port: 443,
|
||||
method: 'GET'
|
||||
};
|
||||
console.log('[transifex] Fetching ' + options.path);
|
||||
const request = https.request(options, (res) => {
|
||||
const xlfBuffer = [];
|
||||
res.on('data', (chunk) => xlfBuffer.push(chunk));
|
||||
res.on('end', () => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(new File({ contents: Buffer.concat(xlfBuffer), path: `${project}/${slug}.xlf` }));
|
||||
}
|
||||
else if (res.statusCode === 404) {
|
||||
console.log(`[transifex] ${slug} in ${project} returned no data.`);
|
||||
resolve(null);
|
||||
}
|
||||
else {
|
||||
reject(`${slug} in ${project} returned no data. Response code: ${res.statusCode}.`);
|
||||
}
|
||||
});
|
||||
});
|
||||
request.on('error', (err) => {
|
||||
reject(`Failed to query resource ${slug} with the following error: ${err}. ${options.path}`);
|
||||
});
|
||||
request.end();
|
||||
}));
|
||||
}
|
||||
function prepareI18nFiles() {
|
||||
const parsePromises = [];
|
||||
return (0, event_stream_1.through)(function (xlf) {
|
||||
const stream = this;
|
||||
const parsePromise = XLF.parse(xlf.contents.toString());
|
||||
parsePromises.push(parsePromise);
|
||||
parsePromise.then(resolvedFiles => {
|
||||
resolvedFiles.forEach(file => {
|
||||
const translatedFile = createI18nFile(file.originalFilePath, file.messages);
|
||||
stream.queue(translatedFile);
|
||||
});
|
||||
});
|
||||
}, function () {
|
||||
Promise.all(parsePromises)
|
||||
.then(() => { this.queue(null); })
|
||||
.catch(reason => { throw new Error(reason); });
|
||||
});
|
||||
}
|
||||
exports.prepareI18nFiles = prepareI18nFiles;
|
||||
function createI18nFile(originalFilePath, messages) {
|
||||
function createI18nFile(name, messages) {
|
||||
const result = Object.create(null);
|
||||
result[''] = [
|
||||
'--------------------------------------------------------------------------------------------',
|
||||
|
@ -1021,12 +651,20 @@ function createI18nFile(originalFilePath, messages) {
|
|||
content = content.replace(/\n/g, '\r\n');
|
||||
}
|
||||
return new File({
|
||||
path: path.join(originalFilePath + '.i18n.json'),
|
||||
path: path.join(name + '.i18n.json'),
|
||||
contents: Buffer.from(content, 'utf8')
|
||||
});
|
||||
}
|
||||
const i18nPackVersion = '1.0.0';
|
||||
function prepareI18nPackFiles(externalExtensions, resultingTranslationPaths, pseudo = false) {
|
||||
function getRecordFromL10nJsonFormat(l10nJsonFormat) {
|
||||
const record = {};
|
||||
for (const key of Object.keys(l10nJsonFormat)) {
|
||||
const value = l10nJsonFormat[key];
|
||||
record[key] = typeof value === 'string' ? value : value.message;
|
||||
}
|
||||
return record;
|
||||
}
|
||||
function prepareI18nPackFiles(externalExtensions, resultingTranslationPaths) {
|
||||
const parsePromises = [];
|
||||
const mainPack = { version: i18nPackVersion, contents: {} };
|
||||
const extensionsPacks = {};
|
||||
|
@ -1036,11 +674,11 @@ function prepareI18nPackFiles(externalExtensions, resultingTranslationPaths, pse
|
|||
const resource = path.basename(xlf.relative, '.xlf');
|
||||
const contents = xlf.contents.toString();
|
||||
log(`Found ${project}: ${resource}`);
|
||||
const parsePromise = pseudo ? XLF.parsePseudo(contents) : XLF.parse(contents);
|
||||
const parsePromise = (0, l10n_dev_1.getL10nFilesFromXlf)(contents);
|
||||
parsePromises.push(parsePromise);
|
||||
parsePromise.then(resolvedFiles => {
|
||||
resolvedFiles.forEach(file => {
|
||||
const path = file.originalFilePath;
|
||||
const path = file.name;
|
||||
const firstSlash = path.indexOf('/');
|
||||
if (project === extensionsProject) {
|
||||
let extPack = extensionsPacks[resource];
|
||||
|
@ -1050,14 +688,14 @@ function prepareI18nPackFiles(externalExtensions, resultingTranslationPaths, pse
|
|||
const externalId = externalExtensions[resource];
|
||||
if (!externalId) { // internal extension: remove 'extensions/extensionId/' segnent
|
||||
const secondSlash = path.indexOf('/', firstSlash + 1);
|
||||
extPack.contents[path.substr(secondSlash + 1)] = file.messages;
|
||||
extPack.contents[path.substring(secondSlash + 1)] = getRecordFromL10nJsonFormat(file.messages);
|
||||
}
|
||||
else {
|
||||
extPack.contents[path] = file.messages;
|
||||
extPack.contents[path] = getRecordFromL10nJsonFormat(file.messages);
|
||||
}
|
||||
}
|
||||
else {
|
||||
mainPack.contents[path.substr(firstSlash + 1)] = file.messages;
|
||||
mainPack.contents[path.substring(firstSlash + 1)] = getRecordFromL10nJsonFormat(file.messages);
|
||||
}
|
||||
});
|
||||
}).catch(reason => {
|
||||
|
@ -1099,7 +737,7 @@ function prepareIslFiles(language, innoSetupConfig) {
|
|||
parsePromises.push(parsePromise);
|
||||
parsePromise.then(resolvedFiles => {
|
||||
resolvedFiles.forEach(file => {
|
||||
const translatedFile = createIslFile(file.originalFilePath, file.messages, language, innoSetupConfig);
|
||||
const translatedFile = createIslFile(file.name, file.messages, language, innoSetupConfig);
|
||||
stream.queue(translatedFile);
|
||||
});
|
||||
}).catch(reason => {
|
||||
|
@ -1114,14 +752,14 @@ function prepareIslFiles(language, innoSetupConfig) {
|
|||
});
|
||||
}
|
||||
exports.prepareIslFiles = prepareIslFiles;
|
||||
function createIslFile(originalFilePath, messages, language, innoSetup) {
|
||||
function createIslFile(name, messages, language, innoSetup) {
|
||||
const content = [];
|
||||
let originalContent;
|
||||
if (path.basename(originalFilePath) === 'Default') {
|
||||
originalContent = new TextModel(fs.readFileSync(originalFilePath + '.isl', 'utf8'));
|
||||
if (path.basename(name) === 'Default') {
|
||||
originalContent = new TextModel(fs.readFileSync(name + '.isl', 'utf8'));
|
||||
}
|
||||
else {
|
||||
originalContent = new TextModel(fs.readFileSync(originalFilePath + '.en.isl', 'utf8'));
|
||||
originalContent = new TextModel(fs.readFileSync(name + '.en.isl', 'utf8'));
|
||||
}
|
||||
originalContent.lines.forEach(line => {
|
||||
if (line.length > 0) {
|
||||
|
@ -1143,7 +781,7 @@ function createIslFile(originalFilePath, messages, language, innoSetup) {
|
|||
}
|
||||
}
|
||||
});
|
||||
const basename = path.basename(originalFilePath);
|
||||
const basename = path.basename(name);
|
||||
const filePath = `${basename}.${language.id}.isl`;
|
||||
const encoded = iconv.encode(Buffer.from(content.join('\r\n'), 'utf8').toString(), innoSetup.codePage);
|
||||
return new File({
|
||||
|
@ -1174,6 +812,3 @@ function encodeEntities(value) {
|
|||
function decodeEntities(value) {
|
||||
return value.replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&');
|
||||
}
|
||||
function pseudify(message) {
|
||||
return '\uFF3B' + message.replace(/[aouei]/g, '$&$&') + '\uFF3D';
|
||||
}
|
||||
|
|
|
@ -6,17 +6,15 @@
|
|||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
|
||||
import { through, readable, ThroughStream } from 'event-stream';
|
||||
import { through, ThroughStream } from 'event-stream';
|
||||
import * as File from 'vinyl';
|
||||
import * as Is from 'is';
|
||||
import * as xml2js from 'xml2js';
|
||||
import * as https from 'https';
|
||||
import * as gulp from 'gulp';
|
||||
import * as fancyLog from 'fancy-log';
|
||||
import * as ansiColors from 'ansi-colors';
|
||||
import * as iconv from '@vscode/iconv-lite-umd';
|
||||
|
||||
const NUMBER_OF_CONCURRENT_DOWNLOADS = 4;
|
||||
import { l10nJsonFormat, getL10nXlf, l10nJsonDetails, getL10nFilesFromXlf } from '@vscode/l10n-dev';
|
||||
|
||||
function log(message: any, ...rest: any[]): void {
|
||||
fancyLog(ansiColors.green('[i18n]'), message, ...rest);
|
||||
|
@ -58,11 +56,6 @@ export const externalExtensionsWithTranslations = {
|
|||
'vscode-node-debug2': 'ms-vscode.node-debug2'
|
||||
};
|
||||
|
||||
|
||||
interface Map<V> {
|
||||
[key: string]: V;
|
||||
}
|
||||
|
||||
interface Item {
|
||||
id: string;
|
||||
message: string;
|
||||
|
@ -74,12 +67,6 @@ export interface Resource {
|
|||
project: string;
|
||||
}
|
||||
|
||||
interface ParsedXLF {
|
||||
messages: Map<string>;
|
||||
originalFilePath: string;
|
||||
language: string;
|
||||
}
|
||||
|
||||
interface LocalizeInfo {
|
||||
key: string;
|
||||
comment: string[];
|
||||
|
@ -93,9 +80,9 @@ module LocalizeInfo {
|
|||
}
|
||||
|
||||
interface BundledFormat {
|
||||
keys: Map<(string | LocalizeInfo)[]>;
|
||||
messages: Map<string[]>;
|
||||
bundles: Map<string[]>;
|
||||
keys: Record<string, (string | LocalizeInfo)[]>;
|
||||
messages: Record<string, string[]>;
|
||||
bundles: Record<string, string[]>;
|
||||
}
|
||||
|
||||
module BundledFormat {
|
||||
|
@ -111,27 +98,6 @@ module BundledFormat {
|
|||
}
|
||||
}
|
||||
|
||||
interface ValueFormat {
|
||||
message: string;
|
||||
comment: string[];
|
||||
}
|
||||
|
||||
interface PackageJsonFormat {
|
||||
[key: string]: string | ValueFormat;
|
||||
}
|
||||
|
||||
module PackageJsonFormat {
|
||||
export function is(value: any): value is PackageJsonFormat {
|
||||
if (Is.undef(value) || !Is.object(value)) {
|
||||
return false;
|
||||
}
|
||||
return Object.keys(value).every(key => {
|
||||
const element = value[key];
|
||||
return Is.string(element) || (Is.object(element) && Is.defined(element.message) && Is.defined(element.comment));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
interface BundledExtensionFormat {
|
||||
[key: string]: {
|
||||
messages: string[];
|
||||
|
@ -181,7 +147,7 @@ class TextModel {
|
|||
|
||||
export class XLF {
|
||||
private buffer: string[];
|
||||
private files: Map<Item[]>;
|
||||
private files: Record<string, Item[]>;
|
||||
public numberOfMessages: number;
|
||||
|
||||
constructor(public project: string) {
|
||||
|
@ -274,37 +240,11 @@ export class XLF {
|
|||
this.buffer.push(line.toString());
|
||||
}
|
||||
|
||||
static parsePseudo = function (xlfString: string): Promise<ParsedXLF[]> {
|
||||
return new Promise((resolve) => {
|
||||
const parser = new xml2js.Parser();
|
||||
const files: { messages: Map<string>; originalFilePath: string; language: string }[] = [];
|
||||
parser.parseString(xlfString, function (_err: any, result: any) {
|
||||
const fileNodes: any[] = result['xliff']['file'];
|
||||
fileNodes.forEach(file => {
|
||||
const originalFilePath = file.$.original;
|
||||
const messages: Map<string> = {};
|
||||
const transUnits = file.body[0]['trans-unit'];
|
||||
if (transUnits) {
|
||||
transUnits.forEach((unit: any) => {
|
||||
const key = unit.$.id;
|
||||
const val = pseudify(unit.source[0]['_'].toString());
|
||||
if (key && val) {
|
||||
messages[key] = decodeEntities(val);
|
||||
}
|
||||
});
|
||||
files.push({ messages: messages, originalFilePath: originalFilePath, language: 'ps' });
|
||||
}
|
||||
});
|
||||
resolve(files);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
static parse = function (xlfString: string): Promise<ParsedXLF[]> {
|
||||
static parse = function (xlfString: string): Promise<l10nJsonDetails[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const parser = new xml2js.Parser();
|
||||
|
||||
const files: { messages: Map<string>; originalFilePath: string; language: string }[] = [];
|
||||
const files: { messages: Record<string, string>; name: string; language: string }[] = [];
|
||||
|
||||
parser.parseString(xlfString, function (err: any, result: any) {
|
||||
if (err) {
|
||||
|
@ -317,15 +257,15 @@ export class XLF {
|
|||
}
|
||||
|
||||
fileNodes.forEach((file) => {
|
||||
const originalFilePath = file.$.original;
|
||||
if (!originalFilePath) {
|
||||
const name = file.$.original;
|
||||
if (!name) {
|
||||
reject(new Error(`XLF parsing error: XLIFF file node does not contain original attribute to determine the original location of the resource file.`));
|
||||
}
|
||||
const language = file.$['target-language'];
|
||||
if (!language) {
|
||||
reject(new Error(`XLF parsing error: XLIFF file node does not contain target-language attribute to determine translated language.`));
|
||||
}
|
||||
const messages: Map<string> = {};
|
||||
const messages: Record<string, string> = {};
|
||||
|
||||
const transUnits = file.body[0]['trans-unit'];
|
||||
if (transUnits) {
|
||||
|
@ -341,12 +281,12 @@ export class XLF {
|
|||
val = val._ ? val._ : '';
|
||||
}
|
||||
if (!key) {
|
||||
reject(new Error(`XLF parsing error: trans-unit ${JSON.stringify(unit, undefined, 0)} defined in file ${originalFilePath} is missing the ID attribute.`));
|
||||
reject(new Error(`XLF parsing error: trans-unit ${JSON.stringify(unit, undefined, 0)} defined in file ${name} is missing the ID attribute.`));
|
||||
return;
|
||||
}
|
||||
messages[key] = decodeEntities(val);
|
||||
});
|
||||
files.push({ messages: messages, originalFilePath: originalFilePath, language: language.toLowerCase() });
|
||||
files.push({ messages, name, language: language.toLowerCase() });
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -356,49 +296,6 @@ export class XLF {
|
|||
};
|
||||
}
|
||||
|
||||
export interface ITask<T> {
|
||||
(): T;
|
||||
}
|
||||
|
||||
interface ILimitedTaskFactory<T> {
|
||||
factory: ITask<Promise<T>>;
|
||||
c: (value?: T | Promise<T>) => void;
|
||||
e: (error?: any) => void;
|
||||
}
|
||||
|
||||
export class Limiter<T> {
|
||||
private runningPromises: number;
|
||||
private outstandingPromises: ILimitedTaskFactory<any>[];
|
||||
|
||||
constructor(private maxDegreeOfParalellism: number) {
|
||||
this.outstandingPromises = [];
|
||||
this.runningPromises = 0;
|
||||
}
|
||||
|
||||
queue(factory: ITask<Promise<T>>): Promise<T> {
|
||||
return new Promise<T>((c, e) => {
|
||||
this.outstandingPromises.push({ factory, c, e });
|
||||
this.consume();
|
||||
});
|
||||
}
|
||||
|
||||
private consume(): void {
|
||||
while (this.outstandingPromises.length && this.runningPromises < this.maxDegreeOfParalellism) {
|
||||
const iLimitedTask = this.outstandingPromises.shift()!;
|
||||
this.runningPromises++;
|
||||
|
||||
const promise = iLimitedTask.factory();
|
||||
promise.then(iLimitedTask.c).catch(iLimitedTask.e);
|
||||
promise.then(() => this.consumed()).catch(() => this.consumed());
|
||||
}
|
||||
}
|
||||
|
||||
private consumed(): void {
|
||||
this.runningPromises--;
|
||||
this.consume();
|
||||
}
|
||||
}
|
||||
|
||||
function sortLanguages(languages: Language[]): Language[] {
|
||||
return languages.sort((a: Language, b: Language): number => {
|
||||
return a.id < b.id ? -1 : (a.id > b.id ? 1 : 0);
|
||||
|
@ -480,9 +377,9 @@ function processCoreBundleFormat(fileHeader: string, languages: Language[], json
|
|||
const messageSection = json.messages;
|
||||
const bundleSection = json.bundles;
|
||||
|
||||
const statistics: Map<number> = Object.create(null);
|
||||
const statistics: Record<string, number> = Object.create(null);
|
||||
|
||||
const defaultMessages: Map<Map<string>> = Object.create(null);
|
||||
const defaultMessages: Record<string, Record<string, string>> = Object.create(null);
|
||||
const modules = Object.keys(keysSection);
|
||||
modules.forEach((module) => {
|
||||
const keys = keysSection[module];
|
||||
|
@ -491,7 +388,7 @@ function processCoreBundleFormat(fileHeader: string, languages: Language[], json
|
|||
emitter.emit('error', `Message for module ${module} corrupted. Mismatch in number of keys and messages.`);
|
||||
return;
|
||||
}
|
||||
const messageMap: Map<string> = Object.create(null);
|
||||
const messageMap: Record<string, string> = Object.create(null);
|
||||
defaultMessages[module] = messageMap;
|
||||
keys.map((key, i) => {
|
||||
if (typeof key === 'string') {
|
||||
|
@ -514,7 +411,7 @@ function processCoreBundleFormat(fileHeader: string, languages: Language[], json
|
|||
}
|
||||
|
||||
statistics[language.id] = 0;
|
||||
const localizedModules: Map<string[]> = Object.create(null);
|
||||
const localizedModules: Record<string, string[]> = Object.create(null);
|
||||
const languageFolderName = language.translationId || language.id;
|
||||
const i18nFile = path.join(languageDirectory, `vscode-language-pack-${languageFolderName}`, 'translations', 'main.i18n.json');
|
||||
let allMessages: I18nFormat | undefined;
|
||||
|
@ -648,7 +545,7 @@ export function createXlfFilesForCoreBundle(): ThroughStream {
|
|||
const basename = path.basename(file.path);
|
||||
if (basename === 'nls.metadata.json') {
|
||||
if (file.isBuffer()) {
|
||||
const xlfs: Map<XLF> = Object.create(null);
|
||||
const xlfs: Record<string, XLF> = Object.create(null);
|
||||
const json: BundledFormat = JSON.parse((file.contents as Buffer).toString('utf8'));
|
||||
for (const coreModule in json.keys) {
|
||||
const projectResource = getResource(coreModule);
|
||||
|
@ -704,44 +601,35 @@ export function createXlfFilesForExtensions(): ThroughStream {
|
|||
return;
|
||||
}
|
||||
counter++;
|
||||
let _xlf: XLF;
|
||||
function getXlf() {
|
||||
if (!_xlf) {
|
||||
_xlf = new XLF(extensionsProject);
|
||||
let _l10nMap: Map<string, l10nJsonFormat>;
|
||||
function getL10nMap() {
|
||||
if (!_l10nMap) {
|
||||
_l10nMap = new Map();
|
||||
}
|
||||
return _xlf;
|
||||
return _l10nMap;
|
||||
}
|
||||
gulp.src([`.build/extensions/${extensionName}/package.nls.json`, `.build/extensions/${extensionName}/**/nls.metadata.json`], { allowEmpty: true }).pipe(through(function (file: File) {
|
||||
if (file.isBuffer()) {
|
||||
const buffer: Buffer = file.contents as Buffer;
|
||||
const basename = path.basename(file.path);
|
||||
if (basename === 'package.nls.json') {
|
||||
const json: PackageJsonFormat = JSON.parse(buffer.toString('utf8'));
|
||||
const keys: Array<string | LocalizeInfo> = [];
|
||||
const messages: string[] = [];
|
||||
Object.keys(json).forEach((key) => {
|
||||
const value = json[key];
|
||||
if (Is.string(value)) {
|
||||
keys.push(key);
|
||||
messages.push(value);
|
||||
} else if (value) {
|
||||
keys.push({
|
||||
key,
|
||||
comment: value.comment
|
||||
});
|
||||
messages.push(value.message);
|
||||
} else {
|
||||
keys.push(key);
|
||||
messages.push(`Unknown message for key: ${key}`);
|
||||
}
|
||||
});
|
||||
getXlf().addFile(`extensions/${extensionName}/package`, keys, messages);
|
||||
const json: l10nJsonFormat = JSON.parse(buffer.toString('utf8'));
|
||||
getL10nMap().set(`extensions/${extensionName}/package`, json);
|
||||
} else if (basename === 'nls.metadata.json') {
|
||||
const json: BundledExtensionFormat = JSON.parse(buffer.toString('utf8'));
|
||||
const relPath = path.relative(`.build/extensions/${extensionName}`, path.dirname(file.path));
|
||||
for (const file in json) {
|
||||
const fileContent = json[file];
|
||||
getXlf().addFile(`extensions/${extensionName}/${relPath}/${file}`, fileContent.keys, fileContent.messages);
|
||||
const info: l10nJsonFormat = Object.create(null);
|
||||
for (let i = 0; i < fileContent.messages.length; i++) {
|
||||
const message = fileContent.messages[i];
|
||||
const { key, comment } = LocalizeInfo.is(fileContent.keys[i])
|
||||
? fileContent.keys[i] as LocalizeInfo
|
||||
: { key: fileContent.keys[i] as string, comment: undefined };
|
||||
|
||||
info[key] = comment ? { message, comment } : message;
|
||||
}
|
||||
getL10nMap().set(`extensions/${extensionName}/${relPath}/${file}`, info);
|
||||
}
|
||||
} else {
|
||||
this.emit('error', new Error(`${file.path} is not a valid extension nls file`));
|
||||
|
@ -749,10 +637,10 @@ export function createXlfFilesForExtensions(): ThroughStream {
|
|||
}
|
||||
}
|
||||
}, function () {
|
||||
if (_xlf) {
|
||||
if (_l10nMap) {
|
||||
const xlfFile = new File({
|
||||
path: path.join(extensionsProject, extensionName + '.xlf'),
|
||||
contents: Buffer.from(_xlf.toString(), 'utf8')
|
||||
contents: Buffer.from(getL10nXlf(_l10nMap), 'utf8')
|
||||
});
|
||||
folderStream.queue(xlfFile);
|
||||
}
|
||||
|
@ -828,321 +716,7 @@ export function createXlfFilesForIsl(): ThroughStream {
|
|||
});
|
||||
}
|
||||
|
||||
export function pushXlfFiles(apiHostname: string, username: string, password: string): ThroughStream {
|
||||
const tryGetPromises: Array<Promise<boolean>> = [];
|
||||
const updateCreatePromises: Array<Promise<boolean>> = [];
|
||||
|
||||
return through(function (this: ThroughStream, file: File) {
|
||||
const project = path.dirname(file.relative);
|
||||
const fileName = path.basename(file.path);
|
||||
const slug = fileName.substr(0, fileName.length - '.xlf'.length);
|
||||
const credentials = `${username}:${password}`;
|
||||
|
||||
// Check if resource already exists, if not, then create it.
|
||||
let promise = tryGetResource(project, slug, apiHostname, credentials);
|
||||
tryGetPromises.push(promise);
|
||||
promise.then(exists => {
|
||||
if (exists) {
|
||||
promise = updateResource(project, slug, file, apiHostname, credentials);
|
||||
} else {
|
||||
promise = createResource(project, slug, file, apiHostname, credentials);
|
||||
}
|
||||
updateCreatePromises.push(promise);
|
||||
});
|
||||
|
||||
}, function () {
|
||||
// End the pipe only after all the communication with Transifex API happened
|
||||
Promise.all(tryGetPromises).then(() => {
|
||||
Promise.all(updateCreatePromises).then(() => {
|
||||
this.queue(null);
|
||||
}).catch((reason) => { throw new Error(reason); });
|
||||
}).catch((reason) => { throw new Error(reason); });
|
||||
});
|
||||
}
|
||||
|
||||
function getAllResources(project: string, apiHostname: string, username: string, password: string): Promise<string[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const credentials = `${username}:${password}`;
|
||||
const options = {
|
||||
hostname: apiHostname,
|
||||
path: `/api/2/project/${project}/resources`,
|
||||
auth: credentials,
|
||||
method: 'GET'
|
||||
};
|
||||
|
||||
const request = https.request(options, (res) => {
|
||||
const buffer: Buffer[] = [];
|
||||
res.on('data', (chunk: Buffer) => buffer.push(chunk));
|
||||
res.on('end', () => {
|
||||
if (res.statusCode === 200) {
|
||||
const json = JSON.parse(Buffer.concat(buffer).toString());
|
||||
if (Array.isArray(json)) {
|
||||
resolve(json.map(o => o.slug));
|
||||
return;
|
||||
}
|
||||
reject(`Unexpected data format. Response code: ${res.statusCode}.`);
|
||||
} else {
|
||||
reject(`No resources in ${project} returned no data. Response code: ${res.statusCode}.`);
|
||||
}
|
||||
});
|
||||
});
|
||||
request.on('error', (err) => {
|
||||
reject(`Failed to query resources in ${project} with the following error: ${err}. ${options.path}`);
|
||||
});
|
||||
request.end();
|
||||
});
|
||||
}
|
||||
|
||||
export function findObsoleteResources(apiHostname: string, username: string, password: string): ThroughStream {
|
||||
const resourcesByProject: Map<string[]> = Object.create(null);
|
||||
resourcesByProject[extensionsProject] = ([] as any[]).concat(externalExtensionsWithTranslations); // clone
|
||||
|
||||
return through(function (this: ThroughStream, file: File) {
|
||||
const project = path.dirname(file.relative);
|
||||
const fileName = path.basename(file.path);
|
||||
const slug = fileName.substr(0, fileName.length - '.xlf'.length);
|
||||
|
||||
let slugs = resourcesByProject[project];
|
||||
if (!slugs) {
|
||||
resourcesByProject[project] = slugs = [];
|
||||
}
|
||||
slugs.push(slug);
|
||||
this.push(file);
|
||||
}, function () {
|
||||
|
||||
const json = JSON.parse(fs.readFileSync('./build/lib/i18n.resources.json', 'utf8'));
|
||||
const i18Resources = [...json.editor, ...json.workbench].map((r: Resource) => r.project + '/' + r.name.replace(/\//g, '_'));
|
||||
const extractedResources: string[] = [];
|
||||
for (const project of [workbenchProject, editorProject]) {
|
||||
for (const resource of resourcesByProject[project]) {
|
||||
if (resource !== 'setup_messages') {
|
||||
extractedResources.push(project + '/' + resource);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (i18Resources.length !== extractedResources.length) {
|
||||
console.log(`[i18n] Obsolete resources in file 'build/lib/i18n.resources.json': JSON.stringify(${i18Resources.filter(p => extractedResources.indexOf(p) === -1)})`);
|
||||
console.log(`[i18n] Missing resources in file 'build/lib/i18n.resources.json': JSON.stringify(${extractedResources.filter(p => i18Resources.indexOf(p) === -1)})`);
|
||||
}
|
||||
|
||||
const promises: Array<Promise<void>> = [];
|
||||
for (const project in resourcesByProject) {
|
||||
promises.push(
|
||||
getAllResources(project, apiHostname, username, password).then(resources => {
|
||||
const expectedResources = resourcesByProject[project];
|
||||
const unusedResources = resources.filter(resource => resource && expectedResources.indexOf(resource) === -1);
|
||||
if (unusedResources.length) {
|
||||
console.log(`[transifex] Obsolete resources in project '${project}': ${unusedResources.join(', ')}`);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
return Promise.all(promises).then(_ => {
|
||||
this.push(null);
|
||||
}).catch((reason) => { throw new Error(reason); });
|
||||
});
|
||||
}
|
||||
|
||||
function tryGetResource(project: string, slug: string, apiHostname: string, credentials: string): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const options = {
|
||||
hostname: apiHostname,
|
||||
path: `/api/2/project/${project}/resource/${slug}/?details`,
|
||||
auth: credentials,
|
||||
method: 'GET'
|
||||
};
|
||||
|
||||
const request = https.request(options, (response) => {
|
||||
if (response.statusCode === 404) {
|
||||
resolve(false);
|
||||
} else if (response.statusCode === 200) {
|
||||
resolve(true);
|
||||
} else {
|
||||
reject(`Failed to query resource ${project}/${slug}. Response: ${response.statusCode} ${response.statusMessage}`);
|
||||
}
|
||||
});
|
||||
request.on('error', (err) => {
|
||||
reject(`Failed to get ${project}/${slug} on Transifex: ${err}`);
|
||||
});
|
||||
|
||||
request.end();
|
||||
});
|
||||
}
|
||||
|
||||
function createResource(project: string, slug: string, xlfFile: File, apiHostname: string, credentials: any): Promise<any> {
|
||||
return new Promise((_resolve, reject) => {
|
||||
const data = JSON.stringify({
|
||||
'content': xlfFile.contents.toString(),
|
||||
'name': slug,
|
||||
'slug': slug,
|
||||
'i18n_type': 'XLIFF'
|
||||
});
|
||||
const options = {
|
||||
hostname: apiHostname,
|
||||
path: `/api/2/project/${project}/resources`,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(data)
|
||||
},
|
||||
auth: credentials,
|
||||
method: 'POST'
|
||||
};
|
||||
|
||||
const request = https.request(options, (res) => {
|
||||
if (res.statusCode === 201) {
|
||||
log(`Resource ${project}/${slug} successfully created on Transifex.`);
|
||||
} else {
|
||||
reject(`Something went wrong in the request creating ${slug} in ${project}. ${res.statusCode}`);
|
||||
}
|
||||
});
|
||||
request.on('error', (err) => {
|
||||
reject(`Failed to create ${project}/${slug} on Transifex: ${err}`);
|
||||
});
|
||||
|
||||
request.write(data);
|
||||
request.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The following link provides information about how Transifex handles updates of a resource file:
|
||||
* https://dev.befoolish.co/tx-docs/public/projects/updating-content#what-happens-when-you-update-files
|
||||
*/
|
||||
function updateResource(project: string, slug: string, xlfFile: File, apiHostname: string, credentials: string): Promise<any> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const data = JSON.stringify({ content: xlfFile.contents.toString() });
|
||||
const options = {
|
||||
hostname: apiHostname,
|
||||
path: `/api/2/project/${project}/resource/${slug}/content`,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(data)
|
||||
},
|
||||
auth: credentials,
|
||||
method: 'PUT'
|
||||
};
|
||||
|
||||
const request = https.request(options, (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
res.setEncoding('utf8');
|
||||
|
||||
let responseBuffer: string = '';
|
||||
res.on('data', function (chunk) {
|
||||
responseBuffer += chunk;
|
||||
});
|
||||
res.on('end', () => {
|
||||
const response = JSON.parse(responseBuffer);
|
||||
log(`Resource ${project}/${slug} successfully updated on Transifex. Strings added: ${response.strings_added}, updated: ${response.strings_added}, deleted: ${response.strings_added}`);
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
reject(`Something went wrong in the request updating ${slug} in ${project}. ${res.statusCode}`);
|
||||
}
|
||||
});
|
||||
request.on('error', (err) => {
|
||||
reject(`Failed to update ${project}/${slug} on Transifex: ${err}`);
|
||||
});
|
||||
|
||||
request.write(data);
|
||||
request.end();
|
||||
});
|
||||
}
|
||||
|
||||
export function pullSetupXlfFiles(apiHostname: string, username: string, password: string, language: Language, includeDefault: boolean): NodeJS.ReadableStream {
|
||||
const setupResources = [{ name: 'setup_messages', project: workbenchProject }];
|
||||
if (includeDefault) {
|
||||
setupResources.push({ name: 'setup_default', project: setupProject });
|
||||
}
|
||||
return pullXlfFiles(apiHostname, username, password, language, setupResources);
|
||||
}
|
||||
|
||||
function pullXlfFiles(apiHostname: string, username: string, password: string, language: Language, resources: Resource[]): NodeJS.ReadableStream {
|
||||
const credentials = `${username}:${password}`;
|
||||
const expectedTranslationsCount = resources.length;
|
||||
let translationsRetrieved = 0, called = false;
|
||||
|
||||
return readable(function (_count: any, callback: any) {
|
||||
// Mark end of stream when all resources were retrieved
|
||||
if (translationsRetrieved === expectedTranslationsCount) {
|
||||
return this.emit('end');
|
||||
}
|
||||
|
||||
if (!called) {
|
||||
called = true;
|
||||
const stream = this;
|
||||
resources.map(function (resource) {
|
||||
retrieveResource(language, resource, apiHostname, credentials).then((file: File | null) => {
|
||||
if (file) {
|
||||
stream.emit('data', file);
|
||||
}
|
||||
translationsRetrieved++;
|
||||
}).catch(error => { throw new Error(error); });
|
||||
});
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
const limiter = new Limiter<File | null>(NUMBER_OF_CONCURRENT_DOWNLOADS);
|
||||
|
||||
function retrieveResource(language: Language, resource: Resource, apiHostname: string, credentials: string): Promise<File | null> {
|
||||
return limiter.queue(() => new Promise<File | null>((resolve, reject) => {
|
||||
const slug = resource.name.replace(/\//g, '_');
|
||||
const project = resource.project;
|
||||
const transifexLanguageId = language.id === 'ps' ? 'en' : language.translationId || language.id;
|
||||
const options = {
|
||||
hostname: apiHostname,
|
||||
path: `/api/2/project/${project}/resource/${slug}/translation/${transifexLanguageId}?file&mode=onlyreviewed`,
|
||||
auth: credentials,
|
||||
port: 443,
|
||||
method: 'GET'
|
||||
};
|
||||
console.log('[transifex] Fetching ' + options.path);
|
||||
|
||||
const request = https.request(options, (res) => {
|
||||
const xlfBuffer: Buffer[] = [];
|
||||
res.on('data', (chunk: Buffer) => xlfBuffer.push(chunk));
|
||||
res.on('end', () => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(new File({ contents: Buffer.concat(xlfBuffer), path: `${project}/${slug}.xlf` }));
|
||||
} else if (res.statusCode === 404) {
|
||||
console.log(`[transifex] ${slug} in ${project} returned no data.`);
|
||||
resolve(null);
|
||||
} else {
|
||||
reject(`${slug} in ${project} returned no data. Response code: ${res.statusCode}.`);
|
||||
}
|
||||
});
|
||||
});
|
||||
request.on('error', (err) => {
|
||||
reject(`Failed to query resource ${slug} with the following error: ${err}. ${options.path}`);
|
||||
});
|
||||
request.end();
|
||||
}));
|
||||
}
|
||||
|
||||
export function prepareI18nFiles(): ThroughStream {
|
||||
const parsePromises: Promise<ParsedXLF[]>[] = [];
|
||||
|
||||
return through(function (this: ThroughStream, xlf: File) {
|
||||
const stream = this;
|
||||
const parsePromise = XLF.parse(xlf.contents.toString());
|
||||
parsePromises.push(parsePromise);
|
||||
parsePromise.then(
|
||||
resolvedFiles => {
|
||||
resolvedFiles.forEach(file => {
|
||||
const translatedFile = createI18nFile(file.originalFilePath, file.messages);
|
||||
stream.queue(translatedFile);
|
||||
});
|
||||
}
|
||||
);
|
||||
}, function () {
|
||||
Promise.all(parsePromises)
|
||||
.then(() => { this.queue(null); })
|
||||
.catch(reason => { throw new Error(reason); });
|
||||
});
|
||||
}
|
||||
|
||||
function createI18nFile(originalFilePath: string, messages: any): File {
|
||||
function createI18nFile(name: string, messages: any): File {
|
||||
const result = Object.create(null);
|
||||
result[''] = [
|
||||
'--------------------------------------------------------------------------------------------',
|
||||
|
@ -1160,7 +734,7 @@ function createI18nFile(originalFilePath: string, messages: any): File {
|
|||
content = content.replace(/\n/g, '\r\n');
|
||||
}
|
||||
return new File({
|
||||
path: path.join(originalFilePath + '.i18n.json'),
|
||||
path: path.join(name + '.i18n.json'),
|
||||
contents: Buffer.from(content, 'utf8')
|
||||
});
|
||||
}
|
||||
|
@ -1168,7 +742,7 @@ function createI18nFile(originalFilePath: string, messages: any): File {
|
|||
interface I18nPack {
|
||||
version: string;
|
||||
contents: {
|
||||
[path: string]: Map<string>;
|
||||
[path: string]: Record<string, string>;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1179,22 +753,31 @@ export interface TranslationPath {
|
|||
resourceName: string;
|
||||
}
|
||||
|
||||
export function prepareI18nPackFiles(externalExtensions: Map<string>, resultingTranslationPaths: TranslationPath[], pseudo = false): NodeJS.ReadWriteStream {
|
||||
const parsePromises: Promise<ParsedXLF[]>[] = [];
|
||||
function getRecordFromL10nJsonFormat(l10nJsonFormat: l10nJsonFormat): Record<string, string> {
|
||||
const record: Record<string, string> = {};
|
||||
for (const key of Object.keys(l10nJsonFormat)) {
|
||||
const value = l10nJsonFormat[key];
|
||||
record[key] = typeof value === 'string' ? value : value.message;
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
export function prepareI18nPackFiles(externalExtensions: Record<string, string>, resultingTranslationPaths: TranslationPath[]): NodeJS.ReadWriteStream {
|
||||
const parsePromises: Promise<l10nJsonDetails[]>[] = [];
|
||||
const mainPack: I18nPack = { version: i18nPackVersion, contents: {} };
|
||||
const extensionsPacks: Map<I18nPack> = {};
|
||||
const extensionsPacks: Record<string, I18nPack> = {};
|
||||
const errors: any[] = [];
|
||||
return through(function (this: ThroughStream, xlf: File) {
|
||||
const project = path.basename(path.dirname(path.dirname(xlf.relative)));
|
||||
const resource = path.basename(xlf.relative, '.xlf');
|
||||
const contents = xlf.contents.toString();
|
||||
log(`Found ${project}: ${resource}`);
|
||||
const parsePromise = pseudo ? XLF.parsePseudo(contents) : XLF.parse(contents);
|
||||
const parsePromise = getL10nFilesFromXlf(contents);
|
||||
parsePromises.push(parsePromise);
|
||||
parsePromise.then(
|
||||
resolvedFiles => {
|
||||
resolvedFiles.forEach(file => {
|
||||
const path = file.originalFilePath;
|
||||
const path = file.name;
|
||||
const firstSlash = path.indexOf('/');
|
||||
|
||||
if (project === extensionsProject) {
|
||||
|
@ -1205,12 +788,12 @@ export function prepareI18nPackFiles(externalExtensions: Map<string>, resultingT
|
|||
const externalId = externalExtensions[resource];
|
||||
if (!externalId) { // internal extension: remove 'extensions/extensionId/' segnent
|
||||
const secondSlash = path.indexOf('/', firstSlash + 1);
|
||||
extPack.contents[path.substr(secondSlash + 1)] = file.messages;
|
||||
extPack.contents[path.substring(secondSlash + 1)] = getRecordFromL10nJsonFormat(file.messages);
|
||||
} else {
|
||||
extPack.contents[path] = file.messages;
|
||||
extPack.contents[path] = getRecordFromL10nJsonFormat(file.messages);
|
||||
}
|
||||
} else {
|
||||
mainPack.contents[path.substr(firstSlash + 1)] = file.messages;
|
||||
mainPack.contents[path.substring(firstSlash + 1)] = getRecordFromL10nJsonFormat(file.messages);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -1248,7 +831,7 @@ export function prepareI18nPackFiles(externalExtensions: Map<string>, resultingT
|
|||
}
|
||||
|
||||
export function prepareIslFiles(language: Language, innoSetupConfig: InnoSetup): ThroughStream {
|
||||
const parsePromises: Promise<ParsedXLF[]>[] = [];
|
||||
const parsePromises: Promise<l10nJsonDetails[]>[] = [];
|
||||
|
||||
return through(function (this: ThroughStream, xlf: File) {
|
||||
const stream = this;
|
||||
|
@ -1257,7 +840,7 @@ export function prepareIslFiles(language: Language, innoSetupConfig: InnoSetup):
|
|||
parsePromise.then(
|
||||
resolvedFiles => {
|
||||
resolvedFiles.forEach(file => {
|
||||
const translatedFile = createIslFile(file.originalFilePath, file.messages, language, innoSetupConfig);
|
||||
const translatedFile = createIslFile(file.name, file.messages, language, innoSetupConfig);
|
||||
stream.queue(translatedFile);
|
||||
});
|
||||
}
|
||||
|
@ -1273,13 +856,13 @@ export function prepareIslFiles(language: Language, innoSetupConfig: InnoSetup):
|
|||
});
|
||||
}
|
||||
|
||||
function createIslFile(originalFilePath: string, messages: Map<string>, language: Language, innoSetup: InnoSetup): File {
|
||||
function createIslFile(name: string, messages: l10nJsonFormat, language: Language, innoSetup: InnoSetup): File {
|
||||
const content: string[] = [];
|
||||
let originalContent: TextModel;
|
||||
if (path.basename(originalFilePath) === 'Default') {
|
||||
originalContent = new TextModel(fs.readFileSync(originalFilePath + '.isl', 'utf8'));
|
||||
if (path.basename(name) === 'Default') {
|
||||
originalContent = new TextModel(fs.readFileSync(name + '.isl', 'utf8'));
|
||||
} else {
|
||||
originalContent = new TextModel(fs.readFileSync(originalFilePath + '.en.isl', 'utf8'));
|
||||
originalContent = new TextModel(fs.readFileSync(name + '.en.isl', 'utf8'));
|
||||
}
|
||||
originalContent.lines.forEach(line => {
|
||||
if (line.length > 0) {
|
||||
|
@ -1302,7 +885,7 @@ function createIslFile(originalFilePath: string, messages: Map<string>, language
|
|||
}
|
||||
});
|
||||
|
||||
const basename = path.basename(originalFilePath);
|
||||
const basename = path.basename(name);
|
||||
const filePath = `${basename}.${language.id}.isl`;
|
||||
const encoded = iconv.encode(Buffer.from(content.join('\r\n'), 'utf8').toString(), innoSetup.codePage);
|
||||
|
||||
|
@ -1336,7 +919,3 @@ function encodeEntities(value: string): string {
|
|||
function decodeEntities(value: string): string {
|
||||
return value.replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&');
|
||||
}
|
||||
|
||||
function pseudify(message: string) {
|
||||
return '\uFF3B' + message.replace(/[aouei]/g, '$&$&') + '\uFF3D';
|
||||
}
|
||||
|
|
|
@ -9,20 +9,20 @@ const i18n = require("../i18n");
|
|||
suite('XLF Parser Tests', () => {
|
||||
const sampleXlf = '<?xml version="1.0" encoding="utf-8"?><xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2"><file original="vs/base/common/keybinding" source-language="en" datatype="plaintext"><body><trans-unit id="key1"><source xml:lang="en">Key #1</source></trans-unit><trans-unit id="key2"><source xml:lang="en">Key #2 &</source></trans-unit></body></file></xliff>';
|
||||
const sampleTranslatedXlf = '<?xml version="1.0" encoding="utf-8"?><xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2"><file original="vs/base/common/keybinding" source-language="en" target-language="ru" datatype="plaintext"><body><trans-unit id="key1"><source xml:lang="en">Key #1</source><target>Кнопка #1</target></trans-unit><trans-unit id="key2"><source xml:lang="en">Key #2 &</source><target>Кнопка #2 &</target></trans-unit></body></file></xliff>';
|
||||
const originalFilePath = 'vs/base/common/keybinding';
|
||||
const name = 'vs/base/common/keybinding';
|
||||
const keys = ['key1', 'key2'];
|
||||
const messages = ['Key #1', 'Key #2 &'];
|
||||
const translatedMessages = { key1: 'Кнопка #1', key2: 'Кнопка #2 &' };
|
||||
test('Keys & messages to XLF conversion', () => {
|
||||
const xlf = new i18n.XLF('vscode-workbench');
|
||||
xlf.addFile(originalFilePath, keys, messages);
|
||||
xlf.addFile(name, keys, messages);
|
||||
const xlfString = xlf.toString();
|
||||
assert.strictEqual(xlfString.replace(/\s{2,}/g, ''), sampleXlf);
|
||||
});
|
||||
test('XLF to keys & messages conversion', () => {
|
||||
i18n.XLF.parse(sampleTranslatedXlf).then(function (resolvedFiles) {
|
||||
assert.deepStrictEqual(resolvedFiles[0].messages, translatedMessages);
|
||||
assert.strictEqual(resolvedFiles[0].originalFilePath, originalFilePath);
|
||||
assert.strictEqual(resolvedFiles[0].name, name);
|
||||
});
|
||||
});
|
||||
test('JSON file source path to Transifex resource match', () => {
|
||||
|
|
|
@ -9,14 +9,14 @@ import i18n = require('../i18n');
|
|||
suite('XLF Parser Tests', () => {
|
||||
const sampleXlf = '<?xml version="1.0" encoding="utf-8"?><xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2"><file original="vs/base/common/keybinding" source-language="en" datatype="plaintext"><body><trans-unit id="key1"><source xml:lang="en">Key #1</source></trans-unit><trans-unit id="key2"><source xml:lang="en">Key #2 &</source></trans-unit></body></file></xliff>';
|
||||
const sampleTranslatedXlf = '<?xml version="1.0" encoding="utf-8"?><xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2"><file original="vs/base/common/keybinding" source-language="en" target-language="ru" datatype="plaintext"><body><trans-unit id="key1"><source xml:lang="en">Key #1</source><target>Кнопка #1</target></trans-unit><trans-unit id="key2"><source xml:lang="en">Key #2 &</source><target>Кнопка #2 &</target></trans-unit></body></file></xliff>';
|
||||
const originalFilePath = 'vs/base/common/keybinding';
|
||||
const name = 'vs/base/common/keybinding';
|
||||
const keys = ['key1', 'key2'];
|
||||
const messages = ['Key #1', 'Key #2 &'];
|
||||
const translatedMessages = { key1: 'Кнопка #1', key2: 'Кнопка #2 &' };
|
||||
|
||||
test('Keys & messages to XLF conversion', () => {
|
||||
const xlf = new i18n.XLF('vscode-workbench');
|
||||
xlf.addFile(originalFilePath, keys, messages);
|
||||
xlf.addFile(name, keys, messages);
|
||||
const xlfString = xlf.toString();
|
||||
|
||||
assert.strictEqual(xlfString.replace(/\s{2,}/g, ''), sampleXlf);
|
||||
|
@ -25,7 +25,7 @@ suite('XLF Parser Tests', () => {
|
|||
test('XLF to keys & messages conversion', () => {
|
||||
i18n.XLF.parse(sampleTranslatedXlf).then(function (resolvedFiles) {
|
||||
assert.deepStrictEqual(resolvedFiles[0].messages, translatedMessages);
|
||||
assert.strictEqual(resolvedFiles[0].originalFilePath, originalFilePath);
|
||||
assert.strictEqual(resolvedFiles[0].name, name);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -68,7 +68,7 @@ function update(options) {
|
|||
console.log(`Importing translations for ${languageId} form '${location}' to '${translationDataFolder}' ...`);
|
||||
let translationPaths = [];
|
||||
gulp.src(path.join(location, '**', languageId, '*.xlf'), { silent: false })
|
||||
.pipe(i18n.prepareI18nPackFiles(i18n.externalExtensionsWithTranslations, translationPaths, languageId === 'ps'))
|
||||
.pipe(i18n.prepareI18nPackFiles(i18n.externalExtensionsWithTranslations, translationPaths))
|
||||
.on('error', (error) => {
|
||||
console.log(`Error occurred while importing translations:`);
|
||||
translationPaths = undefined;
|
||||
|
|
|
@ -128,6 +128,7 @@
|
|||
"@typescript-eslint/eslint-plugin": "^5.39.0",
|
||||
"@typescript-eslint/experimental-utils": "^5.39.0",
|
||||
"@typescript-eslint/parser": "^5.39.0",
|
||||
"@vscode/l10n-dev": "0.0.15",
|
||||
"@vscode/telemetry-extractor": "^1.9.8",
|
||||
"@vscode/test-web": "^0.0.29",
|
||||
"ansi-colors": "^3.2.3",
|
||||
|
|
88
yarn.lock
88
yarn.lock
|
@ -1253,6 +1253,18 @@
|
|||
resolved "https://registry.yarnpkg.com/@vscode/iconv-lite-umd/-/iconv-lite-umd-0.7.0.tgz#d2f1e0664ee6036408f9743fee264ea0699b0e48"
|
||||
integrity sha512-bRRFxLfg5dtAyl5XyiVWz/ZBPahpOpPrNYnnHpOpUZvam4tKH35wdhP4Kj6PbM0+KdliOsPzbGWpkxcdpNB/sg==
|
||||
|
||||
"@vscode/l10n-dev@0.0.15":
|
||||
version "0.0.15"
|
||||
resolved "https://registry.yarnpkg.com/@vscode/l10n-dev/-/l10n-dev-0.0.15.tgz#677b527987ccd39e32c50956f139736a788061d6"
|
||||
integrity sha512-zLuo/pa+FtnFrVq/7M8VHshgejNZ6TvnRW9/um1pLkg92PZ9glDgmwXUv1AdpBu5KNzgH9odiMKS4YQDkS12wQ==
|
||||
dependencies:
|
||||
deepmerge-json "^1.5.0"
|
||||
glob "^8.0.3"
|
||||
pseudo-localization "^2.4.0"
|
||||
typescript "^4.7.4"
|
||||
xml2js "^0.4.23"
|
||||
yargs "^17.5.1"
|
||||
|
||||
"@vscode/ripgrep@^1.14.2":
|
||||
version "1.14.2"
|
||||
resolved "https://registry.yarnpkg.com/@vscode/ripgrep/-/ripgrep-1.14.2.tgz#47c0eec2b64f53d8f7e1b5ffd22a62e229191c34"
|
||||
|
@ -2781,6 +2793,15 @@ cliui@^7.0.2:
|
|||
strip-ansi "^6.0.0"
|
||||
wrap-ansi "^7.0.0"
|
||||
|
||||
cliui@^8.0.1:
|
||||
version "8.0.1"
|
||||
resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa"
|
||||
integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==
|
||||
dependencies:
|
||||
string-width "^4.2.0"
|
||||
strip-ansi "^6.0.1"
|
||||
wrap-ansi "^7.0.0"
|
||||
|
||||
clone-buffer@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58"
|
||||
|
@ -3591,6 +3612,11 @@ deep-is@~0.1.3:
|
|||
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
|
||||
integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
|
||||
|
||||
deepmerge-json@^1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/deepmerge-json/-/deepmerge-json-1.5.0.tgz#6daa3600d53fc1f646604853bc99e95e260fbda0"
|
||||
integrity sha512-jZRrDmBKjmGcqMFEUJ14FjMJwm05Qaked+1vxaALRtF0UAl7lPU8OLWXFxvoeg3jbQM249VPFVn8g2znaQkEtA==
|
||||
|
||||
deepmerge@^3.0.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-3.1.0.tgz#a612626ce4803da410d77554bfd80361599c034d"
|
||||
|
@ -5025,6 +5051,11 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.1:
|
|||
has "^1.0.3"
|
||||
has-symbols "^1.0.1"
|
||||
|
||||
get-stdin@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-7.0.0.tgz#8d5de98f15171a125c5e516643c7a6d0ea8a96f6"
|
||||
integrity sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ==
|
||||
|
||||
get-stream@6.0.1, get-stream@^6.0.0:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7"
|
||||
|
@ -5172,6 +5203,17 @@ glob@^7.1.3:
|
|||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
glob@^8.0.3:
|
||||
version "8.0.3"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-8.0.3.tgz#415c6eb2deed9e502c68fa44a272e6da6eeca42e"
|
||||
integrity sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==
|
||||
dependencies:
|
||||
fs.realpath "^1.0.0"
|
||||
inflight "^1.0.4"
|
||||
inherits "2"
|
||||
minimatch "^5.0.1"
|
||||
once "^1.3.0"
|
||||
|
||||
global-agent@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/global-agent/-/global-agent-3.0.0.tgz#ae7cd31bd3583b93c5a16437a1afe27cc33a1ab6"
|
||||
|
@ -7322,7 +7364,7 @@ minimatch@4.2.1:
|
|||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimatch@^5.1.0:
|
||||
minimatch@^5.0.1, minimatch@^5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.0.tgz#1717b464f4971b144f6aabe8f2d0b8e4511e09c7"
|
||||
integrity sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==
|
||||
|
@ -8876,6 +8918,16 @@ prr@~1.0.1:
|
|||
resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
|
||||
integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY=
|
||||
|
||||
pseudo-localization@^2.4.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/pseudo-localization/-/pseudo-localization-2.4.0.tgz#5c19da35bc182ad7fc00d82d33dd42e88005e241"
|
||||
integrity sha512-ISYMOKY8+f+PmiXMFw2y6KLY74LBrv/8ml/VjjoVEV2k+MS+OJZz7ydciK5ntJwxPrKQPTU1+oXq9Mx2b0zEzg==
|
||||
dependencies:
|
||||
flat "^5.0.2"
|
||||
get-stdin "^7.0.0"
|
||||
typescript "^4.7.4"
|
||||
yargs "^17.2.1"
|
||||
|
||||
pseudomap@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
|
||||
|
@ -10050,6 +10102,15 @@ string-width@^4.1.0, string-width@^4.2.0:
|
|||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
string-width@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
string.prototype.padend@^3.0.0:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/string.prototype.padend/-/string.prototype.padend-3.1.1.tgz#824c84265dbac46cade2b957b38b6a5d8d1683c5"
|
||||
|
@ -10771,6 +10832,11 @@ typescript@^2.6.2:
|
|||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.6.2.tgz#3c5b6fd7f6de0914269027f03c0946758f7673a4"
|
||||
integrity sha1-PFtv1/beCRQmkCfwPAlGdY92c6Q=
|
||||
|
||||
typescript@^4.7.4:
|
||||
version "4.8.4"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6"
|
||||
integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==
|
||||
|
||||
typescript@^4.9.0-dev.20221011:
|
||||
version "4.9.0-dev.20221011"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.0-dev.20221011.tgz#5c0ccbb7cfc1d8928fec987b7fc490cd664869e3"
|
||||
|
@ -11534,7 +11600,7 @@ ws@^7.2.0:
|
|||
resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c"
|
||||
integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==
|
||||
|
||||
xml2js@^0.4.17:
|
||||
xml2js@^0.4.17, xml2js@^0.4.23:
|
||||
version "0.4.23"
|
||||
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66"
|
||||
integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==
|
||||
|
@ -11681,6 +11747,11 @@ yargs-parser@^20.2.2:
|
|||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
|
||||
integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
|
||||
|
||||
yargs-parser@^21.0.0:
|
||||
version "21.1.1"
|
||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
|
||||
integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
|
||||
|
||||
yargs-unparser@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb"
|
||||
|
@ -11738,6 +11809,19 @@ yargs@^15.3.0:
|
|||
y18n "^4.0.0"
|
||||
yargs-parser "^18.1.2"
|
||||
|
||||
yargs@^17.2.1, yargs@^17.5.1:
|
||||
version "17.6.0"
|
||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.6.0.tgz#e134900fc1f218bc230192bdec06a0a5f973e46c"
|
||||
integrity sha512-8H/wTDqlSwoSnScvV2N/JHfLWOKuh5MVla9hqLjK3nsfyy6Y4kDSYSvkU5YCUEPOSnRXfIyx3Sq+B/IWudTo4g==
|
||||
dependencies:
|
||||
cliui "^8.0.1"
|
||||
escalade "^3.1.1"
|
||||
get-caller-file "^2.0.5"
|
||||
require-directory "^2.1.1"
|
||||
string-width "^4.2.3"
|
||||
y18n "^5.0.5"
|
||||
yargs-parser "^21.0.0"
|
||||
|
||||
yargs@^7.1.0:
|
||||
version "7.1.1"
|
||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.1.tgz#67f0ef52e228d4ee0d6311acede8850f53464df6"
|
||||
|
|
Loading…
Reference in a new issue