diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/contributions.ts b/src/vs/code/electron-browser/sharedProcess/contrib/contributions.ts index 593d731262f..50ec33a43c9 100644 --- a/src/vs/code/electron-browser/sharedProcess/contrib/contributions.ts +++ b/src/vs/code/electron-browser/sharedProcess/contrib/contributions.ts @@ -7,12 +7,12 @@ import { NodeCachedDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/nodeCachedDataCleaner'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { LanguagePackExtensions } from 'vs/code/electron-browser/sharedProcess/contrib/languagePackExtensions'; import { IDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; +import { LanguagePacksCache } from 'vs/platform/localizations/node/localizations'; export function createSharedProcessContributions(service: IInstantiationService): IDisposable { return combinedDisposable([ service.createInstance(NodeCachedDataCleaner), - service.createInstance(LanguagePackExtensions) + service.createInstance(LanguagePacksCache) ]); } diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 5eb1cb408ab..94ed0c0c198 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -10,6 +10,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import Event from 'vs/base/common/event'; import { IPager } from 'vs/base/common/paging'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ILocalization } from 'vs/platform/localizations/common/localizations'; export const EXTENSION_IDENTIFIER_PATTERN = '^([a-z0-9A-Z][a-z0-9\-A-Z]*)\\.([a-z0-9A-Z][a-z0-9\-A-Z]*)$'; export const EXTENSION_IDENTIFIER_REGEX = new RegExp(EXTENSION_IDENTIFIER_PATTERN); @@ -85,12 +86,6 @@ export interface IColor { defaults: { light: string, dark: string, highContrast: string }; } -export interface ILocalization { - languageId: string; - languageName?: string; - translations: string; -} - export interface IExtensionContributions { commands?: ICommand[]; configuration?: IConfiguration; diff --git a/src/vs/platform/extensionManagement/test/common/extensionEnablementService.test.ts b/src/vs/platform/extensionManagement/test/common/extensionEnablementService.test.ts index 63d92406632..8113bdd0f14 100644 --- a/src/vs/platform/extensionManagement/test/common/extensionEnablementService.test.ts +++ b/src/vs/platform/extensionManagement/test/common/extensionEnablementService.test.ts @@ -326,7 +326,7 @@ suite('ExtensionEnablementService Test', () => { }); test('test canChangeEnablement return false for language packs', () => { - assert.equal(testObject.canChangeEnablement(aLocalExtension('pub.a', { localizations: [{ languageId: 'gr', translations: 'somepath' }] })), false); + assert.equal(testObject.canChangeEnablement(aLocalExtension('pub.a', { localizations: [{ languageId: 'gr', translations: [{ id: 'vscode', path: 'path' }] }] })), false); }); }); diff --git a/src/vs/platform/localizations/common/localizations.ts b/src/vs/platform/localizations/common/localizations.ts new file mode 100644 index 00000000000..bf11d96da2e --- /dev/null +++ b/src/vs/platform/localizations/common/localizations.ts @@ -0,0 +1,92 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import { localize } from 'vs/nls'; +import { ExtensionsRegistry } from 'vs/platform/extensions/common/extensionsRegistry'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; + +export interface ILocalization { + languageId: string; + languageName?: string; + languageNameLocalized?: string; + translations: ITranslation[]; +} + +export interface ITranslation { + id: string; + path: string; +} + +export const ILocalizationsService = createDecorator('localizationsService'); +export interface ILocalizationsService { + _serviceBrand: any; +} + +export function isValidLocalization(localization: ILocalization): boolean { + if (typeof localization.languageId !== 'string') { + return false; + } + if (!Array.isArray(localization.translations) || localization.translations.length === 0) { + return false; + } + for (const translation of localization.translations) { + if (typeof translation.id !== 'string') { + return false; + } + if (typeof translation.path !== 'string') { + return false; + } + } + if (localization.languageName && typeof localization.languageName !== 'string') { + return false; + } + if (localization.languageNameLocalized && typeof localization.languageNameLocalized !== 'string') { + return false; + } + return true; +} + +ExtensionsRegistry.registerExtensionPoint('localizations', [], { + description: localize('vscode.extension.contributes.localizations', "Contributes localizations to the editor"), + type: 'array', + items: { + type: 'object', + properties: { + languageId: { + description: localize('vscode.extension.contributes.localizations.languageId', 'Id of the language into which the display strings are translated.'), + type: 'string' + }, + languageName: { + description: localize('vscode.extension.contributes.localizations.languageName', 'Name of the language in English.'), + type: 'string' + }, + languageNameLocalized: { + description: localize('vscode.extension.contributes.localizations.languageNameLocalized', 'Name of the language in contributed language.'), + type: 'string' + }, + translations: { + description: localize('vscode.extension.contributes.localizations.translations', 'List of translations associated to the language.'), + type: 'array', + default: [], + items: { + type: 'object', + properties: { + id: { + type: 'string', + description: localize('vscode.extension.contributes.localizations.translations.id', "Id of VS Code or Extension for which this translation is contributed to. Id of VS Code is always `vscode` and of extension should be in format `publisherId.extensionName`."), + pattern: '^((vscode)|([a-z0-9A-Z][a-z0-9\-A-Z]*)\\.([a-z0-9A-Z][a-z0-9\-A-Z]*))$', + patternErrorMessage: localize('vscode.extension.contributes.localizations.translations.id.pattern', "Id should be `vscode` or in format `publisherId.extensionName` for translating VS code or an extension respectively.") + }, + path: { + type: 'string', + description: localize('vscode.extension.contributes.localizations.translations.path', "A relative path to a file containing translations for the language.") + } + } + } + } + } + } +}); \ No newline at end of file diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/languagePackExtensions.ts b/src/vs/platform/localizations/node/localizations.ts similarity index 52% rename from src/vs/code/electron-browser/sharedProcess/contrib/languagePackExtensions.ts rename to src/vs/platform/localizations/node/localizations.ts index 824a6c2ee62..b75c06696fb 100644 --- a/src/vs/code/electron-browser/sharedProcess/contrib/languagePackExtensions.ts +++ b/src/vs/platform/localizations/node/localizations.ts @@ -4,22 +4,27 @@ *--------------------------------------------------------------------------------------------*/ import * as pfs from 'vs/base/node/pfs'; +import { createHash } from 'crypto'; import { IExtensionManagementService, ILocalExtension, IExtensionIdentifier } from 'vs/platform/extensionManagement/common/extensionManagement'; import { Disposable } from 'vs/base/common/lifecycle'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { join } from 'vs/base/common/paths'; import { TPromise } from 'vs/base/common/winjs.base'; import { Limiter } from 'vs/base/common/async'; -import { areSameExtensions, getGalleryExtensionIdFromLocal, getIdFromLocalExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { areSameExtensions, getGalleryExtensionIdFromLocal } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ILogService } from 'vs/platform/log/common/log'; +import { isValidLocalization } from 'vs/platform/localizations/common/localizations'; -interface ILanguageSource { - extensionIdentifier: IExtensionIdentifier; - version: string; - translations: string; +interface ILanguagePack { + hash: string; + extensions: { + extensionIdentifier: IExtensionIdentifier; + version: string; + }[]; + translations: { [id: string]: string }; } -export class LanguagePackExtensions extends Disposable { +export class LanguagePacksCache extends Disposable { private languagePacksFilePath: string; private languagePacksFileLimiter: Limiter; @@ -39,64 +44,81 @@ export class LanguagePackExtensions extends Disposable { this.reset(); } - private reset(): void { - this.extensionManagementService.getInstalled() - .then(installed => { - this.withLanguagePacks(languagePacks => { - for (const language of Object.keys(languagePacks)) { - languagePacks[language] = []; - } - this.addLanguagePacksFromExtensions(languagePacks, ...installed); - }); - }); - } - private onDidInstallExtension(extension: ILocalExtension): void { if (extension && extension.manifest && extension.manifest.contributes && extension.manifest.contributes.localizations && extension.manifest.contributes.localizations.length) { this.logService.debug('Adding language packs from the extension', extension.identifier.id); - this.withLanguagePacks(languagePacks => { - this.removeLanguagePacksFromExtensions(languagePacks, { id: getGalleryExtensionIdFromLocal(extension), uuid: extension.identifier.uuid }); - this.addLanguagePacksFromExtensions(languagePacks, extension); - }); + this.reset(); } } private onDidUninstallExtension(identifier: IExtensionIdentifier): void { - this.logService.debug('Removing language packs from the extension', identifier.id); - this.withLanguagePacks(languagePacks => this.removeLanguagePacksFromExtensions(languagePacks, { id: getIdFromLocalExtensionId(identifier.id), uuid: identifier.uuid })); + if (this.withLanguagePacks(languagePacks => Object.keys(languagePacks).some(language => languagePacks[language] && languagePacks[language].extensions.some(e => areSameExtensions(e.extensionIdentifier, identifier))))) { + this.logService.debug('Removing language packs from the extension', identifier.id); + this.reset(); + } } - private addLanguagePacksFromExtensions(languagePacks: { [language: string]: ILanguageSource[] }, ...extensions: ILocalExtension[]): void { + private reset(): void { + this.extensionManagementService.getInstalled() + .then(installed => { + this.withLanguagePacks(languagePacks => { + Object.keys(languagePacks).forEach(language => languagePacks[language] = undefined); + this.createLanguagePacksFromExtensions(languagePacks, ...installed); + }); + }); + } + + private createLanguagePacksFromExtensions(languagePacks: { [language: string]: ILanguagePack }, ...extensions: ILocalExtension[]): void { for (const extension of extensions) { if (extension && extension.manifest && extension.manifest.contributes && extension.manifest.contributes.localizations && extension.manifest.contributes.localizations.length) { - const extensionIdentifier = { id: getGalleryExtensionIdFromLocal(extension), uuid: extension.identifier.uuid }; - for (const localizationContribution of extension.manifest.contributes.localizations) { - if (localizationContribution.languageId && localizationContribution.translations) { - const languageSources = languagePacks[localizationContribution.languageId] || []; - languageSources.splice(0, 0, { extensionIdentifier, translations: join(extension.path, localizationContribution.translations), version: extension.manifest.version }); - languagePacks[localizationContribution.languageId] = languageSources; - } + this.createLanguagePacksFromExtension(languagePacks, extension); + } + } + Object.keys(languagePacks).forEach(languageId => this.updateHash(languagePacks[languageId])); + } + + private createLanguagePacksFromExtension(languagePacks: { [language: string]: ILanguagePack }, extension: ILocalExtension): void { + const extensionIdentifier = { id: getGalleryExtensionIdFromLocal(extension), uuid: extension.identifier.uuid }; + for (const localizationContribution of extension.manifest.contributes.localizations) { + if (isValidLocalization(localizationContribution)) { + let languagePack = languagePacks[localizationContribution.languageId]; + if (!languagePack) { + languagePack = { hash: '', extensions: [], translations: {} }; + languagePacks[localizationContribution.languageId] = languagePack; + } + let extensionInLanguagePack = languagePack.extensions.filter(e => areSameExtensions(e.extensionIdentifier, extensionIdentifier))[0]; + if (extensionInLanguagePack) { + extensionInLanguagePack.version = extension.manifest.version; + } else { + languagePack.extensions.push({ extensionIdentifier, version: extension.manifest.version }); + } + for (const translation of localizationContribution.translations) { + languagePack.translations[translation.id] = translation.path; } } } } - private removeLanguagePacksFromExtensions(languagePacks: { [language: string]: ILanguageSource[] }, ...extensionIdentifiers: IExtensionIdentifier[]): void { - for (const language of Object.keys(languagePacks)) { - languagePacks[language] = languagePacks[language].filter(languageSource => !extensionIdentifiers.some(extensionIdentifier => areSameExtensions(extensionIdentifier, languageSource.extensionIdentifier))); + private updateHash(languagePack: ILanguagePack): void { + if (languagePack) { + const md5 = createHash('md5'); + for (const extension of languagePack.extensions) { + md5.update(extension.extensionIdentifier.uuid || extension.extensionIdentifier.id).update(extension.version); + } + languagePack.hash = md5.digest('hex'); } } - private withLanguagePacks(fn: (languagePacks: { [language: string]: ILanguageSource[] }) => T): TPromise { + private withLanguagePacks(fn: (languagePacks: { [language: string]: ILanguagePack }) => T): TPromise { return this.languagePacksFileLimiter.queue(() => { let result: T = null; return pfs.readFile(this.languagePacksFilePath, 'utf8') .then(null, err => err.code === 'ENOENT' ? TPromise.as('{}') : TPromise.wrapError(err)) - .then<{ [language: string]: ILanguageSource[] }>(raw => { try { return JSON.parse(raw); } catch (e) { return {}; } }) + .then<{ [language: string]: ILanguagePack }>(raw => { try { return JSON.parse(raw); } catch (e) { return {}; } }) .then(languagePacks => { result = fn(languagePacks); return languagePacks; }) .then(languagePacks => { for (const language of Object.keys(languagePacks)) { - if (!(languagePacks[language] && languagePacks[language].length)) { + if (!languagePacks[language]) { delete languagePacks[language]; } } diff --git a/src/vs/workbench/api/browser/localizationsExtensionPoint.ts b/src/vs/workbench/api/browser/localizationsExtensionPoint.ts deleted file mode 100644 index cd92ed0e803..00000000000 --- a/src/vs/workbench/api/browser/localizationsExtensionPoint.ts +++ /dev/null @@ -1,70 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -'use strict'; - -import { localize } from 'vs/nls'; -import { IJSONSchema } from 'vs/base/common/jsonSchema'; -import { ExtensionMessageCollector, ExtensionsRegistry } from 'vs/platform/extensions/common/extensionsRegistry'; - -namespace schema { - - // --localizations contribution point - - export interface ILocalizationDescriptor { - languageId: string; - languageName: string; - translations: string; - } - - export function validateLocalizationDescriptors(localizationDescriptors: ILocalizationDescriptor[], collector: ExtensionMessageCollector): boolean { - if (!Array.isArray(localizationDescriptors)) { - collector.error(localize('requirearray', "localizations must be an array")); - return false; - } - - for (let descriptor of localizationDescriptors) { - if (typeof descriptor.languageId !== 'string') { - collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'languageId')); - return false; - } - if (typeof descriptor.languageName !== 'string') { - collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'languageName')); - return false; - } - if (descriptor.translations && typeof descriptor.translations !== 'string') { - collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'translations')); - return false; - } - } - - return true; - } - - export const localizationsContribution: IJSONSchema = { - description: localize('vscode.extension.contributes.localizations', "Contributes localizations to the editor"), - type: 'array', - items: { - type: 'object', - properties: { - id: { - description: localize('vscode.extension.contributes.localizations.languageId', 'Id of the language into which the display strings are translated.'), - type: 'string' - }, - name: { - description: localize('vscode.extension.contributes.localizations.languageName', 'Name of the language into which the display strings are translated.'), - type: 'string' - }, - translations: { - description: localize('vscode.extension.contributes.localizations.translations', 'A relative path to the folder containing all translation files for the contributed language.'), - type: 'string', - default: 'translations' - } - } - } - }; -} - -ExtensionsRegistry.registerExtensionPoint('localizations', [], schema.localizationsContribution) - .setHandler((extensions) => extensions.forEach(extension => schema.validateLocalizationDescriptors(extension.value, extension.collector))); \ No newline at end of file diff --git a/src/vs/workbench/parts/extensions/browser/extensionEditor.ts b/src/vs/workbench/parts/extensions/browser/extensionEditor.ts index 8e6b31e1f55..f2dc4747e31 100644 --- a/src/vs/workbench/parts/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/parts/extensions/browser/extensionEditor.ts @@ -634,8 +634,8 @@ export class ExtensionEditor extends BaseEditor { const details = $('details', { open: true, ontoggle: onDetailsToggle }, $('summary', null, localize('localizations', "Localizations ({0})", localizations.length)), $('table', null, - $('tr', null, $('th', null, localize('localizations language id', "Language Id")), $('th', null, localize('localizations language name', "Langauge Name")), $('th', null, localize('translations location', "Translations Location"))), - ...localizations.map(localization => $('tr', null, $('td', null, localization.languageId), $('td', null, localization.languageName), $('td', null, localization.translations))) + $('tr', null, $('th', null, localize('localizations language id', "Language Id")), $('th', null, localize('localizations language name', "Langauge Name")), $('th', null, localize('localizations localized language name', "Langauge Name (Localized)"))), + ...localizations.map(localization => $('tr', null, $('td', null, localization.languageId), $('td', null, localization.languageName), $('td', null, localization.languageNameLocalized))) ) ); diff --git a/src/vs/workbench/workbench.main.ts b/src/vs/workbench/workbench.main.ts index 2966cea4559..8149e729d2a 100644 --- a/src/vs/workbench/workbench.main.ts +++ b/src/vs/workbench/workbench.main.ts @@ -22,7 +22,7 @@ import 'vs/platform/actions/electron-browser/menusExtensionPoint'; import 'vs/workbench/api/browser/viewsExtensionPoint'; // Localizations -import 'vs/workbench/api/browser/localizationsExtensionPoint'; +import 'vs/platform/localizations/common/localizations'; // Workbench import 'vs/workbench/browser/actions/toggleActivityBarVisibility';