Skip to content

Commit

Permalink
Introduce localize2 function
Browse files Browse the repository at this point in the history
This is syntax sugar around:
```
{ value localize('id', "Hello"), original: 'Hello' }
```

That will now be returned when you do:
```
localize2('id', "Hello");
```
  • Loading branch information
TylerLeonhardt committed Oct 3, 2023
1 parent 43a2ba6 commit e4134fe
Show file tree
Hide file tree
Showing 7 changed files with 160 additions and 34 deletions.
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

0 comments on commit e4134fe

Please sign in to comment.