build/lib/nls.ts/analyze
	function analyze(ts: typeof import('typescript'), contents: string, options: ts.CompilerOptions = {}): ILocalizeAnalysisResult {
		const filename = 'file.ts';
		const serviceHost = new SingleFileServiceHost(ts, Object.assign(clone(options), { noResolve: true }), filename, contents);
		const service = ts.createLanguageService(serviceHost);
		const sourceFile = ts.createSourceFile(filename, contents, ts.ScriptTarget.ES5, true);

		// all imports
		const imports = lazy(collect(ts, sourceFile, n => isImportNode(ts, n) ? CollectStepResult.YesAndRecurse : CollectStepResult.NoAndRecurse));

		// import nls = require('vs/nls');
		const importEqualsDeclarations = imports
			.filter(n => n.kind === ts.SyntaxKind.ImportEqualsDeclaration)
			.map(n => <ts.ImportEqualsDeclaration>n)
			.filter(d => d.moduleReference.kind === ts.SyntaxKind.ExternalModuleReference)
			.filter(d => (<ts.ExternalModuleReference>d.moduleReference).expression.getText() === '\'vs/nls\'');

		// import ... from 'vs/nls';
		const importDeclarations = imports
			.filter(n => n.kind === ts.SyntaxKind.ImportDeclaration)
			.map(n => <ts.ImportDeclaration>n)
			.filter(d => d.moduleSpecifier.kind === ts.SyntaxKind.StringLiteral)
			.filter(d => d.moduleSpecifier.getText() === '\'vs/nls\'')
			.filter(d => !!d.importClause && !!d.importClause.namedBindings);

		const nlsExpressions = importEqualsDeclarations
			.map(d => (<ts.ExternalModuleReference>d.moduleReference).expression)
			.concat(importDeclarations.map(d => d.moduleSpecifier))
			.map<ISpan>(d => ({
				start: ts.getLineAndCharacterOfPosition(sourceFile, d.getStart()),
				end: ts.getLineAndCharacterOfPosition(sourceFile, d.getEnd())
			}));

		// `nls.localize(...)` calls
		const nlsLocalizeCallExpressions = importDeclarations
			.filter(d => !!(d.importClause && d.importClause.namedBindings && d.importClause.namedBindings.kind === ts.SyntaxKind.NamespaceImport))
			.map(d => (<ts.NamespaceImport>d.importClause!.namedBindings).name)
			.concat(importEqualsDeclarations.map(d => d.name))

			// find read-only references to `nls`
			.map(n => service.getReferencesAtPosition(filename, n.pos + 1))
			.flatten()
			.filter(r => !r.isWriteAccess)

			// find the deepest call expressions AST nodes that contain those references
			.map(r => collect(ts, sourceFile, n => isCallExpressionWithinTextSpanCollectStep(ts, r.textSpan, n)))
			.map(a => lazy(a).last())
			.filter(n => !!n)
			.map(n => <ts.CallExpression>n)

			// only `localize` calls
			.filter(n => n.expression.kind === ts.SyntaxKind.PropertyAccessExpression && (<ts.PropertyAccessExpression>n.expression).name.getText() === 'localize');

		// `localize` named imports
		const allLocalizeImportDeclarations = importDeclarations
			.filter(d => !!(d.importClause && d.importClause.namedBindings && d.importClause.namedBindings.kind === ts.SyntaxKind.NamedImports))
			.map(d => ([] as any[]).concat((<ts.NamedImports>d.importClause!.namedBindings!).elements))
			.flatten();

		// `localize` read-only references
		const localizeReferences = allLocalizeImportDeclarations
			.filter(d => d.name.getText() === 'localize')
			.map(n => service.getReferencesAtPosition(filename, n.pos + 1))
			.flatten()
			.filter(r => !r.isWriteAccess);

		// custom named `localize` read-only references
		const namedLocalizeReferences = allLocalizeImportDeclarations
			.filter(d => d.propertyName && d.propertyName.getText() === 'localize')
			.map(n => service.getReferencesAtPosition(filename, n.name.pos + 1))
			.flatten()
			.filter(r => !r.isWriteAccess);

		// find the deepest call expressions AST nodes that contain those references
		const localizeCallExpressions = localizeReferences
			.concat(namedLocalizeReferences)
			.map(r => collect(ts, sourceFile, n => isCallExpressionWithinTextSpanCollectStep(ts, r.textSpan, n)))
			.map(a => lazy(a).last())
			.filter(n => !!n)
			.map(n => <ts.CallExpression>n);

		// collect everything
		const localizeCalls = nlsLocalizeCallExpressions
			.concat(localizeCallExpressions)
			.map(e => e.arguments)
			.filter(a => a.length > 1)
			.sort((a, b) => a[0].getStart() - b[0].getStart())
			.map<ILocalizeCall>(a => ({
				keySpan: { start: ts.getLineAndCharacterOfPosition(sourceFile, a[0].getStart()), end: ts.getLineAndCharacterOfPosition(sourceFile, a[0].getEnd()) },
				key: a[0].getText(),
				valueSpan: { start: ts.getLineAndCharacterOfPosition(sourceFile, a[1].getStart()), end: ts.getLineAndCharacterOfPosition(sourceFile, a[1].getEnd()) },
				value: a[1].getText()
			}));

		return {
			localizeCalls: localizeCalls.toArray(),
			nlsExpressions: nlsExpressions.toArray()
		};
	}