diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js
index 61b1db21fde..989d1a78b72 100644
--- a/build/gulpfile.vscode.js
+++ b/build/gulpfile.vscode.js
@@ -29,6 +29,7 @@ const packageJson = require('../package.json');
const product = require('../product.json');
const shrinkwrap = require('../npm-shrinkwrap.json');
const crypto = require('crypto');
+const i18n = require('./lib/i18n');
const dependencies = Object.keys(shrinkwrap.dependencies)
.concat(Array.isArray(product.extraNodeModules) ? product.extraNodeModules : []); // additional dependencies from our product configuration
@@ -339,6 +340,38 @@ gulp.task('vscode-linux-ia32-min', ['minify-vscode', 'clean-vscode-linux-ia32'],
gulp.task('vscode-linux-x64-min', ['minify-vscode', 'clean-vscode-linux-x64'], packageTask('linux', 'x64', { minified: true }));
gulp.task('vscode-linux-arm-min', ['minify-vscode', 'clean-vscode-linux-arm'], packageTask('linux', 'arm', { minified: true }));
+const apiUrl = process.env.TRANSIFEX_API_URL;
+const apiName = process.env.TRANSIFEX_API_NAME;
+const apiToken = process.env.TRANSIFEX_API_TOKEN;
+
+gulp.task('vscode-translations-update', function() {
+ const pathToMetadata = './out-vscode/nls.metadata.json';
+ const pathToExtensions = './extensions/**/*.nls.json';
+ const pathToSetup = 'build/win32/**/{Default.isl,messages.en.isl}';
+
+ gulp.src(pathToMetadata)
+ .pipe(i18n.prepareXlfFiles())
+ .pipe(i18n.pushXlfFiles(apiUrl, apiName, apiToken));
+
+ gulp.src(pathToSetup)
+ .pipe(i18n.prepareXlfFiles())
+ .pipe(i18n.pushXlfFiles(apiUrl, apiName, apiToken));
+
+ return gulp.src(pathToExtensions)
+ .pipe(i18n.prepareXlfFiles('vscode-extensions'))
+ .pipe(i18n.pushXlfFiles(apiUrl, apiName, apiToken));
+});
+
+gulp.task('vscode-translations-pull', function() {
+ i18n.pullXlfFiles('vscode-editor-workbench', apiUrl, apiName, apiToken)
+ .pipe(i18n.prepareJsonFiles())
+ .pipe(vfs.dest('./i18n'));
+
+ return i18n.pullXlfFiles('vscode-extensions', apiUrl, apiName, apiToken)
+ .pipe(i18n.prepareJsonFiles())
+ .pipe(vfs.dest('./i18n'));
+});
+
// Sourcemaps
gulp.task('upload-vscode-sourcemaps', ['minify-vscode'], () => {
diff --git a/build/lib/i18n.js b/build/lib/i18n.js
index 7dcf0a0b763..a8256d1ad37 100644
--- a/build/lib/i18n.js
+++ b/build/lib/i18n.js
@@ -10,6 +10,13 @@ var event_stream_1 = require("event-stream");
var File = require("vinyl");
var Is = require("is");
var util = require('gulp-util');
+var xml2js = require("xml2js");
+var glob = require("glob");
+var util = require('gulp-util');
+var request = require('request');
+var es = require('event-stream');
+var iconv = require('iconv-lite');
+
function log(message) {
var rest = [];
for (var _i = 1; _i < arguments.length; _i++) {
@@ -37,6 +44,174 @@ 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(function (key) {
+ var element = value[key];
+ return Is.string(element) || (Is.object(element) && Is.defined(element.message) && Is.defined(element.comment));
+ });
+ }
+ PackageJsonFormat.is = is;
+})(PackageJsonFormat || (PackageJsonFormat = {}));
+var ModuleJsonFormat;
+(function (ModuleJsonFormat) {
+ function is(value) {
+ var candidate = value;
+ return Is.defined(candidate)
+ && Is.array(candidate.messages) && candidate.messages.every(function (message) { return Is.string(message); })
+ && Is.array(candidate.keys) && candidate.keys.every(function (key) { return Is.string(key) || LocalizeInfo.is(key); });
+ }
+ ModuleJsonFormat.is = is;
+})(ModuleJsonFormat || (ModuleJsonFormat = {}));
+var Line = (function () {
+ function Line(indent) {
+ if (indent === void 0) { indent = 0; }
+ this.indent = indent;
+ this.buffer = [];
+ if (indent > 0) {
+ this.buffer.push(new Array(indent + 1).join(' '));
+ }
+ }
+ Line.prototype.append = function (value) {
+ this.buffer.push(value);
+ return this;
+ };
+ Line.prototype.toString = function () {
+ return this.buffer.join('');
+ };
+ return Line;
+}());
+exports.Line = Line;
+var TextModel = (function () {
+ function TextModel(contents) {
+ this._lines = contents.split(/\r\n|\r|\n/);
+ }
+ Object.defineProperty(TextModel.prototype, "lines", {
+ get: function () {
+ return this._lines;
+ },
+ enumerable: true,
+ configurable: true
+ });
+ return TextModel;
+}());
+var XLF = (function () {
+ function XLF(project) {
+ this.project = project;
+ this.buffer = [];
+ this.files = Object.create(null);
+ }
+ XLF.prototype.toString = function () {
+ this.appendHeader();
+ for (var file in this.files) {
+ this.appendNewLine("", 2);
+ for (var _i = 0, _a = this.files[file]; _i < _a.length; _i++) {
+ var item = _a[_i];
+ this.addStringItem(item);
+ }
+ this.appendNewLine('', 2);
+ }
+ this.appendFooter();
+ return this.buffer.join('\r\n');
+ };
+ XLF.prototype.addFile = function (original, keys, messages) {
+ this.files[original] = [];
+ var existingKeys = [];
+ for (var _i = 0, keys_1 = keys; _i < keys_1.length; _i++) {
+ var key = keys_1[_i];
+ // Ignore duplicate keys because Transifex does not populate those with translated values.
+ if (existingKeys.indexOf(key) !== -1) {
+ continue;
+ }
+ existingKeys.push(key);
+ var message = encodeEntities(messages[keys.indexOf(key)]);
+ var comment = undefined;
+ // Check if the message contains description (if so, it becomes an object type in JSON)
+ if (Is.string(key)) {
+ this.files[original].push({ id: key, message: message, comment: comment });
+ }
+ else {
+ if (key['comment'] && key['comment'].length > 0) {
+ comment = key['comment'].map(function (comment) { return encodeEntities(comment); }).join('\r\n');
+ }
+ this.files[original].push({ id: key['key'], message: message, comment: comment });
+ }
+ }
+ };
+ XLF.prototype.addStringItem = function (item) {
+ if (!item.id || !item.message) {
+ //throw new Error('No item ID or value specified.');
+ }
+ this.appendNewLine("", 4);
+ this.appendNewLine("" + encodeEntities(item.message) + "", 6);
+ if (item.comment) {
+ this.appendNewLine("" + item.comment + "", 6);
+ }
+ this.appendNewLine('', 4);
+ };
+ XLF.prototype.appendHeader = function () {
+ this.appendNewLine('', 0);
+ this.appendNewLine('', 0);
+ };
+ XLF.prototype.appendFooter = function () {
+ this.appendNewLine('', 0);
+ };
+ XLF.prototype.appendNewLine = function (content, indent) {
+ var line = new Line(indent);
+ line.append(content);
+ this.buffer.push(line.toString());
+ };
+ return XLF;
+}());
+XLF.parse = function (xlfString) {
+ return new Promise(function (resolve, reject) {
+ var parser = new xml2js.Parser();
+ var files = [];
+ parser.parseString(xlfString, function (err, result) {
+ if (err) {
+ reject("Failed to parse XLIFF string. " + err);
+ }
+ var fileNodes = result['xliff']['file'];
+ if (!fileNodes) {
+ reject('XLIFF file does not contain "xliff" or "file" node(s) required for parsing.');
+ }
+ fileNodes.forEach(function (file) {
+ var originalFilePath = file.$.original;
+ if (!originalFilePath) {
+ reject('XLIFF file node does not contain original attribute to determine the original location of the resource file.');
+ }
+ var language = file.$['target-language'].toLowerCase();
+ if (!language) {
+ reject('XLIFF file node does not contain target-language attribute to determine translated language.');
+ }
+ var messages = {};
+ var transUnits = file.body[0]['trans-unit'];
+ transUnits.forEach(function (unit) {
+ var key = unit.$.id;
+ if (!unit.target) {
+ return; // No translation available
+ }
+ var val = unit.target.toString();
+ if (key && val) {
+ messages[key] = decodeEntities(val);
+ }
+ else {
+ reject('XLIFF file does not contain full localization data. ID or target translation for one of the trans-unit nodes is not present.');
+ }
+ });
+ files.push({ messages: messages, originalFilePath: originalFilePath, language: language });
+ });
+ resolve(files);
+ });
+ });
+};
+exports.XLF = XLF;
+
var vscodeLanguages = [
'chs',
'cht',
@@ -68,6 +243,28 @@ var iso639_3_to_2 = {
'sve': 'sv-se',
'trk': 'tr'
};
+
+var iso639_2_to_3 = {
+ 'zh-cn': 'chs',
+ 'zh-tw': 'cht',
+ 'cs-cz': 'csy',
+ 'de': 'deu',
+ 'en': 'enu',
+ 'es': 'esn',
+ 'fr': 'fra',
+ 'hu': 'hun',
+ 'it': 'ita',
+ 'ja': 'jpn',
+ 'ko': 'kor',
+ 'nl': 'nld',
+ 'pl': 'plk',
+ 'pt-br': 'ptb',
+ 'pt': 'ptg',
+ 'ru': 'rus',
+ 'sv-se': 'sve',
+ 'tr': 'trk'
+};
+
function sortLanguages(directoryNames) {
return directoryNames.map(function (dirName) {
var lower = dirName.toLowerCase();
@@ -288,3 +485,472 @@ function processNlsFiles(opts) {
});
}
exports.processNlsFiles = processNlsFiles;
+
+function prepareXlfFiles(projectName, extensionName) {
+ return event_stream_1.through(function (file) {
+ if (!file.isBuffer()) {
+ log('Error', "Failed to read component file: " + file.relative);
+ }
+ var extension = path.extname(file.path);
+ if (extension === '.json') {
+ var json = JSON.parse(file.contents.toString('utf8'));
+ if (BundledFormat.is(json)) {
+ importBundleJson(file, json, this);
+ }
+ else if (PackageJsonFormat.is(json) || ModuleJsonFormat.is(json)) {
+ importModuleOrPackageJson(file, json, projectName, this, extensionName);
+ }
+ else {
+ log('Error', 'JSON format cannot be deduced.');
+ }
+ }
+ else if (extension === '.isl') {
+ importIsl(file, this);
+ }
+ });
+}
+exports.prepareXlfFiles = prepareXlfFiles;
+function getResource(sourceFile) {
+ var editorProject = 'vscode-editor', workbenchProject = 'vscode-workbench';
+ var resource;
+ if (sourceFile.startsWith('vs/platform')) {
+ return { name: 'vs/platform', project: editorProject };
+ }
+ else if (sourceFile.startsWith('vs/editor/contrib')) {
+ return { name: 'vs/editor/contrib', project: editorProject };
+ }
+ else if (sourceFile.startsWith('vs/editor')) {
+ return { name: 'vs/editor', project: editorProject };
+ }
+ else if (sourceFile.startsWith('vs/base')) {
+ return { name: 'vs/base', project: editorProject };
+ }
+ else if (sourceFile.startsWith('vs/code')) {
+ return { name: 'vs/code', project: workbenchProject };
+ }
+ else if (sourceFile.startsWith('vs/workbench/parts')) {
+ resource = sourceFile.split('/', 4).join('/');
+ return { name: resource, project: workbenchProject };
+ }
+ else if (sourceFile.startsWith('vs/workbench/services')) {
+ resource = sourceFile.split('/', 4).join('/');
+ return { name: resource, project: workbenchProject };
+ }
+ else if (sourceFile.startsWith('vs/workbench')) {
+ return { name: 'vs/workbench', project: workbenchProject };
+ }
+ throw new Error("Could not identify the XLF bundle for " + sourceFile);
+}
+function importBundleJson(file, json, stream) {
+ var transifexEditorXlfs = Object.create(null);
+ for (var source in json.keys) {
+ var projectResource = getResource(source);
+ var resource = projectResource.name;
+ var project = projectResource.project;
+ var keys = json.keys[source];
+ var messages = json.messages[source];
+ if (keys.length !== messages.length) {
+ log('Error:', "There is a mismatch between keys and messages in " + file.relative);
+ }
+ var xlf = transifexEditorXlfs[resource] ? transifexEditorXlfs[resource] : transifexEditorXlfs[resource] = new XLF(project);
+ xlf.addFile(source, keys, messages);
+ }
+ for (var resource in transifexEditorXlfs) {
+ var newFilePath = transifexEditorXlfs[resource].project + "/" + resource.replace(/\//g, '_') + ".xlf";
+ var xlfFile = new File({ path: newFilePath, contents: new Buffer(transifexEditorXlfs[resource].toString(), 'utf-8') });
+ stream.emit('data', xlfFile);
+ }
+}
+// Keeps existing XLF instances and a state of how many files were already processed for faster file emission
+var extensions = Object.create(null);
+function importModuleOrPackageJson(file, json, projectName, stream, extensionName) {
+ if (ModuleJsonFormat.is(json) && json['keys'].length !== json['messages'].length) {
+ log('Error:', "There is a mismatch between keys and messages in " + file.relative);
+ }
+ // Prepare the source path for attribute in XLF & extract messages from JSON
+ var formattedSourcePath = file.relative.replace(/\\/g, '/');
+ var messages = Object.keys(json).map(function (key) { return json[key].toString(); });
+ // Stores the amount of localization files to be transformed to XLF before the emission
+ var localizationFilesCount, originalFilePath;
+ // If preparing XLF for external extension, then use different glob pattern and source path
+ if (extensionName) {
+ localizationFilesCount = glob.sync('**/*.nls.json').length;
+ originalFilePath = "" + formattedSourcePath.substr(0, formattedSourcePath.length - '.nls.json'.length);
+ }
+ else {
+ // Used for vscode/extensions folder
+ extensionName = formattedSourcePath.split('/')[0];
+ localizationFilesCount = glob.sync("./extensions/" + extensionName + "/**/*.nls.json").length;
+ originalFilePath = "extensions/" + formattedSourcePath.substr(0, formattedSourcePath.length - '.nls.json'.length);
+ }
+ var extension = extensions[extensionName] ?
+ extensions[extensionName] : extensions[extensionName] = { xlf: new XLF(projectName), processed: 0 };
+ if (ModuleJsonFormat.is(json)) {
+ extension.xlf.addFile(originalFilePath, json['keys'], json['messages']);
+ }
+ else {
+ extension.xlf.addFile(originalFilePath, Object.keys(json), messages);
+ }
+ // Check if XLF is populated with file nodes to emit it
+ if (++extensions[extensionName].processed === localizationFilesCount) {
+ var newFilePath = path.join(projectName, extensionName + '.xlf');
+ var xlfFile = new File({ path: newFilePath, contents: new Buffer(extension.xlf.toString(), 'utf-8') });
+ stream.emit('data', xlfFile);
+ }
+}
+var islXlf, islProcessed = 0;
+function importIsl(file, stream) {
+ var islFiles = ['Default.isl', 'messages.en.isl'];
+ var projectName = 'vscode-workbench';
+ var xlf = islXlf ? islXlf : islXlf = new XLF(projectName), keys = [], messages = [];
+ var model = new TextModel(file.contents.toString());
+ var inMessageSection = false;
+ model.lines.forEach(function (line) {
+ if (line.length === 0) {
+ return;
+ }
+ var firstChar = line.charAt(0);
+ switch (firstChar) {
+ case ';':
+ // Comment line;
+ return;
+ case '[':
+ inMessageSection = '[Messages]' === line || '[CustomMessages]' === line;
+ return;
+ }
+ if (!inMessageSection) {
+ return;
+ }
+ var sections = line.split('=');
+ if (sections.length !== 2) {
+ log('Error:', "Badly formatted message found: " + line);
+ }
+ else {
+ var key = sections[0];
+ var value = sections[1];
+ if (key.length > 0 && value.length > 0) {
+ keys.push(key);
+ messages.push(value);
+ }
+ }
+ });
+ var originalPath = file.path.substring(file.cwd.length + 1, file.path.split('.')[0].length).replace(/\\/g, '/');
+ xlf.addFile(originalPath, keys, messages);
+ // Emit only upon all ISL files combined into single XLF instance
+ if (++islProcessed === islFiles.length) {
+ var newFilePath = path.join(projectName, 'setup.xlf');
+ var xlfFile = new File({ path: newFilePath, contents: new Buffer(xlf.toString(), 'utf-8') });
+ stream.emit('data', xlfFile);
+ }
+}
+function pushXlfFiles(apiUrl, username, password) {
+ return event_stream_1.through(function (file) {
+ var project = path.dirname(file.relative);
+ var fileName = path.basename(file.path);
+ var slug = fileName.substr(0, fileName.length - '.xlf'.length);
+ var credentials = {
+ 'user': username,
+ 'password': password
+ };
+ // Check if resource already exists, if not, then create it.
+ tryGetResource(project, slug, apiUrl, credentials).then(function (exists) {
+ if (exists) {
+ updateResource(project, slug, file, apiUrl, credentials);
+ }
+ else {
+ createResource(project, slug, file, apiUrl, credentials);
+ }
+ });
+ });
+}
+exports.pushXlfFiles = pushXlfFiles;
+function tryGetResource(project, slug, apiUrl, credentials) {
+ return new Promise(function (resolve, reject) {
+ var url = apiUrl + "/project/" + project + "/resource/" + slug + "/?details";
+ request.get(url, { 'auth': credentials }).on('response', function (response) {
+ if (response.statusCode === 404) {
+ resolve(false);
+ }
+ else if (response.statusCode === 200) {
+ resolve(true);
+ }
+ else {
+ reject("Failed to query resource " + slug + ". Response: " + response.statusCode + " " + response.statusMessage);
+ }
+ });
+ });
+}
+function createResource(project, slug, xlfFile, apiUrl, credentials) {
+ var url = apiUrl + "/project/" + project + "/resources";
+ var options = {
+ 'body': {
+ 'content': xlfFile.contents.toString(),
+ 'name': slug,
+ 'slug': slug,
+ 'i18n_type': 'XLIFF'
+ },
+ 'json': true,
+ 'auth': credentials
+ };
+ request.post(url, options, function (err, res) {
+ if (err) {
+ log('Error:', "Failed to create Transifex " + project + "/" + slug + ": " + err);
+ }
+ if (res.statusCode === 201) {
+ log("Resource " + project + "/" + slug + " successfully created on Transifex.");
+ }
+ else {
+ log('Error:', "Something went wrong creating " + slug + " in " + project + ". " + res.statusCode);
+ }
+ });
+}
+/**
+ * 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, apiUrl, credentials) {
+ var url = apiUrl + "/project/" + project + "/resource/" + slug + "/content";
+ var options = {
+ 'body': { 'content': xlfFile.contents.toString() },
+ 'json': true,
+ 'auth': credentials
+ };
+ request.put(url, options, function (err, res, body) {
+ if (err) {
+ log('Error:', "Failed to update Transifex " + project + "/" + slug + ": " + err);
+ }
+ if (res.statusCode === 200) {
+ log("Resource " + project + "/" + slug + " successfully updated on Transifex. Strings added: " + body['strings_added'] + ", updated: " + body['strings_updated'] + ", deleted: " + body['strings_delete']);
+ }
+ else {
+ log('Error:', "Something went wrong updating " + slug + " in " + project + ". " + res.statusCode);
+ }
+ });
+}
+function getMetadataResources(pathToMetadata) {
+ var metadata = fs.readFileSync(pathToMetadata).toString('utf8');
+ var json = JSON.parse(metadata);
+ var slugs = [];
+ var _loop_1 = function (source) {
+ var projectResource = getResource(source);
+ if (!slugs.find(function (slug) { return slug.name === projectResource.name && slug.project === projectResource.project; })) {
+ slugs.push(projectResource);
+ }
+ };
+ for (var source in json['keys']) {
+ _loop_1(source);
+ }
+ return slugs;
+}
+function obtainProjectResources(projectName) {
+ var resources;
+ if (projectName === 'vscode-editor-workbench') {
+ resources = getMetadataResources('./out-vscode/nls.metadata.json');
+ resources.push({ name: 'setup', project: 'vscode-workbench' });
+ }
+ else if (projectName === 'vscode-extensions') {
+ var extensionsToLocalize = glob.sync('./extensions/**/*.nls.json').map(function (extension) { return extension.split('/')[2]; });
+ var resourcesToPull_1 = [];
+ resources = [];
+ extensionsToLocalize.forEach(function (extension) {
+ if (resourcesToPull_1.indexOf(extension) === -1) {
+ resourcesToPull_1.push(extension);
+ resources.push({ name: extension, project: projectName });
+ }
+ });
+ }
+ return resources;
+}
+function pullXlfFiles(projectName, apiUrl, username, password, resources) {
+ if (!resources) {
+ resources = obtainProjectResources(projectName);
+ }
+ if (!resources) {
+ throw new Error('Transifex projects and resources must be defined to be able to pull translations from Transifex.');
+ }
+ var credentials = {
+ 'auth': {
+ 'user': username,
+ 'password': password
+ }
+ };
+ var expectedTranslationsCount = vscodeLanguages.length * resources.length;
+ var translationsRetrieved = 0, called = false;
+ return es.readable(function (count, callback) {
+ // Mark end of stream when all resources were retrieved
+ if (translationsRetrieved === expectedTranslationsCount) {
+ return this.emit('end');
+ }
+ if (!called) {
+ called = true;
+ var stream_1 = this;
+ vscodeLanguages.map(function (language) {
+ resources.map(function (resource) {
+ var slug = resource.name.replace(/\//g, '_');
+ var project = resource.project;
+ var iso639 = iso639_3_to_2[language];
+ var url = apiUrl + "/project/" + project + "/resource/" + slug + "/translation/" + iso639 + "?file&mode=onlyreviewed";
+ var xlfBuffer = '', responseCode;
+ request.get(url, credentials)
+ .on('response', function (response) {
+ responseCode = response.statusCode;
+ })
+ .on('data', function (data) { return xlfBuffer += data; })
+ .on('end', function () {
+ if (responseCode === 200) {
+ stream_1.emit('data', new File({ contents: new Buffer(xlfBuffer) }));
+ }
+ else {
+ log('Error:', slug + " in " + project + " returned no data. Response code: " + responseCode + ".");
+ }
+ translationsRetrieved++;
+ })
+ .on('error', function (error) {
+ log('Error:', "Failed to query resource " + slug + " with the following error: " + error);
+ });
+ });
+ });
+ }
+ callback();
+ });
+}
+exports.pullXlfFiles = pullXlfFiles;
+function prepareJsonFiles() {
+ return event_stream_1.through(function (xlf) {
+ var stream = this;
+ XLF.parse(xlf.contents.toString()).then(function (resolvedFiles) {
+ resolvedFiles.forEach(function (file) {
+ var messages = file.messages, translatedFile;
+ // ISL file path always starts with 'build/'
+ if (file.originalFilePath.startsWith('build/')) {
+ var defaultLanguages = { 'zh-cn': true, 'zh-tw': true, 'ko': true };
+ if (path.basename(file.originalFilePath) === 'Default' && !defaultLanguages[file.language]) {
+ return;
+ }
+ translatedFile = createIslFile('..', file.originalFilePath, messages, iso639_2_to_3[file.language]);
+ }
+ else {
+ translatedFile = createI18nFile(iso639_2_to_3[file.language], file.originalFilePath, messages);
+ }
+ stream.emit('data', translatedFile);
+ });
+ }, function (rejectReason) {
+ log('Error:', rejectReason);
+ });
+ });
+}
+exports.prepareJsonFiles = prepareJsonFiles;
+function createI18nFile(base, originalFilePath, messages) {
+ var content = [
+ '/*---------------------------------------------------------------------------------------------',
+ ' * Copyright (c) Microsoft Corporation. All rights reserved.',
+ ' * Licensed under the MIT License. See License.txt in the project root for license information.',
+ ' *--------------------------------------------------------------------------------------------*/',
+ '// Do not edit this file. It is machine generated.'
+ ].join('\n') + '\n' + JSON.stringify(messages, null, '\t').replace(/\r\n/g, '\n');
+ return new File({
+ path: path.join(base, originalFilePath + '.i18n.json'),
+ contents: new Buffer(content, 'utf8')
+ });
+}
+exports.createI18nFile = createI18nFile;
+var languageNames = {
+ 'chs': 'Simplified Chinese',
+ 'cht': 'Traditional Chinese',
+ 'kor': 'Korean'
+};
+var languageIds = {
+ 'chs': '$0804',
+ 'cht': '$0404',
+ 'kor': '$0412'
+};
+var encodings = {
+ 'chs': 'CP936',
+ 'cht': 'CP950',
+ 'jpn': 'CP932',
+ 'kor': 'CP949',
+ 'deu': 'CP1252',
+ 'fra': 'CP1252',
+ 'esn': 'CP1252',
+ 'rus': 'CP1251',
+ 'ita': 'CP1252'
+};
+function createIslFile(base, originalFilePath, messages, language) {
+ var content = [];
+ var originalContent;
+ if (path.basename(originalFilePath) === 'Default') {
+ originalContent = new TextModel(fs.readFileSync(originalFilePath + '.isl', 'utf8'));
+ }
+ else {
+ originalContent = new TextModel(fs.readFileSync(originalFilePath + '.en.isl', 'utf8'));
+ }
+ originalContent.lines.forEach(function (line) {
+ if (line.length > 0) {
+ var firstChar = line.charAt(0);
+ if (firstChar === '[' || firstChar === ';') {
+ if (line === '; *** Inno Setup version 5.5.3+ English messages ***') {
+ content.push("; *** Inno Setup version 5.5.3+ " + languageNames[language] + " messages ***");
+ }
+ else {
+ content.push(line);
+ }
+ }
+ else {
+ var sections = line.split('=');
+ var key = sections[0];
+ var translated = line;
+ if (key) {
+ if (key === 'LanguageName') {
+ translated = key + "=" + languageNames[language];
+ }
+ else if (key === 'LanguageID') {
+ translated = key + "=" + languageIds[language];
+ }
+ else if (key === 'LanguageCodePage') {
+ translated = key + "=" + encodings[language].substr(2);
+ }
+ else {
+ var translatedMessage = messages[key];
+ if (translatedMessage) {
+ translated = key + "=" + translatedMessage;
+ }
+ }
+ }
+ content.push(translated);
+ }
+ }
+ });
+ var tag = iso639_3_to_2[language];
+ var basename = path.basename(originalFilePath);
+ var filePath = path.join(base, path.dirname(originalFilePath), basename) + "." + tag + ".isl";
+ return new File({
+ path: filePath,
+ contents: iconv.encode(new Buffer(content.join('\r\n'), 'utf8'), encodings[language])
+ });
+}
+exports.createIslFile = createIslFile;
+function encodeEntities(value) {
+ var result = [];
+ for (var i = 0; i < value.length; i++) {
+ var ch = value[i];
+ switch (ch) {
+ case '<':
+ result.push('<');
+ break;
+ case '>':
+ result.push('>');
+ break;
+ case '&':
+ result.push('&');
+ break;
+ default:
+ result.push(ch);
+ }
+ }
+ return result.join('');
+}
+function decodeEntities(value) {
+ return value.replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&');
+}
+exports.decodeEntities = decodeEntities;
+//# sourceMappingURL=i18n.js.map
diff --git a/build/lib/i18n.ts b/build/lib/i18n.ts
index 046cd8c8d8b..146664b6a34 100644
--- a/build/lib/i18n.ts
+++ b/build/lib/i18n.ts
@@ -10,8 +10,13 @@ import { through } from 'event-stream';
import { ThroughStream } from 'through';
import File = require('vinyl');
import * as Is from 'is';
+import xml2js = require('xml2js');
+import * as glob from 'glob';
var util = require('gulp-util');
+const request = require('request');
+const es = require('event-stream');
+var iconv = require('iconv-lite');
function log(message: any, ...rest: any[]): void {
util.log(util.colors.green('[i18n]'), message, ...rest);
@@ -21,6 +26,17 @@ interface Map {
[key: string]: V;
}
+interface Item {
+ id: string;
+ message: string;
+ comment: string;
+}
+
+interface Resource {
+ name: string;
+ project: string;
+}
+
interface LocalizeInfo {
key: string;
comment: string[];
@@ -52,6 +68,205 @@ 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 => {
+ let element = value[key];
+ return Is.string(element) || (Is.object(element) && Is.defined(element.message) && Is.defined(element.comment));
+ });
+ }
+}
+
+interface ModuleJsonFormat {
+ messages: string[];
+ keys: (string | LocalizeInfo)[];
+}
+
+module ModuleJsonFormat {
+ export function is(value: any): value is ModuleJsonFormat {
+ let candidate = value as ModuleJsonFormat;
+ return Is.defined(candidate)
+ && Is.array(candidate.messages) && candidate.messages.every(message => Is.string(message))
+ && Is.array(candidate.keys) && candidate.keys.every(key => Is.string(key) || LocalizeInfo.is(key));
+ }
+}
+
+export class Line {
+ private buffer: string[] = [];
+
+ constructor(private indent: number = 0) {
+ if (indent > 0) {
+ this.buffer.push(new Array(indent + 1).join(' '));
+ }
+ }
+
+ public append(value: string): Line {
+ this.buffer.push(value);
+ return this;
+ }
+
+ public toString(): string {
+ return this.buffer.join('');
+ }
+}
+
+class TextModel {
+ private _lines: string[];
+
+ constructor(contents: string) {
+ this._lines = contents.split(/\r\n|\r|\n/);
+ }
+
+ public get lines(): string[] {
+ return this._lines;
+ }
+}
+
+export class XLF {
+ private buffer: string[];
+ private files: Map- ;
+
+ constructor(public project: string) {
+ this.buffer = [];
+ this.files = Object.create(null);
+ }
+
+ public toString(): string {
+ this.appendHeader();
+
+ for (let file in this.files) {
+ this.appendNewLine(``, 2);
+ for (let item of this.files[file]) {
+ this.addStringItem(item);
+ }
+ this.appendNewLine('', 2);
+ }
+
+ this.appendFooter();
+ return this.buffer.join('\r\n');
+ }
+
+ public addFile(original: string, keys: any[], messages: string[]) {
+ this.files[original] = [];
+ let existingKeys = [];
+
+ for (let key of keys) {
+ // Ignore duplicate keys because Transifex does not populate those with translated values.
+ if (existingKeys.indexOf(key) !== -1) {
+ continue;
+ }
+ existingKeys.push(key);
+
+ let message: string = encodeEntities(messages[keys.indexOf(key)]);
+ let comment: string = undefined;
+
+ // Check if the message contains description (if so, it becomes an object type in JSON)
+ if (Is.string(key)) {
+ this.files[original].push({ id: key, message: message, comment: comment });
+ } else {
+ if (key['comment'] && key['comment'].length > 0) {
+ comment = key['comment'].map(comment => encodeEntities(comment)).join('\r\n');
+ }
+
+ this.files[original].push({ id: key['key'], message: message, comment: comment });
+ }
+ }
+ }
+
+ private addStringItem(item: Item): void {
+ if (!item.id || !item.message) {
+ //throw new Error('No item ID or value specified.');
+ }
+
+ this.appendNewLine(``, 4);
+ this.appendNewLine(`${encodeEntities(item.message)}`, 6);
+
+ if (item.comment) {
+ this.appendNewLine(`${item.comment}`, 6);
+ }
+
+ this.appendNewLine('', 4);
+ }
+
+ private appendHeader(): void {
+ this.appendNewLine('', 0);
+ this.appendNewLine('', 0);
+ }
+
+ private appendFooter(): void {
+ this.appendNewLine('', 0);
+ }
+
+ private appendNewLine(content: string, indent?: number): void {
+ let line = new Line(indent);
+ line.append(content);
+ this.buffer.push(line.toString());
+ }
+
+ static parse = function(xlfString: string) : Promise<{ messages: Map, originalFilePath: string, language: string }[]> {
+ return new Promise((resolve, reject) => {
+ let parser = new xml2js.Parser();
+
+ let files: { messages: Map, originalFilePath: string, language: string }[] = [];
+
+ parser.parseString(xlfString, function(err, result) {
+ if (err) {
+ reject(`Failed to parse XLIFF string. ${err}`);
+ }
+
+ const fileNodes: any[] = result['xliff']['file'];
+ if (!fileNodes) {
+ reject('XLIFF file does not contain "xliff" or "file" node(s) required for parsing.');
+ }
+
+ fileNodes.forEach((file) => {
+ const originalFilePath = file.$.original;
+ if (!originalFilePath) {
+ reject('XLIFF file node does not contain original attribute to determine the original location of the resource file.');
+ }
+ const language = file.$['target-language'].toLowerCase();
+ if (!language) {
+ reject('XLIFF file node does not contain target-language attribute to determine translated language.');
+ }
+
+ let messages: Map = {};
+ const transUnits = file.body[0]['trans-unit'];
+
+ transUnits.forEach(unit => {
+ const key = unit.$.id;
+ if (!unit.target) {
+ return; // No translation available
+ }
+
+ const val = unit.target.toString();
+ if (key && val) {
+ messages[key] = decodeEntities(val);
+ } else {
+ reject('XLIFF file does not contain full localization data. ID or target translation for one of the trans-unit nodes is not present.');
+ }
+ });
+
+ files.push({ messages: messages, originalFilePath: originalFilePath, language: language });
+ });
+
+ resolve(files);
+ });
+ });
+ };
+}
+
const vscodeLanguages: string[] = [
'chs',
'cht',
@@ -85,6 +300,26 @@ const iso639_3_to_2: Map = {
'trk': 'tr'
};
+const iso639_2_to_3: Map = {
+ 'zh-cn': 'chs',
+ 'zh-tw': 'cht',
+ 'cs-cz': 'csy',
+ 'de': 'deu',
+ 'en': 'enu',
+ 'es': 'esn',
+ 'fr': 'fra',
+ 'hu': 'hun',
+ 'it': 'ita',
+ 'ja': 'jpn',
+ 'ko': 'kor',
+ 'nl': 'nld',
+ 'pl': 'plk',
+ 'pt-br': 'ptb',
+ 'pt': 'ptg',
+ 'ru': 'rus',
+ 'sv-se': 'sve',
+ 'tr': 'trk'
+};
interface IDirectoryInfo {
name: string;
iso639_2: string;
@@ -138,7 +373,7 @@ function stripComments(content: string): string {
}
});
return result;
-};
+}
function escapeCharacters(value:string):string {
var result:string[] = [];
@@ -308,4 +543,509 @@ export function processNlsFiles(opts:{fileHeader:string;}): ThroughStream {
}
this.emit('data', file);
});
+}
+
+export function prepareXlfFiles(projectName?: string, extensionName?: string): ThroughStream {
+ return through(
+ function (file: File) {
+ if (!file.isBuffer()) {
+ log('Error', `Failed to read component file: ${file.relative}`);
+ }
+
+ const extension = path.extname(file.path);
+ if (extension === '.json') {
+ const json = JSON.parse((file.contents).toString('utf8'));
+
+ if (BundledFormat.is(json)) {
+ importBundleJson(file, json, this);
+ } else if (PackageJsonFormat.is(json) || ModuleJsonFormat.is(json)) {
+ importModuleOrPackageJson(file, json, projectName, this, extensionName);
+ } else {
+ log('Error', 'JSON format cannot be deduced.');
+ }
+ } else if (extension === '.isl') {
+ importIsl(file, this);
+ }
+ }
+ );
+}
+
+function getResource(sourceFile: string): Resource {
+ const editorProject: string = 'vscode-editor',
+ workbenchProject: string = 'vscode-workbench';
+ let resource: string;
+
+ if (sourceFile.startsWith('vs/platform')) {
+ return { name: 'vs/platform', project: editorProject };
+ } else if (sourceFile.startsWith('vs/editor/contrib')) {
+ return { name: 'vs/editor/contrib', project: editorProject };
+ } else if (sourceFile.startsWith('vs/editor')) {
+ return { name: 'vs/editor', project: editorProject };
+ } else if (sourceFile.startsWith('vs/base')) {
+ return { name: 'vs/base', project: editorProject };
+ } else if (sourceFile.startsWith('vs/code')) {
+ return { name: 'vs/code', project: workbenchProject };
+ } else if (sourceFile.startsWith('vs/workbench/parts')) {
+ resource = sourceFile.split('/', 4).join('/');
+ return { name: resource, project: workbenchProject };
+ } else if (sourceFile.startsWith('vs/workbench/services')) {
+ resource = sourceFile.split('/', 4).join('/');
+ return { name: resource, project: workbenchProject };
+ } else if (sourceFile.startsWith('vs/workbench')) {
+ return { name: 'vs/workbench', project: workbenchProject };
+ }
+
+ throw new Error (`Could not identify the XLF bundle for ${sourceFile}`);
+}
+
+
+function importBundleJson(file: File, json: BundledFormat, stream: ThroughStream): void {
+ let transifexEditorXlfs: Map = Object.create(null);
+
+ for (let source in json.keys) {
+ const projectResource = getResource(source);
+ const resource = projectResource.name;
+ const project = projectResource.project;
+
+ const keys = json.keys[source];
+ const messages = json.messages[source];
+ if (keys.length !== messages.length) {
+ log('Error:', `There is a mismatch between keys and messages in ${file.relative}`);
+ }
+
+ let xlf = transifexEditorXlfs[resource] ? transifexEditorXlfs[resource] : transifexEditorXlfs[resource] = new XLF(project);
+ xlf.addFile(source, keys, messages);
+ }
+
+ for (let resource in transifexEditorXlfs) {
+ const newFilePath = `${transifexEditorXlfs[resource].project}/${resource.replace(/\//g, '_')}.xlf`;
+ const xlfFile = new File({ path: newFilePath, contents: new Buffer(transifexEditorXlfs[resource].toString(), 'utf-8')});
+ stream.emit('data', xlfFile);
+ }
+}
+
+// Keeps existing XLF instances and a state of how many files were already processed for faster file emission
+var extensions: Map<{ xlf: XLF, processed: number }> = Object.create(null);
+function importModuleOrPackageJson(file: File, json: ModuleJsonFormat | PackageJsonFormat, projectName: string, stream: ThroughStream, extensionName?: string): void {
+ if (ModuleJsonFormat.is(json) && json['keys'].length !== json['messages'].length) {
+ log('Error:', `There is a mismatch between keys and messages in ${file.relative}`);
+ }
+
+ // Prepare the source path for attribute in XLF & extract messages from JSON
+ const formattedSourcePath = file.relative.replace(/\\/g, '/');
+ const messages = Object.keys(json).map((key) => json[key].toString());
+
+ // Stores the amount of localization files to be transformed to XLF before the emission
+ let localizationFilesCount,
+ originalFilePath;
+ // If preparing XLF for external extension, then use different glob pattern and source path
+ if (extensionName) {
+ localizationFilesCount = glob.sync('**/*.nls.json').length;
+ originalFilePath = `${formattedSourcePath.substr(0, formattedSourcePath.length - '.nls.json'.length)}`;
+ } else {
+ // Used for vscode/extensions folder
+ extensionName = formattedSourcePath.split('/')[0];
+ localizationFilesCount = glob.sync(`./extensions/${extensionName}/**/*.nls.json`).length;
+ originalFilePath = `extensions/${formattedSourcePath.substr(0, formattedSourcePath.length - '.nls.json'.length)}`;
+ }
+
+ let extension = extensions[extensionName] ?
+ extensions[extensionName] : extensions[extensionName] = { xlf: new XLF(projectName), processed: 0 };
+
+ if (ModuleJsonFormat.is(json)) {
+ extension.xlf.addFile(originalFilePath, json['keys'], json['messages']);
+ } else {
+ extension.xlf.addFile(originalFilePath, Object.keys(json), messages);
+ }
+
+ // Check if XLF is populated with file nodes to emit it
+ if (++extensions[extensionName].processed === localizationFilesCount) {
+ const newFilePath = path.join(projectName, extensionName + '.xlf');
+ const xlfFile = new File({ path: newFilePath, contents: new Buffer(extension.xlf.toString(), 'utf-8')});
+ stream.emit('data', xlfFile);
+ }
+}
+
+var islXlf: XLF,
+ islProcessed: number = 0;
+
+function importIsl(file: File, stream: ThroughStream) {
+ const islFiles = ['Default.isl', 'messages.en.isl'];
+ const projectName = 'vscode-workbench';
+
+ let xlf = islXlf ? islXlf : islXlf = new XLF(projectName),
+ keys: string[] = [],
+ messages: string[] = [];
+
+ let model = new TextModel(file.contents.toString());
+ let inMessageSection = false;
+ model.lines.forEach(line => {
+ if (line.length === 0) {
+ return;
+ }
+ let firstChar = line.charAt(0);
+ switch (firstChar) {
+ case ';':
+ // Comment line;
+ return;
+ case '[':
+ inMessageSection = '[Messages]' === line || '[CustomMessages]' === line;
+ return;
+ }
+ if (!inMessageSection) {
+ return;
+ }
+ let sections: string[] = line.split('=');
+ if (sections.length !== 2) {
+ log('Error:', `Badly formatted message found: ${line}`);
+ } else {
+ let key = sections[0];
+ let value = sections[1];
+ if (key.length > 0 && value.length > 0) {
+ keys.push(key);
+ messages.push(value);
+ }
+ }
+ });
+
+ const originalPath = file.path.substring(file.cwd.length+1, file.path.split('.')[0].length).replace(/\\/g, '/');
+ xlf.addFile(originalPath, keys, messages);
+
+ // Emit only upon all ISL files combined into single XLF instance
+ if (++islProcessed === islFiles.length) {
+ const newFilePath = path.join(projectName, 'setup.xlf');
+ const xlfFile = new File({ path: newFilePath, contents: new Buffer(xlf.toString(), 'utf-8')});
+ stream.emit('data', xlfFile);
+ }
+}
+
+export function pushXlfFiles(apiUrl: string, username: string, password: string): ThroughStream {
+ return through(function(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 = {
+ 'user': username,
+ 'password': password
+ };
+
+ // Check if resource already exists, if not, then create it.
+ tryGetResource(project, slug, apiUrl, credentials).then(exists => {
+ if (exists) {
+ updateResource(project, slug, file, apiUrl, credentials);
+ } else {
+ createResource(project, slug, file, apiUrl, credentials);
+ }
+ });
+ });
+}
+
+function tryGetResource(project: string, slug: string, apiUrl: string, credentials: any): Promise {
+ return new Promise((resolve, reject) => {
+ const url = `${apiUrl}/project/${project}/resource/${slug}/?details`;
+ request.get(url, { 'auth': credentials }).on('response', function (response) {
+ if (response.statusCode === 404) {
+ resolve(false);
+ } else if (response.statusCode === 200) {
+ resolve(true);
+ } else {
+ reject(`Failed to query resource ${slug}. Response: ${response.statusCode} ${response.statusMessage}`);
+ }
+ });
+ });
+}
+
+function createResource(project: string, slug: string, xlfFile: File, apiUrl:string, credentials: any): void {
+ const url = `${apiUrl}/project/${project}/resources`;
+ const options = {
+ 'body': {
+ 'content': xlfFile.contents.toString(),
+ 'name': slug,
+ 'slug': slug,
+ 'i18n_type': 'XLIFF'
+ },
+ 'json': true,
+ 'auth': credentials
+ };
+
+ request.post(url, options, function(err, res) {
+ if (err) {
+ log('Error:', `Failed to create Transifex ${project}/${slug}: ${err}`);
+ }
+
+ if (res.statusCode === 201) {
+ log(`Resource ${project}/${slug} successfully created on Transifex.`);
+ } else {
+ log('Error:', `Something went wrong creating ${slug} in ${project}. ${res.statusCode}`);
+ }
+ });
+}
+
+/**
+ * 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, apiUrl: string, credentials: any) : void {
+ const url = `${apiUrl}/project/${project}/resource/${slug}/content`;
+ const options = {
+ 'body': { 'content': xlfFile.contents.toString() },
+ 'json': true,
+ 'auth': credentials
+ };
+
+ request.put(url, options, function(err, res, body) {
+ if (err) {
+ log('Error:', `Failed to update Transifex ${project}/${slug}: ${err}`);
+ }
+
+ if (res.statusCode === 200) {
+ log(`Resource ${project}/${slug} successfully updated on Transifex. Strings added: ${body['strings_added']}, updated: ${body['strings_updated']}, deleted: ${body['strings_delete']}`);
+ } else {
+ log('Error:', `Something went wrong updating ${slug} in ${project}. ${res.statusCode}`);
+ }
+ });
+}
+
+function getMetadataResources(pathToMetadata: string) : Resource[] {
+ const metadata = fs.readFileSync(pathToMetadata).toString('utf8');
+ const json = JSON.parse(metadata);
+ let slugs = [];
+
+ for (let source in json['keys']) {
+ let projectResource = getResource(source);
+ if (!slugs.find(slug => slug.name === projectResource.name && slug.project === projectResource.project)) {
+ slugs.push(projectResource);
+ }
+ }
+
+ return slugs;
+}
+
+function obtainProjectResources(projectName: string): Resource[] {
+ let resources: Resource[];
+
+ if (projectName === 'vscode-editor-workbench') {
+ resources = getMetadataResources('./out-vscode/nls.metadata.json');
+ resources.push({ name: 'setup', project: 'vscode-workbench' });
+ } else if (projectName === 'vscode-extensions') {
+ let extensionsToLocalize: string[] = glob.sync('./extensions/**/*.nls.json').map(extension => extension.split('/')[2]);
+ let resourcesToPull: string[] = [];
+ resources = [];
+
+ extensionsToLocalize.forEach(extension => {
+ if (resourcesToPull.indexOf(extension) === -1) { // remove duplicate elements returned by glob
+ resourcesToPull.push(extension);
+ resources.push({ name: extension, project: projectName });
+ }
+ });
+ }
+
+ return resources;
+}
+
+export function pullXlfFiles(projectName: string, apiUrl: string, username: string, password: string, resources?: Resource[]): NodeJS.ReadableStream {
+ if (!resources) {
+ resources = obtainProjectResources(projectName);
+ }
+ if (!resources) {
+ throw new Error('Transifex projects and resources must be defined to be able to pull translations from Transifex.');
+ }
+
+ const credentials = {
+ 'auth': {
+ 'user': username,
+ 'password': password
+ }
+ };
+ let expectedTranslationsCount = vscodeLanguages.length * resources.length;
+ let translationsRetrieved = 0, called = false;
+
+ return es.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;
+
+ vscodeLanguages.map(function(language) {
+ resources.map(function (resource) {
+ const slug = resource.name.replace(/\//g, '_');
+ const project = resource.project;
+ const iso639 = iso639_3_to_2[language];
+ const url = `${apiUrl}/project/${project}/resource/${slug}/translation/${iso639}?file&mode=onlyreviewed`;
+
+ let xlfBuffer: string = '', responseCode: number;
+ request.get(url, credentials)
+ .on('response', (response) => {
+ responseCode = response.statusCode;
+ })
+ .on('data', (data) => xlfBuffer += data)
+ .on('end', () => {
+ if (responseCode === 200) {
+ stream.emit('data', new File({ contents: new Buffer(xlfBuffer) }));
+ } else {
+ log('Error:', `${slug} in ${project} returned no data. Response code: ${responseCode}.`);
+ }
+ translationsRetrieved++;
+ })
+ .on('error', (error) => {
+ log('Error:', `Failed to query resource ${slug} with the following error: ${error}`);
+ });
+ });
+ });
+ }
+
+ callback();
+ });
+}
+
+export function prepareJsonFiles(): ThroughStream {
+ return through(function(xlf: File) {
+ let stream = this;
+
+ XLF.parse(xlf.contents.toString()).then(
+ function(resolvedFiles) {
+ resolvedFiles.forEach(file => {
+ let messages = file.messages, translatedFile;
+
+ // ISL file path always starts with 'build/'
+ if (file.originalFilePath.startsWith('build/')) {
+ const defaultLanguages = { 'zh-cn': true, 'zh-tw': true, 'ko': true };
+ if (path.basename(file.originalFilePath) === 'Default' && !defaultLanguages[file.language]) {
+ return;
+ }
+
+ translatedFile = createIslFile('..', file.originalFilePath, messages, iso639_2_to_3[file.language]);
+ } else {
+ translatedFile = createI18nFile(iso639_2_to_3[file.language], file.originalFilePath, messages);
+ }
+
+ stream.emit('data', translatedFile);
+ });
+ },
+ function(rejectReason) {
+ log('Error:', rejectReason);
+ }
+ );
+ });
+}
+
+export function createI18nFile(base: string, originalFilePath: string, messages: Map): File {
+ let content = [
+ '/*---------------------------------------------------------------------------------------------',
+ ' * Copyright (c) Microsoft Corporation. All rights reserved.',
+ ' * Licensed under the MIT License. See License.txt in the project root for license information.',
+ ' *--------------------------------------------------------------------------------------------*/',
+ '// Do not edit this file. It is machine generated.'
+ ].join('\n') + '\n' + JSON.stringify(messages, null, '\t').replace(/\r\n/g, '\n');
+
+ return new File({
+ path: path.join(base, originalFilePath + '.i18n.json'),
+ contents: new Buffer(content, 'utf8')
+ });
+}
+
+
+const languageNames: Map = {
+ 'chs': 'Simplified Chinese',
+ 'cht': 'Traditional Chinese',
+ 'kor': 'Korean'
+};
+
+const languageIds: Map = {
+ 'chs': '$0804',
+ 'cht': '$0404',
+ 'kor': '$0412'
+};
+
+const encodings: Map = {
+ 'chs': 'CP936',
+ 'cht': 'CP950',
+ 'jpn': 'CP932',
+ 'kor': 'CP949',
+ 'deu': 'CP1252',
+ 'fra': 'CP1252',
+ 'esn': 'CP1252',
+ 'rus': 'CP1251',
+ 'ita': 'CP1252'
+};
+
+export function createIslFile(base: string, originalFilePath: string, messages: Map, language: string): File {
+ let content: string[] = [];
+ let originalContent: TextModel;
+ if (path.basename(originalFilePath) === 'Default') {
+ originalContent = new TextModel(fs.readFileSync(originalFilePath + '.isl', 'utf8'));
+ } else {
+ originalContent = new TextModel(fs.readFileSync(originalFilePath + '.en.isl', 'utf8'));
+ }
+
+ originalContent.lines.forEach(line => {
+ if (line.length > 0) {
+ let firstChar = line.charAt(0);
+ if (firstChar === '[' || firstChar === ';') {
+ if (line === '; *** Inno Setup version 5.5.3+ English messages ***') {
+ content.push(`; *** Inno Setup version 5.5.3+ ${languageNames[language]} messages ***`);
+ } else {
+ content.push(line);
+ }
+ } else {
+ let sections: string[] = line.split('=');
+ let key = sections[0];
+ let translated = line;
+ if (key) {
+ if (key === 'LanguageName') {
+ translated = `${key}=${languageNames[language]}`;
+ } else if (key === 'LanguageID') {
+ translated = `${key}=${languageIds[language]}`;
+ } else if (key === 'LanguageCodePage') {
+ translated = `${key}=${encodings[language].substr(2)}`;
+ } else {
+ let translatedMessage = messages[key];
+ if (translatedMessage) {
+ translated = `${key}=${translatedMessage}`;
+ }
+ }
+ }
+
+ content.push(translated);
+ }
+ }
+ });
+
+ let tag = iso639_3_to_2[language];
+ let basename = path.basename(originalFilePath);
+ let filePath = `${path.join(base, path.dirname(originalFilePath), basename)}.${tag}.isl`;
+
+ return new File({
+ path: filePath,
+ contents: iconv.encode(new Buffer(content.join('\r\n'), 'utf8'), encodings[language])
+ });
+}
+
+function encodeEntities(value: string): string {
+ var result: string[] = [];
+ for (var i = 0; i < value.length; i++) {
+ var ch = value[i];
+ switch (ch) {
+ case '<':
+ result.push('<');
+ break;
+ case '>':
+ result.push('>');
+ break;
+ case '&':
+ result.push('&');
+ break;
+ default:
+ result.push(ch);
+ }
+ }
+ return result.join('');
+}
+
+export function decodeEntities(value:string): string {
+ return value.replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&');
}
\ No newline at end of file
diff --git a/extensions/typescript/package.json b/extensions/typescript/package.json
index c5721ef1c0c..d93d295c725 100644
--- a/extensions/typescript/package.json
+++ b/extensions/typescript/package.json
@@ -403,4 +403,4 @@
}
]
}
-}
\ No newline at end of file
+}