Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/build/lib/i18n.ts
3520 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import path from 'path';
7
import fs from 'fs';
8
9
import { map, merge, through, ThroughStream } from 'event-stream';
10
import jsonMerge from 'gulp-merge-json';
11
import File from 'vinyl';
12
import xml2js from 'xml2js';
13
import gulp from 'gulp';
14
import fancyLog from 'fancy-log';
15
import ansiColors from 'ansi-colors';
16
import iconv from '@vscode/iconv-lite-umd';
17
import { l10nJsonFormat, getL10nXlf, l10nJsonDetails, getL10nFilesFromXlf, getL10nJson } from '@vscode/l10n-dev';
18
19
const REPO_ROOT_PATH = path.join(__dirname, '../..');
20
21
function log(message: any, ...rest: any[]): void {
22
fancyLog(ansiColors.green('[i18n]'), message, ...rest);
23
}
24
25
export interface Language {
26
id: string; // language id, e.g. zh-tw, de
27
translationId?: string; // language id used in translation tools, e.g. zh-hant, de (optional, if not set, the id is used)
28
folderName?: string; // language specific folder name, e.g. cht, deu (optional, if not set, the id is used)
29
}
30
31
export interface InnoSetup {
32
codePage: string; //code page for encoding (http://www.jrsoftware.org/ishelp/index.php?topic=langoptionssection)
33
}
34
35
export const defaultLanguages: Language[] = [
36
{ id: 'zh-tw', folderName: 'cht', translationId: 'zh-hant' },
37
{ id: 'zh-cn', folderName: 'chs', translationId: 'zh-hans' },
38
{ id: 'ja', folderName: 'jpn' },
39
{ id: 'ko', folderName: 'kor' },
40
{ id: 'de', folderName: 'deu' },
41
{ id: 'fr', folderName: 'fra' },
42
{ id: 'es', folderName: 'esn' },
43
{ id: 'ru', folderName: 'rus' },
44
{ id: 'it', folderName: 'ita' }
45
];
46
47
// languages requested by the community to non-stable builds
48
export const extraLanguages: Language[] = [
49
{ id: 'pt-br', folderName: 'ptb' },
50
{ id: 'hu', folderName: 'hun' },
51
{ id: 'tr', folderName: 'trk' }
52
];
53
54
interface Item {
55
id: string;
56
message: string;
57
comment?: string;
58
}
59
60
export interface Resource {
61
name: string;
62
project: string;
63
}
64
65
interface LocalizeInfo {
66
key: string;
67
comment: string[];
68
}
69
70
module LocalizeInfo {
71
export function is(value: any): value is LocalizeInfo {
72
const candidate = value as LocalizeInfo;
73
return candidate && typeof candidate.key === 'string' && (candidate.comment === undefined || (Array.isArray(candidate.comment) && candidate.comment.every(element => typeof element === 'string')));
74
}
75
}
76
77
interface BundledFormat {
78
keys: Record<string, (string | LocalizeInfo)[]>;
79
messages: Record<string, string[]>;
80
bundles: Record<string, string[]>;
81
}
82
83
module BundledFormat {
84
export function is(value: any): value is BundledFormat {
85
if (value === undefined) {
86
return false;
87
}
88
89
const candidate = value as BundledFormat;
90
const length = Object.keys(value).length;
91
92
return length === 3 && !!candidate.keys && !!candidate.messages && !!candidate.bundles;
93
}
94
}
95
96
type NLSKeysFormat = [string /* module ID */, string[] /* keys */];
97
98
module NLSKeysFormat {
99
export function is(value: any): value is NLSKeysFormat {
100
if (value === undefined) {
101
return false;
102
}
103
104
const candidate = value as NLSKeysFormat;
105
return Array.isArray(candidate) && Array.isArray(candidate[1]);
106
}
107
}
108
109
interface BundledExtensionFormat {
110
[key: string]: {
111
messages: string[];
112
keys: (string | LocalizeInfo)[];
113
};
114
}
115
116
interface I18nFormat {
117
version: string;
118
contents: {
119
[module: string]: {
120
[messageKey: string]: string;
121
};
122
};
123
}
124
125
export class Line {
126
private buffer: string[] = [];
127
128
constructor(indent: number = 0) {
129
if (indent > 0) {
130
this.buffer.push(new Array(indent + 1).join(' '));
131
}
132
}
133
134
public append(value: string): Line {
135
this.buffer.push(value);
136
return this;
137
}
138
139
public toString(): string {
140
return this.buffer.join('');
141
}
142
}
143
144
class TextModel {
145
private _lines: string[];
146
147
constructor(contents: string) {
148
this._lines = contents.split(/\r\n|\r|\n/);
149
}
150
151
public get lines(): string[] {
152
return this._lines;
153
}
154
}
155
156
export class XLF {
157
private buffer: string[];
158
private files: Record<string, Item[]>;
159
public numberOfMessages: number;
160
161
constructor(public project: string) {
162
this.buffer = [];
163
this.files = Object.create(null);
164
this.numberOfMessages = 0;
165
}
166
167
public toString(): string {
168
this.appendHeader();
169
170
const files = Object.keys(this.files).sort();
171
for (const file of files) {
172
this.appendNewLine(`<file original="${file}" source-language="en" datatype="plaintext"><body>`, 2);
173
const items = this.files[file].sort((a: Item, b: Item) => {
174
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
175
});
176
for (const item of items) {
177
this.addStringItem(file, item);
178
}
179
this.appendNewLine('</body></file>');
180
}
181
this.appendFooter();
182
return this.buffer.join('\r\n');
183
}
184
185
public addFile(original: string, keys: (string | LocalizeInfo)[], messages: string[]) {
186
if (keys.length === 0) {
187
console.log('No keys in ' + original);
188
return;
189
}
190
if (keys.length !== messages.length) {
191
throw new Error(`Unmatching keys(${keys.length}) and messages(${messages.length}).`);
192
}
193
this.numberOfMessages += keys.length;
194
this.files[original] = [];
195
const existingKeys = new Set<string>();
196
for (let i = 0; i < keys.length; i++) {
197
const key = keys[i];
198
let realKey: string | undefined;
199
let comment: string | undefined;
200
if (typeof key === 'string') {
201
realKey = key;
202
comment = undefined;
203
} else if (LocalizeInfo.is(key)) {
204
realKey = key.key;
205
if (key.comment && key.comment.length > 0) {
206
comment = key.comment.map(comment => encodeEntities(comment)).join('\r\n');
207
}
208
}
209
if (!realKey || existingKeys.has(realKey)) {
210
continue;
211
}
212
existingKeys.add(realKey);
213
const message: string = encodeEntities(messages[i]);
214
this.files[original].push({ id: realKey, message: message, comment: comment });
215
}
216
}
217
218
private addStringItem(file: string, item: Item): void {
219
if (!item.id || item.message === undefined || item.message === null) {
220
throw new Error(`No item ID or value specified: ${JSON.stringify(item)}. File: ${file}`);
221
}
222
if (item.message.length === 0) {
223
log(`Item with id ${item.id} in file ${file} has an empty message.`);
224
}
225
226
this.appendNewLine(`<trans-unit id="${item.id}">`, 4);
227
this.appendNewLine(`<source xml:lang="en">${item.message}</source>`, 6);
228
229
if (item.comment) {
230
this.appendNewLine(`<note>${item.comment}</note>`, 6);
231
}
232
233
this.appendNewLine('</trans-unit>', 4);
234
}
235
236
private appendHeader(): void {
237
this.appendNewLine('<?xml version="1.0" encoding="utf-8"?>', 0);
238
this.appendNewLine('<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">', 0);
239
}
240
241
private appendFooter(): void {
242
this.appendNewLine('</xliff>', 0);
243
}
244
245
private appendNewLine(content: string, indent?: number): void {
246
const line = new Line(indent);
247
line.append(content);
248
this.buffer.push(line.toString());
249
}
250
251
static parse = function (xlfString: string): Promise<l10nJsonDetails[]> {
252
return new Promise((resolve, reject) => {
253
const parser = new xml2js.Parser();
254
255
const files: { messages: Record<string, string>; name: string; language: string }[] = [];
256
257
parser.parseString(xlfString, function (err: any, result: any) {
258
if (err) {
259
reject(new Error(`XLF parsing error: Failed to parse XLIFF string. ${err}`));
260
}
261
262
const fileNodes: any[] = result['xliff']['file'];
263
if (!fileNodes) {
264
reject(new Error(`XLF parsing error: XLIFF file does not contain "xliff" or "file" node(s) required for parsing.`));
265
}
266
267
fileNodes.forEach((file) => {
268
const name = file.$.original;
269
if (!name) {
270
reject(new Error(`XLF parsing error: XLIFF file node does not contain original attribute to determine the original location of the resource file.`));
271
}
272
const language = file.$['target-language'];
273
if (!language) {
274
reject(new Error(`XLF parsing error: XLIFF file node does not contain target-language attribute to determine translated language.`));
275
}
276
const messages: Record<string, string> = {};
277
278
const transUnits = file.body[0]['trans-unit'];
279
if (transUnits) {
280
transUnits.forEach((unit: any) => {
281
const key = unit.$.id;
282
if (!unit.target) {
283
return; // No translation available
284
}
285
286
let val = unit.target[0];
287
if (typeof val !== 'string') {
288
// We allow empty source values so support them for translations as well.
289
val = val._ ? val._ : '';
290
}
291
if (!key) {
292
reject(new Error(`XLF parsing error: trans-unit ${JSON.stringify(unit, undefined, 0)} defined in file ${name} is missing the ID attribute.`));
293
return;
294
}
295
messages[key] = decodeEntities(val);
296
});
297
files.push({ messages, name, language: language.toLowerCase() });
298
}
299
});
300
301
resolve(files);
302
});
303
});
304
};
305
}
306
307
function sortLanguages(languages: Language[]): Language[] {
308
return languages.sort((a: Language, b: Language): number => {
309
return a.id < b.id ? -1 : (a.id > b.id ? 1 : 0);
310
});
311
}
312
313
function stripComments(content: string): string {
314
// Copied from stripComments.js
315
//
316
// First group matches a double quoted string
317
// Second group matches a single quoted string
318
// Third group matches a multi line comment
319
// Forth group matches a single line comment
320
// Fifth group matches a trailing comma
321
const regexp = /("[^"\\]*(?:\\.[^"\\]*)*")|('[^'\\]*(?:\\.[^'\\]*)*')|(\/\*[^\/\*]*(?:(?:\*|\/)[^\/\*]*)*?\*\/)|(\/{2,}.*?(?:(?:\r?\n)|$))|(,\s*[}\]])/g;
322
const result = content.replace(regexp, (match, _m1: string, _m2: string, m3: string, m4: string, m5: string) => {
323
// Only one of m1, m2, m3, m4, m5 matches
324
if (m3) {
325
// A block comment. Replace with nothing
326
return '';
327
} else if (m4) {
328
// Since m4 is a single line comment is is at least of length 2 (e.g. //)
329
// If it ends in \r?\n then keep it.
330
const length = m4.length;
331
if (m4[length - 1] === '\n') {
332
return m4[length - 2] === '\r' ? '\r\n' : '\n';
333
} else {
334
return '';
335
}
336
} else if (m5) {
337
// Remove the trailing comma
338
return match.substring(1);
339
} else {
340
// We match a string
341
return match;
342
}
343
});
344
return result;
345
}
346
347
function processCoreBundleFormat(base: string, fileHeader: string, languages: Language[], json: NLSKeysFormat, emitter: ThroughStream) {
348
const languageDirectory = path.join(REPO_ROOT_PATH, '..', 'vscode-loc', 'i18n');
349
if (!fs.existsSync(languageDirectory)) {
350
log(`No VS Code localization repository found. Looking at ${languageDirectory}`);
351
log(`To bundle translations please check out the vscode-loc repository as a sibling of the vscode repository.`);
352
}
353
const sortedLanguages = sortLanguages(languages);
354
sortedLanguages.forEach((language) => {
355
if (process.env['VSCODE_BUILD_VERBOSE']) {
356
log(`Generating nls bundles for: ${language.id}`);
357
}
358
359
const languageFolderName = language.translationId || language.id;
360
const i18nFile = path.join(languageDirectory, `vscode-language-pack-${languageFolderName}`, 'translations', 'main.i18n.json');
361
let allMessages: I18nFormat | undefined;
362
if (fs.existsSync(i18nFile)) {
363
const content = stripComments(fs.readFileSync(i18nFile, 'utf8'));
364
allMessages = JSON.parse(content);
365
}
366
367
let nlsIndex = 0;
368
const nlsResult: Array<string | undefined> = [];
369
for (const [moduleId, nlsKeys] of json) {
370
const moduleTranslations = allMessages?.contents[moduleId];
371
for (const nlsKey of nlsKeys) {
372
nlsResult.push(moduleTranslations?.[nlsKey]); // pushing `undefined` is fine, as we keep english strings as fallback for monaco editor in the build
373
nlsIndex++;
374
}
375
}
376
377
emitter.queue(new File({
378
contents: Buffer.from(`${fileHeader}
379
globalThis._VSCODE_NLS_MESSAGES=${JSON.stringify(nlsResult)};
380
globalThis._VSCODE_NLS_LANGUAGE=${JSON.stringify(language.id)};`),
381
base,
382
path: `${base}/nls.messages.${language.id}.js`
383
}));
384
});
385
}
386
387
export function processNlsFiles(opts: { out: string; fileHeader: string; languages: Language[] }): ThroughStream {
388
return through(function (this: ThroughStream, file: File) {
389
const fileName = path.basename(file.path);
390
if (fileName === 'nls.keys.json') {
391
try {
392
const contents = file.contents!.toString('utf8');
393
const json = JSON.parse(contents);
394
if (NLSKeysFormat.is(json)) {
395
processCoreBundleFormat(file.base, opts.fileHeader, opts.languages, json, this);
396
}
397
} catch (error) {
398
this.emit('error', `Failed to read component file: ${error}`);
399
}
400
}
401
this.queue(file);
402
});
403
}
404
405
const editorProject: string = 'vscode-editor',
406
workbenchProject: string = 'vscode-workbench',
407
extensionsProject: string = 'vscode-extensions',
408
setupProject: string = 'vscode-setup',
409
serverProject: string = 'vscode-server';
410
411
export function getResource(sourceFile: string): Resource {
412
let resource: string;
413
414
if (/^vs\/platform/.test(sourceFile)) {
415
return { name: 'vs/platform', project: editorProject };
416
} else if (/^vs\/editor\/contrib/.test(sourceFile)) {
417
return { name: 'vs/editor/contrib', project: editorProject };
418
} else if (/^vs\/editor/.test(sourceFile)) {
419
return { name: 'vs/editor', project: editorProject };
420
} else if (/^vs\/base/.test(sourceFile)) {
421
return { name: 'vs/base', project: editorProject };
422
} else if (/^vs\/code/.test(sourceFile)) {
423
return { name: 'vs/code', project: workbenchProject };
424
} else if (/^vs\/server/.test(sourceFile)) {
425
return { name: 'vs/server', project: serverProject };
426
} else if (/^vs\/workbench\/contrib/.test(sourceFile)) {
427
resource = sourceFile.split('/', 4).join('/');
428
return { name: resource, project: workbenchProject };
429
} else if (/^vs\/workbench\/services/.test(sourceFile)) {
430
resource = sourceFile.split('/', 4).join('/');
431
return { name: resource, project: workbenchProject };
432
} else if (/^vs\/workbench/.test(sourceFile)) {
433
return { name: 'vs/workbench', project: workbenchProject };
434
}
435
436
throw new Error(`Could not identify the XLF bundle for ${sourceFile}`);
437
}
438
439
440
export function createXlfFilesForCoreBundle(): ThroughStream {
441
return through(function (this: ThroughStream, file: File) {
442
const basename = path.basename(file.path);
443
if (basename === 'nls.metadata.json') {
444
if (file.isBuffer()) {
445
const xlfs: Record<string, XLF> = Object.create(null);
446
const json: BundledFormat = JSON.parse((file.contents as Buffer).toString('utf8'));
447
for (const coreModule in json.keys) {
448
const projectResource = getResource(coreModule);
449
const resource = projectResource.name;
450
const project = projectResource.project;
451
452
const keys = json.keys[coreModule];
453
const messages = json.messages[coreModule];
454
if (keys.length !== messages.length) {
455
this.emit('error', `There is a mismatch between keys and messages in ${file.relative} for module ${coreModule}`);
456
return;
457
} else {
458
let xlf = xlfs[resource];
459
if (!xlf) {
460
xlf = new XLF(project);
461
xlfs[resource] = xlf;
462
}
463
xlf.addFile(`src/${coreModule}`, keys, messages);
464
}
465
}
466
for (const resource in xlfs) {
467
const xlf = xlfs[resource];
468
const filePath = `${xlf.project}/${resource.replace(/\//g, '_')}.xlf`;
469
const xlfFile = new File({
470
path: filePath,
471
contents: Buffer.from(xlf.toString(), 'utf8')
472
});
473
this.queue(xlfFile);
474
}
475
} else {
476
this.emit('error', new Error(`File ${file.relative} is not using a buffer content`));
477
return;
478
}
479
} else {
480
this.emit('error', new Error(`File ${file.relative} is not a core meta data file.`));
481
return;
482
}
483
});
484
}
485
486
function createL10nBundleForExtension(extensionFolderName: string, prefixWithBuildFolder: boolean): NodeJS.ReadWriteStream {
487
const prefix = prefixWithBuildFolder ? '.build/' : '';
488
return gulp
489
.src([
490
// For source code of extensions
491
`${prefix}extensions/${extensionFolderName}/{src,client,server}/**/*.{ts,tsx}`,
492
// // For any dependencies pulled in (think vscode-css-languageservice or @vscode/emmet-helper)
493
`${prefix}extensions/${extensionFolderName}/**/node_modules/{@vscode,vscode-*}/**/*.{js,jsx}`,
494
// // For any dependencies pulled in that bundle @vscode/l10n. They needed to export the bundle
495
`${prefix}extensions/${extensionFolderName}/**/bundle.l10n.json`,
496
])
497
.pipe(map(function (data, callback) {
498
const file = data as File;
499
if (!file.isBuffer()) {
500
// Not a buffer so we drop it
501
callback();
502
return;
503
}
504
const extension = path.extname(file.relative);
505
if (extension !== '.json') {
506
const contents = file.contents.toString('utf8');
507
getL10nJson([{ contents, extension }])
508
.then((json) => {
509
callback(undefined, new File({
510
path: `extensions/${extensionFolderName}/bundle.l10n.json`,
511
contents: Buffer.from(JSON.stringify(json), 'utf8')
512
}));
513
})
514
.catch((err) => {
515
callback(new Error(`File ${file.relative} threw an error when parsing: ${err}`));
516
});
517
// signal pause?
518
return false;
519
}
520
521
// for bundle.l10n.jsons
522
let bundleJson;
523
try {
524
bundleJson = JSON.parse(file.contents.toString('utf8'));
525
} catch (err) {
526
callback(new Error(`File ${file.relative} threw an error when parsing: ${err}`));
527
return;
528
}
529
530
// some validation of the bundle.l10n.json format
531
for (const key in bundleJson) {
532
if (
533
typeof bundleJson[key] !== 'string' &&
534
(typeof bundleJson[key].message !== 'string' || !Array.isArray(bundleJson[key].comment))
535
) {
536
callback(new Error(`Invalid bundle.l10n.json file. The value for key ${key} is not in the expected format.`));
537
return;
538
}
539
}
540
541
callback(undefined, file);
542
}))
543
.pipe(jsonMerge({
544
fileName: `extensions/${extensionFolderName}/bundle.l10n.json`,
545
jsonSpace: '',
546
concatArrays: true
547
}));
548
}
549
550
export const EXTERNAL_EXTENSIONS = [
551
'ms-vscode.js-debug',
552
'ms-vscode.js-debug-companion',
553
'ms-vscode.vscode-js-profile-table',
554
];
555
556
export function createXlfFilesForExtensions(): ThroughStream {
557
let counter: number = 0;
558
let folderStreamEnded: boolean = false;
559
let folderStreamEndEmitted: boolean = false;
560
return through(function (this: ThroughStream, extensionFolder: File) {
561
const folderStream = this;
562
const stat = fs.statSync(extensionFolder.path);
563
if (!stat.isDirectory()) {
564
return;
565
}
566
const extensionFolderName = path.basename(extensionFolder.path);
567
if (extensionFolderName === 'node_modules') {
568
return;
569
}
570
// Get extension id and use that as the id
571
const manifest = fs.readFileSync(path.join(extensionFolder.path, 'package.json'), 'utf-8');
572
const manifestJson = JSON.parse(manifest);
573
const extensionId = manifestJson.publisher + '.' + manifestJson.name;
574
575
counter++;
576
let _l10nMap: Map<string, l10nJsonFormat>;
577
function getL10nMap() {
578
if (!_l10nMap) {
579
_l10nMap = new Map();
580
}
581
return _l10nMap;
582
}
583
merge(
584
gulp.src([`.build/extensions/${extensionFolderName}/package.nls.json`, `.build/extensions/${extensionFolderName}/**/nls.metadata.json`], { allowEmpty: true }),
585
createL10nBundleForExtension(extensionFolderName, EXTERNAL_EXTENSIONS.includes(extensionId))
586
).pipe(through(function (file: File) {
587
if (file.isBuffer()) {
588
const buffer: Buffer = file.contents as Buffer;
589
const basename = path.basename(file.path);
590
if (basename === 'package.nls.json') {
591
const json: l10nJsonFormat = JSON.parse(buffer.toString('utf8'));
592
getL10nMap().set(`extensions/${extensionId}/package`, json);
593
} else if (basename === 'nls.metadata.json') {
594
const json: BundledExtensionFormat = JSON.parse(buffer.toString('utf8'));
595
const relPath = path.relative(`.build/extensions/${extensionFolderName}`, path.dirname(file.path));
596
for (const file in json) {
597
const fileContent = json[file];
598
const info: l10nJsonFormat = Object.create(null);
599
for (let i = 0; i < fileContent.messages.length; i++) {
600
const message = fileContent.messages[i];
601
const { key, comment } = LocalizeInfo.is(fileContent.keys[i])
602
? fileContent.keys[i] as LocalizeInfo
603
: { key: fileContent.keys[i] as string, comment: undefined };
604
605
info[key] = comment ? { message, comment } : message;
606
}
607
getL10nMap().set(`extensions/${extensionId}/${relPath}/${file}`, info);
608
}
609
} else if (basename === 'bundle.l10n.json') {
610
const json: l10nJsonFormat = JSON.parse(buffer.toString('utf8'));
611
getL10nMap().set(`extensions/${extensionId}/bundle`, json);
612
} else {
613
this.emit('error', new Error(`${file.path} is not a valid extension nls file`));
614
return;
615
}
616
}
617
}, function () {
618
if (_l10nMap?.size > 0) {
619
const xlfFile = new File({
620
path: path.join(extensionsProject, extensionId + '.xlf'),
621
contents: Buffer.from(getL10nXlf(_l10nMap), 'utf8')
622
});
623
folderStream.queue(xlfFile);
624
}
625
this.queue(null);
626
counter--;
627
if (counter === 0 && folderStreamEnded && !folderStreamEndEmitted) {
628
folderStreamEndEmitted = true;
629
folderStream.queue(null);
630
}
631
}));
632
}, function () {
633
folderStreamEnded = true;
634
if (counter === 0) {
635
folderStreamEndEmitted = true;
636
this.queue(null);
637
}
638
});
639
}
640
641
export function createXlfFilesForIsl(): ThroughStream {
642
return through(function (this: ThroughStream, file: File) {
643
let projectName: string,
644
resourceFile: string;
645
if (path.basename(file.path) === 'messages.en.isl') {
646
projectName = setupProject;
647
resourceFile = 'messages.xlf';
648
} else {
649
throw new Error(`Unknown input file ${file.path}`);
650
}
651
652
const xlf = new XLF(projectName),
653
keys: string[] = [],
654
messages: string[] = [];
655
656
const model = new TextModel(file.contents!.toString());
657
let inMessageSection = false;
658
model.lines.forEach(line => {
659
if (line.length === 0) {
660
return;
661
}
662
const firstChar = line.charAt(0);
663
switch (firstChar) {
664
case ';':
665
// Comment line;
666
return;
667
case '[':
668
inMessageSection = '[Messages]' === line || '[CustomMessages]' === line;
669
return;
670
}
671
if (!inMessageSection) {
672
return;
673
}
674
const sections: string[] = line.split('=');
675
if (sections.length !== 2) {
676
throw new Error(`Badly formatted message found: ${line}`);
677
} else {
678
const key = sections[0];
679
const value = sections[1];
680
if (key.length > 0 && value.length > 0) {
681
keys.push(key);
682
messages.push(value);
683
}
684
}
685
});
686
687
const originalPath = file.path.substring(file.cwd.length + 1, file.path.split('.')[0].length).replace(/\\/g, '/');
688
xlf.addFile(originalPath, keys, messages);
689
690
// Emit only upon all ISL files combined into single XLF instance
691
const newFilePath = path.join(projectName, resourceFile);
692
const xlfFile = new File({ path: newFilePath, contents: Buffer.from(xlf.toString(), 'utf-8') });
693
this.queue(xlfFile);
694
});
695
}
696
697
function createI18nFile(name: string, messages: any): File {
698
const result = Object.create(null);
699
result[''] = [
700
'--------------------------------------------------------------------------------------------',
701
'Copyright (c) Microsoft Corporation. All rights reserved.',
702
'Licensed under the MIT License. See License.txt in the project root for license information.',
703
'--------------------------------------------------------------------------------------------',
704
'Do not edit this file. It is machine generated.'
705
];
706
for (const key of Object.keys(messages)) {
707
result[key] = messages[key];
708
}
709
710
let content = JSON.stringify(result, null, '\t');
711
if (process.platform === 'win32') {
712
content = content.replace(/\n/g, '\r\n');
713
}
714
return new File({
715
path: path.join(name + '.i18n.json'),
716
contents: Buffer.from(content, 'utf8')
717
});
718
}
719
720
interface I18nPack {
721
version: string;
722
contents: {
723
[path: string]: Record<string, string>;
724
};
725
}
726
727
const i18nPackVersion = '1.0.0';
728
729
export interface TranslationPath {
730
id: string;
731
resourceName: string;
732
}
733
734
function getRecordFromL10nJsonFormat(l10nJsonFormat: l10nJsonFormat): Record<string, string> {
735
const record: Record<string, string> = {};
736
for (const key of Object.keys(l10nJsonFormat).sort()) {
737
const value = l10nJsonFormat[key];
738
record[key] = typeof value === 'string' ? value : value.message;
739
}
740
return record;
741
}
742
743
export function prepareI18nPackFiles(resultingTranslationPaths: TranslationPath[]): NodeJS.ReadWriteStream {
744
const parsePromises: Promise<l10nJsonDetails[]>[] = [];
745
const mainPack: I18nPack = { version: i18nPackVersion, contents: {} };
746
const extensionsPacks: Record<string, I18nPack> = {};
747
const errors: any[] = [];
748
return through(function (this: ThroughStream, xlf: File) {
749
let project = path.basename(path.dirname(path.dirname(xlf.relative)));
750
// strip `-new` since vscode-extensions-loc uses the `-new` suffix to indicate that it's from the new loc pipeline
751
const resource = path.basename(path.basename(xlf.relative, '.xlf'), '-new');
752
if (EXTERNAL_EXTENSIONS.find(e => e === resource)) {
753
project = extensionsProject;
754
}
755
const contents = xlf.contents!.toString();
756
log(`Found ${project}: ${resource}`);
757
const parsePromise = getL10nFilesFromXlf(contents);
758
parsePromises.push(parsePromise);
759
parsePromise.then(
760
resolvedFiles => {
761
resolvedFiles.forEach(file => {
762
const path = file.name;
763
const firstSlash = path.indexOf('/');
764
765
if (project === extensionsProject) {
766
// resource will be the extension id
767
let extPack = extensionsPacks[resource];
768
if (!extPack) {
769
extPack = extensionsPacks[resource] = { version: i18nPackVersion, contents: {} };
770
}
771
// remove 'extensions/extensionId/' segment
772
const secondSlash = path.indexOf('/', firstSlash + 1);
773
extPack.contents[path.substring(secondSlash + 1)] = getRecordFromL10nJsonFormat(file.messages);
774
} else {
775
mainPack.contents[path.substring(firstSlash + 1)] = getRecordFromL10nJsonFormat(file.messages);
776
}
777
});
778
}
779
).catch(reason => {
780
errors.push(reason);
781
});
782
}, function () {
783
Promise.all(parsePromises)
784
.then(() => {
785
if (errors.length > 0) {
786
throw errors;
787
}
788
const translatedMainFile = createI18nFile('./main', mainPack);
789
resultingTranslationPaths.push({ id: 'vscode', resourceName: 'main.i18n.json' });
790
791
this.queue(translatedMainFile);
792
for (const extensionId in extensionsPacks) {
793
const translatedExtFile = createI18nFile(`extensions/${extensionId}`, extensionsPacks[extensionId]);
794
this.queue(translatedExtFile);
795
796
resultingTranslationPaths.push({ id: extensionId, resourceName: `extensions/${extensionId}.i18n.json` });
797
}
798
this.queue(null);
799
})
800
.catch((reason) => {
801
this.emit('error', reason);
802
});
803
});
804
}
805
806
export function prepareIslFiles(language: Language, innoSetupConfig: InnoSetup): ThroughStream {
807
const parsePromises: Promise<l10nJsonDetails[]>[] = [];
808
809
return through(function (this: ThroughStream, xlf: File) {
810
const stream = this;
811
const parsePromise = XLF.parse(xlf.contents!.toString());
812
parsePromises.push(parsePromise);
813
parsePromise.then(
814
resolvedFiles => {
815
resolvedFiles.forEach(file => {
816
const translatedFile = createIslFile(file.name, file.messages, language, innoSetupConfig);
817
stream.queue(translatedFile);
818
});
819
}
820
).catch(reason => {
821
this.emit('error', reason);
822
});
823
}, function () {
824
Promise.all(parsePromises)
825
.then(() => { this.queue(null); })
826
.catch(reason => {
827
this.emit('error', reason);
828
});
829
});
830
}
831
832
function createIslFile(name: string, messages: l10nJsonFormat, language: Language, innoSetup: InnoSetup): File {
833
const content: string[] = [];
834
let originalContent: TextModel;
835
if (path.basename(name) === 'Default') {
836
originalContent = new TextModel(fs.readFileSync(name + '.isl', 'utf8'));
837
} else {
838
originalContent = new TextModel(fs.readFileSync(name + '.en.isl', 'utf8'));
839
}
840
originalContent.lines.forEach(line => {
841
if (line.length > 0) {
842
const firstChar = line.charAt(0);
843
if (firstChar === '[' || firstChar === ';') {
844
content.push(line);
845
} else {
846
const sections: string[] = line.split('=');
847
const key = sections[0];
848
let translated = line;
849
if (key) {
850
const translatedMessage = messages[key];
851
if (translatedMessage) {
852
translated = `${key}=${translatedMessage}`;
853
}
854
}
855
856
content.push(translated);
857
}
858
}
859
});
860
861
const basename = path.basename(name);
862
const filePath = `${basename}.${language.id}.isl`;
863
const encoded = iconv.encode(Buffer.from(content.join('\r\n'), 'utf8').toString(), innoSetup.codePage);
864
865
return new File({
866
path: filePath,
867
contents: Buffer.from(encoded),
868
});
869
}
870
871
function encodeEntities(value: string): string {
872
const result: string[] = [];
873
for (let i = 0; i < value.length; i++) {
874
const ch = value[i];
875
switch (ch) {
876
case '<':
877
result.push('&lt;');
878
break;
879
case '>':
880
result.push('&gt;');
881
break;
882
case '&':
883
result.push('&amp;');
884
break;
885
default:
886
result.push(ch);
887
}
888
}
889
return result.join('');
890
}
891
892
function decodeEntities(value: string): string {
893
return value.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&');
894
}
895
896