Change:
Testing in Firefox with de-ch (Swiss German) set. Open command palette and search for Remote Repositories commands :bug: The first command has a partly English, partly de-ch line under it ( and in English, seems to be de-ch translated) !
Hmm yeah it should look like this (Desktop screenshot) with the English strings next to the German strings. I think this is a bug in how I'm applying the translations... !
Just found out about these functions on the desktop side: Which seems to go through the manifest and turn some s into s. I followed up with to understand how this works.
This code was originally written by so I would ask him to know how this works.
Not quite, it was originally written by , but I'm happy to try and help. My understanding is that the code is taking a JSON object that looks like this: Together with another JSON object that looks like this: And produces a JSON object that looks like this by substituting all string values of the shape with their corresponding value:
I still see this on firefox: ! The first option's english translation still looks wrong? (tested on )
The first one is still wrong because of so we'll track that there. However, the rest now have the English string underneath which they use to not have, which means this is fixed and verified

Discussion:
*  Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ <del> import { cloneAndChange } from 'vs/base/common/objects'; </del> <ins> import { isObject, isString } from 'vs/base/common/types'; import { ILocalizedString } from 'vs/platform/action/common/action'; </ins> import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; <del> const nlsRegex = /^%([wd.-]+)%$/i; </del> <ins> import { localize } from 'vs/nls'; </ins> export interface ITranslations { [key: string]: string | { message: string; comment: string[] }; } <del> export function localizeManifest(manifest: IExtensionManifest, translations: ITranslations, fallbackTranslations?: ITranslations): IExtensionManifest { const patcher = (value: string): string | undefined => { if (typeof value !== 'string') { return undefined; } const match = nlsRegex.exec(value); </del> <ins> export function localizeManifest(extensionManifest: IExtensionManifest, translations: ITranslations, fallbackTranslations?: ITranslations): IExtensionManifest { try { replaceNLStrings(extensionManifest, translations, fallbackTranslations); } catch (error) { /*Ignore Error*/ } return extensionManifest; } </ins> <del> if (!match) { return undefined; </del> <ins> /** * This routine makes the following assumptions: * The root element is an object literal */ function replaceNLStrings(extensionManifest: IExtensionManifest, messages: ITranslations, originalMessages?: ITranslations): void { const processEntry = (obj: any, key: string | number, command?: boolean) => { const value = obj[key]; if (isString(value)) { const str = <string>value; const length = str.length; if (length > 1 && str[0] === '%' && str[length - 1] === '%') { const messageKey = str.substr(1, length - 2); let translated = messages[messageKey]; // If the messages come from a language pack they might miss some keys // Fill them from the original messages. 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; } } else { console.warn(`[${extensionManifest.name}]: ${localize('missingNLSKey', "Couldn't find message for key {0}.", messageKey)}`); } } } else if (isObject(value)) { for (const k in value) { if (value.hasOwnProperty(k)) { k === 'commands' ? processEntry(value, k, true) : processEntry(value, k, command); } } } else if (Array.isArray(value)) { for (let i = 0; i < value.length; i++) { processEntry(value, i, command); } </ins> } <del> const translation = translations?.[match[1]] ?? fallbackTranslations?.[match[1]] ?? value; return typeof translation === 'string' ? translation : (typeof translation.message === 'string' ? translation.message : value); </del> }; <del> return cloneAndChange(manifest, patcher); </del> <ins> for (const key in extensionManifest) { if (extensionManifest.hasOwnProperty(key)) { processEntry(extensionManifest, key); } } </ins> }

import { basename, isEqual, joinPath } from 'vs/base/common/resources'; import * as semver from 'vs/base/common/semver/semver'; import Severity from 'vs/base/common/severity'; <del> import { isEmptyObject, isObject, isString } from 'vs/base/common/types'; </del> <ins> import { isEmptyObject } from 'vs/base/common/types'; </ins> import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { IEnvironmentService } from 'vs/platform/environment/common/environment';

import { IExtensionsProfileScannerService, IScannedProfileExtension } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService'; import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; <del> import { ILocalizedString } from 'vs/platform/action/common/action'; </del> <ins> import { localizeManifest } from 'vs/platform/extensionManagement/common/extensionNls'; </ins> export type IScannedExtensionManifest = IRelaxedExtensionManifest & { __metadata?: Metadata };

return extensionManifest; } const localized = localizedMessages.values || Object.create(null); <del> this.replaceNLStrings(nlsConfiguration.pseudo, extensionManifest, localized, defaults, extensionLocation); </del> <ins> return localizeManifest(extensionManifest, localized, defaults); </ins> } catch (error) { /*Ignore Error*/ }

/** * Parses original message bundle, returns null if the original message bundle is null. */ <del> private async resolveOriginalMessageBundle(originalMessageBundle: URI | null, errors: ParseError[]): Promise<{ [key: string]: string } | null> { </del> <ins> private async resolveOriginalMessageBundle(originalMessageBundle: URI | null, errors: ParseError[]): Promise<{ [key: string]: string } | undefined> { </ins> if (originalMessageBundle) { try { const originalBundleContent = (await this.fileService.readFile(originalMessageBundle)).value.toString(); return parse(originalBundleContent, errors); } catch (error) { /* Ignore Error */ <del> return null; </del> } <del> } else { return null; </del> } <ins> return; </ins> } /**

}); } <del> /** * This routine makes the following assumptions: * The root element is an object literal */ private replaceNLStrings<T extends object>(pseudo: boolean, literal: T, messages: MessageBag, originalMessages: MessageBag | null, extensionLocation: URI): void { const processEntry = (obj: any, key: string | number, command?: boolean) => { const value = obj[key]; if (isString(value)) { const str = <string>value; const length = str.length; if (length > 1 && str[0] === '%' && str[length - 1] === '%') { const messageKey = str.substr(1, length - 2); let translated = messages[messageKey]; // If the messages come from a language pack they might miss some keys // Fill them from the original messages. if (translated === undefined && originalMessages) { translated = originalMessages[messageKey]; } let message: string | undefined = typeof translated === 'string' ? translated : translated.message; if (message !== undefined) { if (pseudo) { // FF3B and FF3D is the Unicode zenkaku representation for [ and ] message = 'uFF3B' + message.replace(/[aouei]/g, '$&$&') + 'uFF3D'; } // This branch returns ILocalizedString's instead of Strings so that the Command Palette can contain both the localized and the original value. if (command && originalMessages && (key === 'title' || key === 'category')) { const originalMessage = originalMessages[messageKey]; const localizedString: ILocalizedString = { value: message, original: typeof originalMessage === 'string' ? originalMessage : originalMessage?.message }; obj[key] = localizedString; } else { obj[key] = message; } } else { this.logService.warn(this.formatMessage(extensionLocation, localize('missingNLSKey', "Couldn't find message for key {0}.", messageKey))); } } } else if (isObject(value)) { for (const k in value) { if (value.hasOwnProperty(k)) { k === 'commands' ? processEntry(value, k, true) : processEntry(value, k, command); } } } else if (Array.isArray(value)) { for (let i = 0; i < value.length; i++) { processEntry(value, i, command); } } }; for (const key in literal) { if (literal.hasOwnProperty(key)) { processEntry(literal, key); } } } </del> private formatMessage(extensionLocation: URI, message: string): string { return `[${extensionLocation.path}]: ${message}`; }

<ins> /*--------------------------------------------------------------------------------------------- *  Copyright (c) Microsoft Corporation. All rights reserved. *  Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; import { deepClone } from 'vs/base/common/objects'; import { ILocalizedString } from 'vs/platform/action/common/action'; import { localizeManifest } from 'vs/platform/extensionManagement/common/extensionNls'; import { IExtensionManifest, IConfiguration } from 'vs/platform/extensions/common/extensions'; const manifest: IExtensionManifest = { name: 'test', publisher: 'test', version: '1.0.0', engines: { vscode: '*' }, contributes: { commands: [ { command: 'test.command', title: '%test.command.title%', category: '%test.command.category%' }, ], authentication: [ { id: 'test.authentication', label: '%test.authentication.label%', } ], configuration: { // to ensure we test another "title" property title: '%test.configuration.title%', properties: { 'test.configuration': { type: 'string', description: 'not important', } } } } }; suite('Localize Manifest', () => { test('replaces template strings', function () { const localizedManifest = localizeManifest( deepClone(manifest), { 'test.command.title': 'Test Command', 'test.command.category': 'Test Category', 'test.authentication.label': 'Test Authentication', 'test.configuration.title': 'Test Configuration', } ); 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, 'Test Authentication'); assert.strictEqual((localizedManifest.contributes?.configuration as IConfiguration).title, 'Test Configuration'); }); test('replaces template strings with fallback if not found in translations', function () { const localizedManifest = localizeManifest( deepClone(manifest), {}, { 'test.command.title': 'Test Command', 'test.command.category': 'Test Category', 'test.authentication.label': 'Test Authentication', 'test.configuration.title': 'Test Configuration', } ); 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, 'Test Authentication'); assert.strictEqual((localizedManifest.contributes?.configuration as IConfiguration).title, 'Test Configuration'); }); test('replaces template strings - command title & categories become ILocalizedString', function () { const localizedManifest = localizeManifest( deepClone(manifest), { 'test.command.title': 'Befehl test', 'test.command.category': 'Testkategorie', 'test.authentication.label': 'Testauthentifizierung', 'test.configuration.title': 'Testkonfiguration', }, { 'test.command.title': 'Test Command', 'test.command.category': 'Test Category', 'test.authentication.label': 'Test Authentication', 'test.configuration.title': 'Test Configuration', } ); const title = localizedManifest.contributes?.commands?.[0].title as ILocalizedString; const category = localizedManifest.contributes?.commands?.[0].category as ILocalizedString; assert.strictEqual(title.value, 'Befehl test'); assert.strictEqual(title.original, 'Test Command'); assert.strictEqual(category.value, 'Testkategorie'); assert.strictEqual(category.original, 'Test Category'); // Everything else stays as a string. assert.strictEqual(localizedManifest.contributes?.authentication?.[0].label, 'Testauthentifizierung'); assert.strictEqual((localizedManifest.contributes?.configuration as IConfiguration).title, 'Testkonfiguration'); }); }); </ins>

import Severity from 'vs/base/common/severity'; import * as strings from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; <ins> import { ILocalizedString } from 'vs/platform/action/common/action'; </ins> import { ExtensionKind } from 'vs/platform/environment/common/environment'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { getRemoteName } from 'vs/platform/remote/common/remoteHosts';

export interface ICommand { command: string; <del> title: string; category?: string; </del> <ins> title: string | ILocalizedString; category?: string | ILocalizedString; </ins> } export interface IConfigurationProperty {

), ...commands.map(c => $('tr', undefined, $('td', undefined, $('code', undefined, c.id)), <del> $('td', undefined, c.title), </del> <ins> $('td', undefined, typeof c.title === 'string' ? c.title : c.title.value), </ins> $('td', undefined, ...c.keybindings.map(keybinding => renderKeybinding(keybinding))), $('td', undefined, ...c.menus.map(context => $('code', undefined, context))) ))