Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/build/lib/nls.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 type * as ts from 'typescript';
7
import lazy from 'lazy.js';
8
import { duplex, through } from 'event-stream';
9
import File from 'vinyl';
10
import sm from 'source-map';
11
import path from 'path';
12
import sort from 'gulp-sort';
13
14
type FileSourceMap = File & { sourceMap: sm.RawSourceMap };
15
16
enum CollectStepResult {
17
Yes,
18
YesAndRecurse,
19
No,
20
NoAndRecurse
21
}
22
23
function collect(ts: typeof import('typescript'), node: ts.Node, fn: (node: ts.Node) => CollectStepResult): ts.Node[] {
24
const result: ts.Node[] = [];
25
26
function loop(node: ts.Node) {
27
const stepResult = fn(node);
28
29
if (stepResult === CollectStepResult.Yes || stepResult === CollectStepResult.YesAndRecurse) {
30
result.push(node);
31
}
32
33
if (stepResult === CollectStepResult.YesAndRecurse || stepResult === CollectStepResult.NoAndRecurse) {
34
ts.forEachChild(node, loop);
35
}
36
}
37
38
loop(node);
39
return result;
40
}
41
42
function clone<T extends object>(object: T): T {
43
const result = {} as any as T;
44
for (const id in object) {
45
result[id] = object[id];
46
}
47
return result;
48
}
49
50
/**
51
* Returns a stream containing the patched JavaScript and source maps.
52
*/
53
export function nls(options: { preserveEnglish: boolean }): NodeJS.ReadWriteStream {
54
let base: string;
55
const input = through();
56
const output = input
57
.pipe(sort()) // IMPORTANT: to ensure stable NLS metadata generation, we must sort the files because NLS messages are globally extracted and indexed across all files
58
.pipe(through(function (f: FileSourceMap) {
59
if (!f.sourceMap) {
60
return this.emit('error', new Error(`File ${f.relative} does not have sourcemaps.`));
61
}
62
63
let source = f.sourceMap.sources[0];
64
if (!source) {
65
return this.emit('error', new Error(`File ${f.relative} does not have a source in the source map.`));
66
}
67
68
const root = f.sourceMap.sourceRoot;
69
if (root) {
70
source = path.join(root, source);
71
}
72
73
const typescript = f.sourceMap.sourcesContent![0];
74
if (!typescript) {
75
return this.emit('error', new Error(`File ${f.relative} does not have the original content in the source map.`));
76
}
77
78
base = f.base;
79
this.emit('data', _nls.patchFile(f, typescript, options));
80
}, function () {
81
for (const file of [
82
new File({
83
contents: Buffer.from(JSON.stringify({
84
keys: _nls.moduleToNLSKeys,
85
messages: _nls.moduleToNLSMessages,
86
}, null, '\t')),
87
base,
88
path: `${base}/nls.metadata.json`
89
}),
90
new File({
91
contents: Buffer.from(JSON.stringify(_nls.allNLSMessages)),
92
base,
93
path: `${base}/nls.messages.json`
94
}),
95
new File({
96
contents: Buffer.from(JSON.stringify(_nls.allNLSModulesAndKeys)),
97
base,
98
path: `${base}/nls.keys.json`
99
}),
100
new File({
101
contents: Buffer.from(`/*---------------------------------------------------------
102
* Copyright (C) Microsoft Corporation. All rights reserved.
103
*--------------------------------------------------------*/
104
globalThis._VSCODE_NLS_MESSAGES=${JSON.stringify(_nls.allNLSMessages)};`),
105
base,
106
path: `${base}/nls.messages.js`
107
})
108
]) {
109
this.emit('data', file);
110
}
111
112
this.emit('end');
113
}));
114
115
return duplex(input, output);
116
}
117
118
function isImportNode(ts: typeof import('typescript'), node: ts.Node): boolean {
119
return node.kind === ts.SyntaxKind.ImportDeclaration || node.kind === ts.SyntaxKind.ImportEqualsDeclaration;
120
}
121
122
module _nls {
123
124
export const moduleToNLSKeys: { [name: string /* module ID */]: ILocalizeKey[] /* keys */ } = {};
125
export const moduleToNLSMessages: { [name: string /* module ID */]: string[] /* messages */ } = {};
126
export const allNLSMessages: string[] = [];
127
export const allNLSModulesAndKeys: Array<[string /* module ID */, string[] /* keys */]> = [];
128
let allNLSMessagesIndex = 0;
129
130
type ILocalizeKey = string | { key: string }; // key might contain metadata for translators and then is not just a string
131
132
interface INlsPatchResult {
133
javascript: string;
134
sourcemap: sm.RawSourceMap;
135
nlsMessages?: string[];
136
nlsKeys?: ILocalizeKey[];
137
}
138
139
interface ISpan {
140
start: ts.LineAndCharacter;
141
end: ts.LineAndCharacter;
142
}
143
144
interface ILocalizeCall {
145
keySpan: ISpan;
146
key: string;
147
valueSpan: ISpan;
148
value: string;
149
}
150
151
interface ILocalizeAnalysisResult {
152
localizeCalls: ILocalizeCall[];
153
}
154
155
interface IPatch {
156
span: ISpan;
157
content: string;
158
}
159
160
function fileFrom(file: File, contents: string, path: string = file.path) {
161
return new File({
162
contents: Buffer.from(contents),
163
base: file.base,
164
cwd: file.cwd,
165
path: path
166
});
167
}
168
169
function mappedPositionFrom(source: string, lc: ts.LineAndCharacter): sm.MappedPosition {
170
return { source, line: lc.line + 1, column: lc.character };
171
}
172
173
function lcFrom(position: sm.Position): ts.LineAndCharacter {
174
return { line: position.line - 1, character: position.column };
175
}
176
177
class SingleFileServiceHost implements ts.LanguageServiceHost {
178
179
private file: ts.IScriptSnapshot;
180
private lib: ts.IScriptSnapshot;
181
182
constructor(ts: typeof import('typescript'), private options: ts.CompilerOptions, private filename: string, contents: string) {
183
this.file = ts.ScriptSnapshot.fromString(contents);
184
this.lib = ts.ScriptSnapshot.fromString('');
185
}
186
187
getCompilationSettings = () => this.options;
188
getScriptFileNames = () => [this.filename];
189
getScriptVersion = () => '1';
190
getScriptSnapshot = (name: string) => name === this.filename ? this.file : this.lib;
191
getCurrentDirectory = () => '';
192
getDefaultLibFileName = () => 'lib.d.ts';
193
194
readFile(path: string, _encoding?: string): string | undefined {
195
if (path === this.filename) {
196
return this.file.getText(0, this.file.getLength());
197
}
198
return undefined;
199
}
200
fileExists(path: string): boolean {
201
return path === this.filename;
202
}
203
}
204
205
function isCallExpressionWithinTextSpanCollectStep(ts: typeof import('typescript'), textSpan: ts.TextSpan, node: ts.Node): CollectStepResult {
206
if (!ts.textSpanContainsTextSpan({ start: node.pos, length: node.end - node.pos }, textSpan)) {
207
return CollectStepResult.No;
208
}
209
210
return node.kind === ts.SyntaxKind.CallExpression ? CollectStepResult.YesAndRecurse : CollectStepResult.NoAndRecurse;
211
}
212
213
function analyze(
214
ts: typeof import('typescript'),
215
contents: string,
216
functionName: 'localize' | 'localize2',
217
options: ts.CompilerOptions = {}
218
): ILocalizeAnalysisResult {
219
const filename = 'file.ts';
220
const serviceHost = new SingleFileServiceHost(ts, Object.assign(clone(options), { noResolve: true }), filename, contents);
221
const service = ts.createLanguageService(serviceHost);
222
const sourceFile = ts.createSourceFile(filename, contents, ts.ScriptTarget.ES5, true);
223
224
// all imports
225
const imports = lazy(collect(ts, sourceFile, n => isImportNode(ts, n) ? CollectStepResult.YesAndRecurse : CollectStepResult.NoAndRecurse));
226
227
// import nls = require('vs/nls');
228
const importEqualsDeclarations = imports
229
.filter(n => n.kind === ts.SyntaxKind.ImportEqualsDeclaration)
230
.map(n => <ts.ImportEqualsDeclaration>n)
231
.filter(d => d.moduleReference.kind === ts.SyntaxKind.ExternalModuleReference)
232
.filter(d => (<ts.ExternalModuleReference>d.moduleReference).expression.getText().endsWith(`/nls.js'`));
233
234
// import ... from 'vs/nls';
235
const importDeclarations = imports
236
.filter(n => n.kind === ts.SyntaxKind.ImportDeclaration)
237
.map(n => <ts.ImportDeclaration>n)
238
.filter(d => d.moduleSpecifier.kind === ts.SyntaxKind.StringLiteral)
239
.filter(d => d.moduleSpecifier.getText().endsWith(`/nls.js'`))
240
.filter(d => !!d.importClause && !!d.importClause.namedBindings);
241
242
// `nls.localize(...)` calls
243
const nlsLocalizeCallExpressions = importDeclarations
244
.filter(d => !!(d.importClause && d.importClause.namedBindings && d.importClause.namedBindings.kind === ts.SyntaxKind.NamespaceImport))
245
.map(d => (<ts.NamespaceImport>d.importClause!.namedBindings).name)
246
.concat(importEqualsDeclarations.map(d => d.name))
247
248
// find read-only references to `nls`
249
.map(n => service.getReferencesAtPosition(filename, n.pos + 1) ?? [])
250
.flatten()
251
.filter(r => !r.isWriteAccess)
252
253
// find the deepest call expressions AST nodes that contain those references
254
.map(r => collect(ts, sourceFile, n => isCallExpressionWithinTextSpanCollectStep(ts, r.textSpan, n)))
255
.map(a => lazy(a).last())
256
.filter(n => !!n)
257
.map(n => <ts.CallExpression>n)
258
259
// only `localize` calls
260
.filter(n => n.expression.kind === ts.SyntaxKind.PropertyAccessExpression && (<ts.PropertyAccessExpression>n.expression).name.getText() === functionName);
261
262
// `localize` named imports
263
const allLocalizeImportDeclarations = importDeclarations
264
.filter(d => !!(d.importClause && d.importClause.namedBindings && d.importClause.namedBindings.kind === ts.SyntaxKind.NamedImports))
265
.map(d => ([] as any[]).concat((<ts.NamedImports>d.importClause!.namedBindings!).elements))
266
.flatten();
267
268
// `localize` read-only references
269
const localizeReferences = allLocalizeImportDeclarations
270
.filter(d => d.name.getText() === functionName)
271
.map(n => service.getReferencesAtPosition(filename, n.pos + 1) ?? [])
272
.flatten()
273
.filter(r => !r.isWriteAccess);
274
275
// custom named `localize` read-only references
276
const namedLocalizeReferences = allLocalizeImportDeclarations
277
.filter(d => d.propertyName && d.propertyName.getText() === functionName)
278
.map(n => service.getReferencesAtPosition(filename, n.name.pos + 1) ?? [])
279
.flatten()
280
.filter(r => !r.isWriteAccess);
281
282
// find the deepest call expressions AST nodes that contain those references
283
const localizeCallExpressions = localizeReferences
284
.concat(namedLocalizeReferences)
285
.map(r => collect(ts, sourceFile, n => isCallExpressionWithinTextSpanCollectStep(ts, r.textSpan, n)))
286
.map(a => lazy(a).last())
287
.filter(n => !!n)
288
.map(n => <ts.CallExpression>n);
289
290
// collect everything
291
const localizeCalls = nlsLocalizeCallExpressions
292
.concat(localizeCallExpressions)
293
.map(e => e.arguments)
294
.filter(a => a.length > 1)
295
.sort((a, b) => a[0].getStart() - b[0].getStart())
296
.map<ILocalizeCall>(a => ({
297
keySpan: { start: ts.getLineAndCharacterOfPosition(sourceFile, a[0].getStart()), end: ts.getLineAndCharacterOfPosition(sourceFile, a[0].getEnd()) },
298
key: a[0].getText(),
299
valueSpan: { start: ts.getLineAndCharacterOfPosition(sourceFile, a[1].getStart()), end: ts.getLineAndCharacterOfPosition(sourceFile, a[1].getEnd()) },
300
value: a[1].getText()
301
}));
302
303
return {
304
localizeCalls: localizeCalls.toArray()
305
};
306
}
307
308
class TextModel {
309
310
private lines: string[];
311
private lineEndings: string[];
312
313
constructor(contents: string) {
314
const regex = /\r\n|\r|\n/g;
315
let index = 0;
316
let match: RegExpExecArray | null;
317
318
this.lines = [];
319
this.lineEndings = [];
320
321
while (match = regex.exec(contents)) {
322
this.lines.push(contents.substring(index, match.index));
323
this.lineEndings.push(match[0]);
324
index = regex.lastIndex;
325
}
326
327
if (contents.length > 0) {
328
this.lines.push(contents.substring(index, contents.length));
329
this.lineEndings.push('');
330
}
331
}
332
333
public get(index: number): string {
334
return this.lines[index];
335
}
336
337
public set(index: number, line: string): void {
338
this.lines[index] = line;
339
}
340
341
public get lineCount(): number {
342
return this.lines.length;
343
}
344
345
/**
346
* Applies patch(es) to the model.
347
* Multiple patches must be ordered.
348
* Does not support patches spanning multiple lines.
349
*/
350
public apply(patch: IPatch): void {
351
const startLineNumber = patch.span.start.line;
352
const endLineNumber = patch.span.end.line;
353
354
const startLine = this.lines[startLineNumber] || '';
355
const endLine = this.lines[endLineNumber] || '';
356
357
this.lines[startLineNumber] = [
358
startLine.substring(0, patch.span.start.character),
359
patch.content,
360
endLine.substring(patch.span.end.character)
361
].join('');
362
363
for (let i = startLineNumber + 1; i <= endLineNumber; i++) {
364
this.lines[i] = '';
365
}
366
}
367
368
public toString(): string {
369
return lazy(this.lines).zip(this.lineEndings)
370
.flatten().toArray().join('');
371
}
372
}
373
374
function patchJavascript(patches: IPatch[], contents: string): string {
375
const model = new TextModel(contents);
376
377
// patch the localize calls
378
lazy(patches).reverse().each(p => model.apply(p));
379
380
return model.toString();
381
}
382
383
function patchSourcemap(patches: IPatch[], rsm: sm.RawSourceMap, smc: sm.SourceMapConsumer): sm.RawSourceMap {
384
const smg = new sm.SourceMapGenerator({
385
file: rsm.file,
386
sourceRoot: rsm.sourceRoot
387
});
388
389
patches = patches.reverse();
390
let currentLine = -1;
391
let currentLineDiff = 0;
392
let source: string | null = null;
393
394
smc.eachMapping(m => {
395
const patch = patches[patches.length - 1];
396
const original = { line: m.originalLine, column: m.originalColumn };
397
const generated = { line: m.generatedLine, column: m.generatedColumn };
398
399
if (currentLine !== generated.line) {
400
currentLineDiff = 0;
401
}
402
403
currentLine = generated.line;
404
generated.column += currentLineDiff;
405
406
if (patch && m.generatedLine - 1 === patch.span.end.line && m.generatedColumn === patch.span.end.character) {
407
const originalLength = patch.span.end.character - patch.span.start.character;
408
const modifiedLength = patch.content.length;
409
const lengthDiff = modifiedLength - originalLength;
410
currentLineDiff += lengthDiff;
411
generated.column += lengthDiff;
412
413
patches.pop();
414
}
415
416
source = rsm.sourceRoot ? path.relative(rsm.sourceRoot, m.source) : m.source;
417
source = source.replace(/\\/g, '/');
418
smg.addMapping({ source, name: m.name, original, generated });
419
}, null, sm.SourceMapConsumer.GENERATED_ORDER);
420
421
if (source) {
422
smg.setSourceContent(source, smc.sourceContentFor(source));
423
}
424
425
return JSON.parse(smg.toString());
426
}
427
428
function parseLocalizeKeyOrValue(sourceExpression: string) {
429
// sourceValue can be "foo", 'foo', `foo` or { .... }
430
// in its evalulated form
431
// we want to return either the string or the object
432
// eslint-disable-next-line no-eval
433
return eval(`(${sourceExpression})`);
434
}
435
436
function patch(ts: typeof import('typescript'), typescript: string, javascript: string, sourcemap: sm.RawSourceMap, options: { preserveEnglish: boolean }): INlsPatchResult {
437
const { localizeCalls } = analyze(ts, typescript, 'localize');
438
const { localizeCalls: localize2Calls } = analyze(ts, typescript, 'localize2');
439
440
if (localizeCalls.length === 0 && localize2Calls.length === 0) {
441
return { javascript, sourcemap };
442
}
443
444
const nlsKeys = localizeCalls.map(lc => parseLocalizeKeyOrValue(lc.key)).concat(localize2Calls.map(lc => parseLocalizeKeyOrValue(lc.key)));
445
const nlsMessages = localizeCalls.map(lc => parseLocalizeKeyOrValue(lc.value)).concat(localize2Calls.map(lc => parseLocalizeKeyOrValue(lc.value)));
446
const smc = new sm.SourceMapConsumer(sourcemap);
447
const positionFrom = mappedPositionFrom.bind(null, sourcemap.sources[0]);
448
449
// build patches
450
const toPatch = (c: { range: ISpan; content: string }): IPatch => {
451
const start = lcFrom(smc.generatedPositionFor(positionFrom(c.range.start)));
452
const end = lcFrom(smc.generatedPositionFor(positionFrom(c.range.end)));
453
return { span: { start, end }, content: c.content };
454
};
455
456
const localizePatches = lazy(localizeCalls)
457
.map(lc => (
458
options.preserveEnglish ? [
459
{ range: lc.keySpan, content: `${allNLSMessagesIndex++}` } // localize('key', "message") => localize(<index>, "message")
460
] : [
461
{ range: lc.keySpan, content: `${allNLSMessagesIndex++}` }, // localize('key', "message") => localize(<index>, null)
462
{ range: lc.valueSpan, content: 'null' }
463
]))
464
.flatten()
465
.map(toPatch);
466
467
const localize2Patches = lazy(localize2Calls)
468
.map(lc => (
469
{ range: lc.keySpan, content: `${allNLSMessagesIndex++}` } // localize2('key', "message") => localize(<index>, "message")
470
))
471
.map(toPatch);
472
473
// Sort patches by their start position
474
const patches = localizePatches.concat(localize2Patches).toArray().sort((a, b) => {
475
if (a.span.start.line < b.span.start.line) {
476
return -1;
477
} else if (a.span.start.line > b.span.start.line) {
478
return 1;
479
} else if (a.span.start.character < b.span.start.character) {
480
return -1;
481
} else if (a.span.start.character > b.span.start.character) {
482
return 1;
483
} else {
484
return 0;
485
}
486
});
487
488
javascript = patchJavascript(patches, javascript);
489
490
sourcemap = patchSourcemap(patches, sourcemap, smc);
491
492
return { javascript, sourcemap, nlsKeys, nlsMessages };
493
}
494
495
export function patchFile(javascriptFile: File, typescript: string, options: { preserveEnglish: boolean }): File {
496
const ts = require('typescript') as typeof import('typescript');
497
// hack?
498
const moduleId = javascriptFile.relative
499
.replace(/\.js$/, '')
500
.replace(/\\/g, '/');
501
502
const { javascript, sourcemap, nlsKeys, nlsMessages } = patch(
503
ts,
504
typescript,
505
javascriptFile.contents!.toString(),
506
(<any>javascriptFile).sourceMap,
507
options
508
);
509
510
const result = fileFrom(javascriptFile, javascript);
511
(<any>result).sourceMap = sourcemap;
512
513
if (nlsKeys) {
514
moduleToNLSKeys[moduleId] = nlsKeys;
515
allNLSModulesAndKeys.push([moduleId, nlsKeys.map(nlsKey => typeof nlsKey === 'string' ? nlsKey : nlsKey.key)]);
516
}
517
518
if (nlsMessages) {
519
moduleToNLSMessages[moduleId] = nlsMessages;
520
allNLSMessages.push(...nlsMessages);
521
}
522
523
return result;
524
}
525
}
526
527