Repository: microsoft/vscode (TypeScript) — Bug Fix

Issue:
No command localizations load if any command title points to an invalid `package.nls.json` entry
<!-- ⚠️⚠️ Do Not Delete This! bug_report_template ⚠️⚠️ -->
<!-- Please read our Rules of Conduct: https://opensource.microsoft.com/codeofconduct/ -->
<!-- 🕮 Read our guide about submitting issues: https://github.com/microsoft/vscode/wiki/Submitting-Bugs-and-Suggestions -->
<!-- 🔎 Search existing issues to avoid creating duplicates. -->
<!-- 🧪 Test using the latest Insiders build to see if your issue has already been fixed: https://code.visualstudio.com/insiders/ -->
<!-- 💡 Instead of creating your report here, use 'Report Issue' from the 'Help' menu in VS Code to pre-fill useful information. -->
<!-- 🔧 Launch with `code --disable-extensions` to check. -->
Does this issue occur when all extensions are disabled?: Yes/No

<!-- 🪓 If you answered No above, use 'Help: Start Extension Bisect' from Command Palette to try to identify the cause. -->
<!-- 📣 Issues caused by an extension need to be reported directly to the extension publisher. The 'Help > Report Issue' dialog can assist with this. -->
Version: 1.72.2 (user setup)
OS: Windows_NT x64 10.0.22518

Steps to Reproduce:

