Fix offending l10n.t call and add an eslint rule to prevent it from happening (#277577)

ref https://github.com/microsoft/vscode/issues/277576
This commit is contained in:
Tyler James Leonhardt
2025-11-14 18:13:03 -08:00
committed by GitHub
parent 9f56e2671c
commit 8711dcb9da
3 changed files with 92 additions and 1 deletions

View File

@@ -0,0 +1,90 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as eslint from 'eslint';
import { TSESTree } from '@typescript-eslint/utils';
/**
* Prevents the use of template literals in localization function calls.
*
* vscode.l10n.t() and nls.localize() cannot handle string templating.
* Use placeholders instead: vscode.l10n.t('Message {0}', value)
*
* Examples:
* ❌ vscode.l10n.t(`Message ${value}`)
* ✅ vscode.l10n.t('Message {0}', value)
*
* ❌ nls.localize('key', `Message ${value}`)
* ✅ nls.localize('key', 'Message {0}', value)
*/
export default new class NoLocalizationTemplateLiterals implements eslint.Rule.RuleModule {
readonly meta: eslint.Rule.RuleMetaData = {
messages: {
noTemplateLiteral: 'Template literals cannot be used in localization calls. Use placeholders like {0}, {1} instead.'
},
docs: {
description: 'Prevents template literals in vscode.l10n.t() and nls.localize() calls',
},
schema: false,
};
create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener {
function checkCallExpression(node: TSESTree.CallExpression) {
const callee = node.callee;
let isLocalizationCall = false;
let isNlsLocalize = false;
// Check for vscode.l10n.t()
if (callee.type === 'MemberExpression') {
const object = callee.object;
const property = callee.property;
// vscode.l10n.t
if (object.type === 'MemberExpression') {
const outerObject = object.object;
const outerProperty = object.property;
if (outerObject.type === 'Identifier' && outerObject.name === 'vscode' &&
outerProperty.type === 'Identifier' && outerProperty.name === 'l10n' &&
property.type === 'Identifier' && property.name === 't') {
isLocalizationCall = true;
}
}
// l10n.t or nls.localize or any *.localize
if (object.type === 'Identifier' && property.type === 'Identifier') {
if (object.name === 'l10n' && property.name === 't') {
isLocalizationCall = true;
} else if (property.name === 'localize') {
isLocalizationCall = true;
isNlsLocalize = true;
}
}
}
if (!isLocalizationCall) {
return;
}
// For vscode.l10n.t(message, ...args) - check the first argument (message)
// For nls.localize(key, message, ...args) - check first two arguments (key and message)
const argsToCheck = isNlsLocalize ? 2 : 1;
for (let i = 0; i < argsToCheck && i < node.arguments.length; i++) {
const arg = node.arguments[i];
if (arg && arg.type === 'TemplateLiteral' && arg.expressions.length > 0) {
context.report({
node: arg,
messageId: 'noTemplateLiteral'
});
}
}
}
return {
CallExpression: (node: any) => checkCallExpression(node as TSESTree.CallExpression)
};
}
};

View File

@@ -90,6 +90,7 @@ export default tseslint.config(
'local/code-no-reader-after-await': 'warn', 'local/code-no-reader-after-await': 'warn',
'local/code-no-observable-get-in-reactive-context': 'warn', 'local/code-no-observable-get-in-reactive-context': 'warn',
'local/code-policy-localization-key-match': 'warn', 'local/code-policy-localization-key-match': 'warn',
'local/code-no-localization-template-literals': 'error',
'local/code-no-deep-import-of-internal': ['error', { '.*Internal': true, 'searchExtTypesInternal': false }], 'local/code-no-deep-import-of-internal': ['error', { '.*Internal': true, 'searchExtTypesInternal': false }],
'local/code-layering': [ 'local/code-layering': [
'warn', 'warn',

View File

@@ -367,7 +367,7 @@ class TypeScriptQuickFixProvider implements vscode.CodeActionProvider<VsCodeCode
let title = action.description; let title = action.description;
if (action.fixName === fixNames.classIncorrectlyImplementsInterface) { if (action.fixName === fixNames.classIncorrectlyImplementsInterface) {
title = vscode.l10n.t('{0} with AI', action.description); title = vscode.l10n.t('{0} with AI', action.description);
message = vscode.l10n.t(`Implement the stubbed-out class members for ${document.getText(diagnostic.range)} with a useful implementation.`); message = vscode.l10n.t('Implement the stubbed-out class members for {0} with a useful implementation.', document.getText(diagnostic.range));
expand = { kind: 'code-action', action }; expand = { kind: 'code-action', action };
} else if (action.fixName === fixNames.fixClassDoesntImplementInheritedAbstractMember) { } else if (action.fixName === fixNames.fixClassDoesntImplementInheritedAbstractMember) {
title = vscode.l10n.t('{0} with AI', action.description); title = vscode.l10n.t('{0} with AI', action.description);