Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce localize2 function #194750

Merged
merged 3 commits into from
Oct 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .eslintplugin/code-no-unexternalized-strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,13 +142,17 @@ export = new class NoUnexternalizedStrings implements eslint.Rule.RuleModule {
// localize(...)
['CallExpression[callee.type="MemberExpression"][callee.object.name="nls"][callee.property.name="localize"]:exit']: (node: any) => visitLocalizeCall(node),

// localize2(...)
['CallExpression[callee.type="MemberExpression"][callee.object.name="nls"][callee.property.name="localize2"]:exit']: (node: any) => visitLocalizeCall(node),

// vscode.l10n.t(...)
['CallExpression[callee.type="MemberExpression"][callee.object.property.name="l10n"][callee.property.name="t"]:exit']: (node: any) => visitL10NCall(node),

// l10n.t(...)
['CallExpression[callee.object.name="l10n"][callee.property.name="t"]:exit']: (node: any) => visitL10NCall(node),

['CallExpression[callee.name="localize"][arguments.length>=2]:exit']: (node: any) => visitLocalizeCall(node),
['CallExpression[callee.name="localize2"][arguments.length>=2]:exit']: (node: any) => visitLocalizeCall(node),
['Program:exit']: reportBadStringsAndBadKeys,
};
}
Expand Down
34 changes: 22 additions & 12 deletions build/lib/nls.js

Large diffs are not rendered by default.

39 changes: 28 additions & 11 deletions build/lib/nls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,12 @@ module _nls {
return node.kind === ts.SyntaxKind.CallExpression ? CollectStepResult.YesAndRecurse : CollectStepResult.NoAndRecurse;
}