1. Use [Python extension](https://github.com/microsoft/vscode-python) for example. Have an entry in `package.json` for a command but misspell the `title` part:
```
{
    "category": "Python",
    "command": "python.setInterpreter",
    "title": "%python.command.python.doesNotExist.title%"
},
```
2. None of the other localizations load:

![image](https://user-images.githubusercontent.com/13199757/197847660-4e1d6d07-d93f-45f7-9ebc-9a69c4f16755.png)

Issue observed in Python extension: https://github.com/microsoft/vscode-python/pull/20063 and Jupyter: https://github.com/microsoft/vscode-jupyter/issues/11600

cc/ @TylerLeonhardt
No command localizations load if any command title points to an invalid `package.nls.json` entry
<!-- ⚠️⚠️ Do Not Delete This! bug_report_template ⚠️⚠️ -->
<!-- Please read our Rules of Conduct: https://opensource.microsoft.com/codeofconduct/ -->
<!-- 🕮 Read our guide about submitting issues: https://github.com/microsoft/vscode/wiki/Submitting-Bugs-and-Suggestions -->
<!-- 🔎 Search existing issues to avoid creating duplicates. -->
<!-- 🧪 Test using the latest Insiders build to see if your issue has already been fixed: https://code.visualstudio.com/insiders/ -->
<!-- 💡 Instead of creating your report here, use 'Report Issue' from the 'Help' menu in VS Code to pre-fill useful information. -->
<!-- 🔧 Launch with `code --disable-extensions` to check. -->
Does this issue occur when all extensions are disabled?: Yes/No

<!-- 🪓 If you answered No above, use 'Help: Start Extension Bisect' from Command Palette to try to identify the cause. -->
<!-- 📣 Issues caused by an extension need to be reported directly to the extension publisher. The 'Help > Report Issue' dialog can assist with this. -->
Version: 1.72.2 (user setup)
OS: Windows_NT x64 10.0.22518

Steps to Reproduce:

1. Use [Python extension](https://github.com/microsoft/vscode-python) for example. Have an entry in `package.json` for a command but misspell the `title` part:
```
{
    "category": "Python",
    "command": "python.setInterpreter",
    "title": "%python.command.python.doesNotExist.title%"
},
```
2. None of the other localizations load:

![image](https://user-images.githubusercontent.com/13199757/197847660-4e1d6d07-d93f-45f7-9ebc-9a69c4f16755.png)

Issue observed in Python extension: https://github.com/microsoft/vscode-python/pull/20063 and Jupyter: https://github.com/microsoft/vscode-jupyter/issues/11600

cc/ @TylerLeonhardt


Discussion:

Resolving patch:
diff --git a/src/vs/platform/extensionManagement/common/extensionNls.ts b/src/vs/platform/extensionManagement/common/extensionNls.ts
--- a/src/vs/platform/extensionManagement/common/extensionNls.ts
+++ b/src/vs/platform/extensionManagement/common/extensionNls.ts
@@ -9,13 +9,14 @@ import { IExtensionManifest } from 'vs/platform/extensions/common/extensions';
 import { localize } from 'vs/nls';
 
 export interface ITranslations {
-	[key: string]: string | { message: string; comment: string[] };
+	[key: string]: string | { message: string; comment: string[] } | undefined;
 }
 
 export function localizeManifest(extensionManifest: IExtensionManifest, translations: ITranslations, fallbackTranslations?: ITranslations): IExtensionManifest {
 	try {
 		replaceNLStrings(extensionManifest, translations, fallbackTranslations);
 	} catch (error) {
+		console.error(error?.message ?? error);
 		/*Ignore Error*/
 	}
 	return extensionManifest;
@@ -39,27 +40,32 @@ function replaceNLStrings(extensionManifest: IExtensionManifest, messages: ITran
 				if (translated === undefined && originalMessages) {
 					translated = originalMessages[messageKey];
 				}
-				const message: string | undefined = typeof translated === 'string' ? translated : translated.message;
-				if (message !== undefined) {
-					// This branch returns ILocalizedString's instead of Strings so that the Command Palette can contain both the localized and the original value.
-					const original = originalMessages?.[messageKey];
-					const originalMessage: string | undefined = typeof original === 'string' ? original : original?.message;
-					if (
-						// if we are translating the title or category of a command
-						command && (key === 'title' || key === 'category') &&
-						// and the original value is not the same as the translated value
-						originalMessage && originalMessage !== message
-					) {
-						const localizedString: ILocalizedString = {
-							value: message,
-							original: originalMessage
-						};
-						obj[key] = localizedString;
-					} else {
-						obj[key] = message;
+				const message: string | undefined = typeof translated === 'string' ? translated : translated?.message;
+
+				// This branch returns ILocalizedString's instead of Strings so that the Command Palette can contain both the localized and the original value.
+				const original = originalMessages?.[messageKey];
+				const originalMessage: string | undefined = typeof original === 'string' ? original : original?.message;
+
+				if (!message) {
+					if (!originalMessage) {
+						console.warn(`[${extensionManifest.name}]: ${localize('missingNLSKey', "Couldn't find message for key {0}.", messageKey)}`);
 					}
+					return;
+				}
+
+				if (
+					// if we are translating the title or category of a command
+					command && (key === 'title' || key === 'category') &&
+					// and the original value is not the same as the translated value
+					originalMessage && originalMessage !== message
+				) {
+					const localizedString: ILocalizedString = {
+						value: message,
+						original: originalMessage
+					};
+					obj[key] = localizedString;
 				} else {
-					console.warn(`[${extensionManifest.name}]: ${localize('missingNLSKey', "Couldn't find message for key {0}.", messageKey)}`);
+					obj[key] = message;
 				}
 			}
 		} else if (isObject(value)) {


Test patch:
diff --git a/src/vs/platform/extensionManagement/test/common/extensionNls.test.ts b/src/vs/platform/extensionManagement/test/common/extensionNls.test.ts
--- a/src/vs/platform/extensionManagement/test/common/extensionNls.test.ts
+++ b/src/vs/platform/extensionManagement/test/common/extensionNls.test.ts
@@ -107,4 +107,42 @@ suite('Localize Manifest', () => {
 		assert.strictEqual(localizedManifest.contributes?.authentication?.[0].label, 'Testauthentifizierung');
 		assert.strictEqual((localizedManifest.contributes?.configuration as IConfiguration).title, 'Testkonfiguration');
 	});
+
+	test('replaces template strings - is best effort #164630', function () {
+		const manifestWithTypo: IExtensionManifest = {
+			name: 'test',
+			publisher: 'test',
+			version: '1.0.0',
+			engines: {
+				vscode: '*'
+			},
+			contributes: {
+				authentication: [
+					{
+						id: 'test.authentication',
+						// This not existing in the bundle shouldn't cause an error.
+						label: '%doesnotexist%',
+					}
+				],
+				commands: [
+					{
+						command: 'test.command',
+						title: '%test.command.title%',
+						category: '%test.command.category%'
+					},
+				],
+			}
+		};
+
+		const localizedManifest = localizeManifest(
+			deepClone(manifestWithTypo),
+			{
+				'test.command.title': 'Test Command',
+				'test.command.category': 'Test Category'
+			});
+
+		assert.strictEqual(localizedManifest.contributes?.commands?.[0].title, 'Test Command');
+		assert.strictEqual(localizedManifest.contributes?.commands?.[0].category, 'Test Category');
+		assert.strictEqual(localizedManifest.contributes?.authentication?.[0].label, '%doesnotexist%');
+	});
 });