From 8711dcb9da7891f13446a06e3b460910e37e81d1 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt <2644648+TylerLeonhardt@users.noreply.github.com> Date: Fri, 14 Nov 2025 18:13:03 -0800 Subject: [PATCH] Fix offending l10n.t call and add an eslint rule to prevent it from happening (#277577) ref https://github.com/microsoft/vscode/issues/277576 --- .../code-no-localization-template-literals.ts | 90 +++++++++++++++++++ eslint.config.js | 1 + .../src/languageFeatures/quickFix.ts | 2 +- 3 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 .eslint-plugin-local/code-no-localization-template-literals.ts diff --git a/.eslint-plugin-local/code-no-localization-template-literals.ts b/.eslint-plugin-local/code-no-localization-template-literals.ts new file mode 100644 index 00000000000..30a5de7f364 --- /dev/null +++ b/.eslint-plugin-local/code-no-localization-template-literals.ts @@ -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) + }; + } +}; diff --git a/eslint.config.js b/eslint.config.js index 1e7dfbc33c6..69729bcdfb4 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -90,6 +90,7 @@ export default tseslint.config( 'local/code-no-reader-after-await': 'warn', 'local/code-no-observable-get-in-reactive-context': '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-layering': [ 'warn', diff --git a/extensions/typescript-language-features/src/languageFeatures/quickFix.ts b/extensions/typescript-language-features/src/languageFeatures/quickFix.ts index c0e3221be2e..c0c89670d1d 100644 --- a/extensions/typescript-language-features/src/languageFeatures/quickFix.ts +++ b/extensions/typescript-language-features/src/languageFeatures/quickFix.ts @@ -367,7 +367,7 @@ class TypeScriptQuickFixProvider implements vscode.CodeActionProvider