function analyze(ts: typeof import('typescript'), contents: string, options: ts.CompilerOptions = {}): ILocalizeAnalysisResult {
function analyze(
ts: typeof import('typescript'),
contents: string,
functionName: 'localize' | 'localize2',
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);
Expand Down Expand Up @@ -231,7 +236,7 @@ module _nls {
.map(n => <ts.CallExpression>n)

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

// `localize` named imports
const allLocalizeImportDeclarations = importDeclarations
Expand All @@ -241,14 +246,14 @@ module _nls {

// `localize` read-only references
const localizeReferences = allLocalizeImportDeclarations
.filter(d => d.name.getText() === 'localize')
.filter(d => d.name.getText() === functionName)
.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')
.filter(d => d.propertyName && d.propertyName.getText() === functionName)
.map(n => service.getReferencesAtPosition(filename, n.name.pos + 1))
.flatten()
.filter(r => !r.isWriteAccess);
Expand Down Expand Up @@ -406,20 +411,21 @@ module _nls {
}

function patch(ts: typeof import('typescript'), moduleId: string, typescript: string, javascript: string, sourcemap: sm.RawSourceMap): INlsStringResult {
const { localizeCalls, nlsExpressions } = analyze(ts, typescript);
const { localizeCalls, nlsExpressions } = analyze(ts, typescript, 'localize');
const { localizeCalls: localize2Calls, nlsExpressions: nls2Expressions } = analyze(ts, typescript, 'localize2');

if (localizeCalls.length === 0) {
return { javascript, sourcemap };
}

const nlsKeys = template(localizeCalls.map(lc => lc.key));
const nls = template(localizeCalls.map(lc => lc.value));
const nlsKeys = template(localizeCalls.map(lc => lc.key).concat(localize2Calls.map(lc => lc.key)));
const nls = template(localizeCalls.map(lc => lc.value).concat(localize2Calls.map(lc => lc.value)));
const smc = new sm.SourceMapConsumer(sourcemap);
const positionFrom = mappedPositionFrom.bind(null, sourcemap.sources[0]);
let i = 0;

// build patches
const patches = lazy(localizeCalls)
const localizePatches = lazy(localizeCalls)
.map(lc => ([
{ range: lc.keySpan, content: '' + (i++) },
{ range: lc.valueSpan, content: 'null' }
Expand All @@ -429,14 +435,25 @@ module _nls {
const start = lcFrom(smc.generatedPositionFor(positionFrom(c.range.start)));
const end = lcFrom(smc.generatedPositionFor(positionFrom(c.range.end)));
return { span: { start, end }, content: c.content };
})
.toArray();
});

const localize2Patches = lazy(localize2Calls)
.map(lc => ([
{ range: lc.keySpan, content: '' + (i++) }
])).flatten()
.map<IPatch>(c => {
const start = lcFrom(smc.generatedPositionFor(positionFrom(c.range.start)));
const end = lcFrom(smc.generatedPositionFor(positionFrom(c.range.end)));
return { span: { start, end }, content: c.content };
});

const patches = localizePatches.concat(localize2Patches).toArray();

javascript = patchJavascript(patches, javascript, moduleId);

// since imports are not within the sourcemap information,
// we must do this MacGyver style
if (nlsExpressions.length) {
if (nlsExpressions.length || nls2Expressions.length) {
javascript = javascript.replace(/^define\(.*$/m, line => {
return line.replace(/(['"])vs\/nls\1/g, `$1vs/nls!${moduleId}$1`);
});
Expand Down
6 changes: 5 additions & 1 deletion src/vs/nls.build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export function localize(data: ILocalizeInfo | string, message: string, ...args:
throw new Error(`Not supported at build time!`);
}

export function localize2(data: ILocalizeInfo | string, message: string, ...args: (string | number | boolean | undefined | null)[]): never {
throw new Error(`Not supported at build time!`);
}

export function getConfiguredDefaultLocale(): string | undefined {
throw new Error(`Not supported at build time!`);
}
Expand All @@ -25,7 +29,7 @@ export function getConfiguredDefaultLocale(): string | undefined {
*/
export function load(name: string, req: AMDLoader.IRelativeRequire, load: AMDLoader.IPluginLoadCallback, config: AMDLoader.IConfigurationOptions): void {
if (!name || name.length === 0) {
load({ localize, getConfiguredDefaultLocale });
load({ localize, localize2, getConfiguredDefaultLocale });
} else {
req([name + '.nls', name + '.nls.keys'], function (messages: string[], keys: string[]) {
buildMap[name] = messages;
Expand Down
13 changes: 13 additions & 0 deletions src/vs/nls.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ export interface ILocalizeInfo {
comment: string[];
}

interface ILocalizedString {
original: string;
value: string;
}

function _format(message: string, args: any[]): string {
let result: string;
if (args.length === 0) {
Expand All @@ -25,6 +30,14 @@ export function localize(data: ILocalizeInfo | string, message: string, ...args:
return _format(message, args);
}

export function localize2(data: ILocalizeInfo | string, message: string, ...args: any[]): ILocalizedString {
const res = _format(message, args);
return {
original: res,
value: res
};
}

export function getConfiguredDefaultLocale(_: string) {
return undefined;
}
93 changes: 86 additions & 7 deletions src/vs/nls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ export interface ILocalizeInfo {
comment: string[];
}

interface ILocalizedString {
original: string;
value: string;
}

interface ILocalizeFunc {
(info: ILocalizeInfo, message: string, ...args: (string | number | boolean | undefined | null)[]): string;
(key: string, message: string, ...args: (string | number | boolean | undefined | null)[]): string;
Expand All @@ -39,8 +44,18 @@ interface IBoundLocalizeFunc {
(idx: number, defaultValue: null): string;
}

interface ILocalize2Func {
(info: ILocalizeInfo, message: string, ...args: (string | number | boolean | undefined | null)[]): ILocalizedString;
(key: string, message: string, ...args: (string | number | boolean | undefined | null)[]): ILocalizedString;
}

interface IBoundLocalize2Func {
(idx: number, defaultValue: string): ILocalizedString;
}

interface IConsumerAPI {
localize: ILocalizeFunc | IBoundLocalizeFunc;
localize2: ILocalize2Func | IBoundLocalize2Func;
getConfiguredDefaultLocale(stringFromLocalizeCall: string): string | undefined;
}

Expand Down Expand Up @@ -107,19 +122,38 @@ function createScopedLocalize(scope: string[]): IBoundLocalizeFunc {
};
}

function createScopedLocalize2(scope: string[]): IBoundLocalize2Func {
return (idx: number, defaultValue: string, ...args) => ({
value: _format(scope[idx], args),
original: _format(defaultValue, args)
});
}

/**
* Localize a message.
* Marks a string to be localized. Returns the localized string.
*
* `message` can contain `{n}` notation where it is replaced by the nth value in `...args`
* For example, `localize({ key: 'sayHello', comment: ['Welcomes user'] }, 'hello {0}', name)`
* @param info The {@linkcode ILocalizeInfo} which describes the id and comments associated with the localized string.
* @param message The string to localize
* @param args The arguments to the string
*
* @note `message` can contain `{n}` notation where it is replaced by the nth value in `...args`
* @example `localize({ key: 'sayHello', comment: ['Welcomes user'] }, 'hello {0}', name)`
*
* @returns string The localized string.
*/
export function localize(info: ILocalizeInfo, message: string, ...args: (string | number | boolean | undefined | null)[]): string;

/**
* Localize a message.
* Marks a string to be localized. Returns the localized string.
*
* @param key The key to use for localizing the string
* @param message The string to localize
* @param args The arguments to the string
*
* @note `message` can contain `{n}` notation where it is replaced by the nth value in `...args`
* @example For example, `localize('sayHello', 'hello {0}', name)`
*
* `message` can contain `{n}` notation where it is replaced by the nth value in `...args`
* For example, `localize('sayHello', 'hello {0}', name)`
* @returns string The localized string.
*/
export function localize(key: string, message: string, ...args: (string | number | boolean | undefined | null)[]): string;

Expand All @@ -130,6 +164,47 @@ export function localize(data: ILocalizeInfo | string, message: string, ...args:
return _format(message, args);
}

/**
* Marks a string to be localized. Returns an {@linkcode ILocalizedString}
* which contains the localized string and the original string.
*
* @param info The {@linkcode ILocalizeInfo} which describes the id and comments associated with the localized string.
* @param message The string to localize
* @param args The arguments to the string
*
* @note `message` can contain `{n}` notation where it is replaced by the nth value in `...args`
* @example `localize2({ key: 'sayHello', comment: ['Welcomes user'] }, 'hello {0}', name)`
*
* @returns ILocalizedString which contains the localized string and the original string.
*/
export function localize2(info: ILocalizeInfo, message: string, ...args: (string | number | boolean | undefined | null)[]): ILocalizedString;

/**
* Marks a string to be localized. Returns an {@linkcode ILocalizedString}
* which contains the localized string and the original string.
*
* @param key The key to use for localizing the string
* @param message The string to localize
* @param args The arguments to the string
*
* @note `message` can contain `{n}` notation where it is replaced by the nth value in `...args`
* @example `localize('sayHello', 'hello {0}', name)`
*
* @returns ILocalizedString which contains the localized string and the original string.
*/
export function localize2(key: string, message: string, ...args: (string | number | boolean | undefined | null)[]): ILocalizedString;

/**
* @skipMangle
*/
export function localize2(data: ILocalizeInfo | string, message: string, ...args: (string | number | boolean | undefined | null)[]): ILocalizedString {
const original = _format(message, args);
return {
value: original,
original
};
}

/**
*
* @param stringFromLocalizeCall You must pass in a string that was returned from a `nls.localize()` call
Expand Down Expand Up @@ -159,6 +234,7 @@ export function setPseudoTranslation(value: boolean) {
export function create(key: string, data: IBundledStrings & IConsumerAPI): IConsumerAPI {
return {
localize: createScopedLocalize(data[key]),
localize2: createScopedLocalize2(data[key]),
getConfiguredDefaultLocale: data.getConfiguredDefaultLocale ?? ((_: string) => undefined)
};
}
Expand All @@ -173,8 +249,9 @@ export function load(name: string, req: AMDLoader.IRelativeRequire, load: AMDLoa
// TODO: We need to give back the mangled names here
return load({
localize: localize,
localize2: localize2,
getConfiguredDefaultLocale: () => pluginConfig.availableLanguages?.['*']
});
} as IConsumerAPI);
}
const language = pluginConfig.availableLanguages ? findLanguageForModule(pluginConfig.availableLanguages, name) : null;
const useDefaultLanguage = language === null || language === DEFAULT_TAG;
Expand All @@ -185,8 +262,10 @@ export function load(name: string, req: AMDLoader.IRelativeRequire, load: AMDLoa
const messagesLoaded = (messages: string[] | IBundledStrings) => {
if (Array.isArray(messages)) {
(messages as any as IConsumerAPI).localize = createScopedLocalize(messages);
(messages as any as IConsumerAPI).localize2 = createScopedLocalize2(messages);
} else {
(messages as any as IConsumerAPI).localize = createScopedLocalize(messages[name]);
(messages as any as IConsumerAPI).localize2 = createScopedLocalize2(messages[name]);
}
(messages as any as IConsumerAPI).getConfiguredDefaultLocale = () => pluginConfig.availableLanguages?.['*'];
load(messages);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { localize } from 'vs/nls';
import { localize, localize2 } from 'vs/nls';
import { IQuickInputService, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { DisposableStore } from 'vs/base/common/lifecycle';
Expand All @@ -15,12 +15,11 @@ import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/com

export class ConfigureDisplayLanguageAction extends Action2 {
public static readonly ID = 'workbench.action.configureLocale';
public static readonly LABEL = localize('configureLocale', "Configure Display Language");

constructor() {
super({
id: ConfigureDisplayLanguageAction.ID,
title: { original: 'Configure Display Language', value: ConfigureDisplayLanguageAction.LABEL },
title: localize2('configureLocale', "Configure Display Language"),
menu: {
id: MenuId.CommandPalette
}
Expand Down
Loading