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