Introduce localize2 function (#194750)

* Introduce `localize2` function

This is syntax sugar around:
```
{ value localize('id', "Hello"), original: 'Hello' }
```

That will now be returned when you do:
```
localize2('id', "Hello");
```

* fix merge conflic

* new source map due to updated deps
This commit is contained in:
Tyler James Leonhardt 2023-10-04 12:57:38 -07:00 committed by GitHub
parent 1fbca59042
commit a27dc7725c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 160 additions and 34 deletions

View file

@ -142,6 +142,9 @@ 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),
@ -149,6 +152,7 @@ export = new class NoUnexternalizedStrings implements eslint.Rule.RuleModule {
['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,
};
}

File diff suppressed because one or more lines are too long

View file

@ -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);
@ -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
@ -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);
@ -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' }
@ -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`);
});

View file

@ -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!`);
}
@ -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;

View file

@ -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) {
@ -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;
}

View file

@ -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;
@ -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;
}
@ -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.
*
* `message` can contain `{n}` notation where it is replaced by the nth value in `...args`
* For example, `localize('sayHello', 'hello {0}', name)`
* @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)`
*
* @returns string The localized string.
*/
export function localize(key: string, message: string, ...args: (string | number | boolean | undefined | null)[]): string;
@ -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
@ -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)
};
}
@ -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;
@ -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);

View file

@ -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';
@ -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
}