diff --git a/README.md b/README.md
index dd009c3a..619db1ee 100644
--- a/README.md
+++ b/README.md
@@ -236,6 +236,8 @@ Additionally, several packages that include the VSCode version of some services
- Survey/feedback support
- **Update**
- Update detection, release notes...
+- **Localization**
+ - Register callbacks to update the display language from the VSCode UI (either from the `Set Display Language` command or from the extension gallery extension packs)
Usage:
diff --git a/demo/package-lock.json b/demo/package-lock.json
index b9ba18cb..79148735 100644
--- a/demo/package-lock.json
+++ b/demo/package-lock.json
@@ -79,6 +79,7 @@
"@codingame/monaco-vscode-layout-service-override": "file:../dist/service-override-layout",
"@codingame/monaco-vscode-less-default-extension": "file:../dist/default-extension-less",
"@codingame/monaco-vscode-lifecycle-service-override": "file:../dist/service-override-lifecycle",
+ "@codingame/monaco-vscode-localization-service-override": "file:../dist/service-override-localization",
"@codingame/monaco-vscode-log-default-extension": "file:../dist/default-extension-log",
"@codingame/monaco-vscode-log-service-override": "file:../dist/service-override-log",
"@codingame/monaco-vscode-lua-default-extension": "file:../dist/default-extension-lua",
@@ -826,7 +827,7 @@
"license": "MIT",
"dependencies": {
"css-url-parser": "^1.1.3",
- "memfs": "^4.8.0",
+ "memfs": "^4.8.1",
"mime-types": "^2.1.35",
"vscode": "npm:@codingame/monaco-vscode-api@^0.0.0-semantic-release",
"yauzl": "^3.0.0"
@@ -1079,6 +1080,22 @@
"vscode": "npm:@codingame/monaco-vscode-api@^0.0.0-semantic-release"
}
},
+ "../dist/service-override-localization": {
+ "version": "0.0.0-semantic-release",
+ "license": "MIT",
+ "dependencies": {
+ "@xterm/addon-canvas": "0.7.0-beta.12",
+ "@xterm/addon-image": "0.8.0-beta.12",
+ "@xterm/addon-search": "0.15.0-beta.12",
+ "@xterm/addon-serialize": "0.13.0-beta.12",
+ "@xterm/addon-unicode11": "0.8.0-beta.12",
+ "@xterm/addon-webgl": "0.18.0-beta.12",
+ "@xterm/xterm": "5.5.0-beta.12",
+ "vscode": "npm:@codingame/monaco-vscode-api@^0.0.0-semantic-release",
+ "vscode-oniguruma": "1.7.0",
+ "vscode-textmate": "9.0.0"
+ }
+ },
"../dist/service-override-log": {
"name": "@codingame/monaco-vscode-log-service-override",
"version": "0.0.0-semantic-release",
@@ -1822,6 +1839,10 @@
"resolved": "../dist/service-override-lifecycle",
"link": true
},
+ "node_modules/@codingame/monaco-vscode-localization-service-override": {
+ "resolved": "../dist/service-override-localization",
+ "link": true
+ },
"node_modules/@codingame/monaco-vscode-log-default-extension": {
"resolved": "../dist/default-extension-log",
"link": true
diff --git a/demo/package.json b/demo/package.json
index 11311403..82b91eb7 100644
--- a/demo/package.json
+++ b/demo/package.json
@@ -180,6 +180,7 @@
"@codingame/monaco-vscode-workspace-trust-service-override": "file:../dist/service-override-workspace-trust",
"@codingame/monaco-vscode-xml-default-extension": "file:../dist/default-extension-xml",
"@codingame/monaco-vscode-yaml-default-extension": "file:../dist/default-extension-yaml",
+ "@codingame/monaco-vscode-localization-service-override": "file:../dist/service-override-localization",
"ansi-colors": "^4.1.3",
"dockerode": "^4.0.2",
"express": "^4.19.2",
diff --git a/demo/src/main.common.ts b/demo/src/main.common.ts
index a8f05e25..4f8831a0 100644
--- a/demo/src/main.common.ts
+++ b/demo/src/main.common.ts
@@ -80,19 +80,6 @@ void getApi().then(async vscode => {
code: 42
}])
- const locale = new URLSearchParams(window.location.search).get('locale') ?? ''
- const select: HTMLSelectElement = document.querySelector('#localeSelect')!
- select.value = locale
- select.addEventListener('change', () => {
- const url = new URL(window.location.href)
- if (select.value !== '') {
- url.searchParams.set('locale', select.value)
- } else {
- url.searchParams.delete('locale')
- }
- window.location.href = url.toString()
- })
-
document.querySelector('#toggleFullWorkbench')!.addEventListener('click', async () => {
const url = new URL(window.location.href)
if (url.searchParams.get('mode') === 'full-workbench') {
diff --git a/demo/src/setup.common.ts b/demo/src/setup.common.ts
index 52114d11..228a3adc 100644
--- a/demo/src/setup.common.ts
+++ b/demo/src/setup.common.ts
@@ -55,6 +55,7 @@ import getSpeechServiceOverride from '@codingame/monaco-vscode-speech-service-ov
import getSurveyServiceOverride from '@codingame/monaco-vscode-survey-service-override'
import getUpdateServiceOverride from '@codingame/monaco-vscode-update-service-override'
import getExplorerServiceOverride from '@codingame/monaco-vscode-explorer-service-override'
+import getLocalizationServiceOverride from '@codingame/monaco-vscode-localization-service-override'
import { EnvironmentOverride } from 'vscode/workbench'
import { Worker } from './tools/crossOriginWorker'
import defaultKeybindings from './user/keybindings.json?raw'
@@ -248,6 +249,8 @@ export const constructOptions: IWorkbenchConstructionOptions = {
message: 'Welcome in monaco-vscode-api demo'
},
productConfiguration: {
+ nameShort: 'monaco-vscode-api',
+ nameLong: 'monaco-vscode-api',
extensionsGallery: {
serviceUrl: 'https://open-vsx.org/vscode/gallery',
itemUrl: 'https://open-vsx.org/vscode/item',
@@ -322,5 +325,66 @@ export const commonServices: IEditorOverrideServices = {
...getSpeechServiceOverride(),
...getSurveyServiceOverride(),
...getUpdateServiceOverride(),
- ...getExplorerServiceOverride()
+ ...getExplorerServiceOverride(),
+ ...getLocalizationServiceOverride({
+ async clearLocale () {
+ const url = new URL(window.location.href)
+ url.searchParams.delete('locale')
+ window.history.pushState(null, '', url.toString())
+ },
+ async setLocale (id) {
+ const url = new URL(window.location.href)
+ url.searchParams.set('locale', id)
+ window.history.pushState(null, '', url.toString())
+ },
+ availableLanguages: [{
+ locale: 'en',
+ languageName: 'English'
+ }, {
+ locale: 'cs',
+ languageName: 'Czech'
+ }, {
+ locale: 'de',
+ languageName: 'German'
+ }, {
+ locale: 'es',
+ languageName: 'Spanish'
+ }, {
+ locale: 'fr',
+ languageName: 'French'
+ }, {
+ locale: 'it',
+ languageName: 'Italian'
+ }, {
+ locale: 'ja',
+ languageName: 'Japanese'
+ }, {
+ locale: 'ko',
+ languageName: 'Korean'
+ }, {
+ locale: 'pl',
+ languageName: 'Polish'
+ }, {
+ locale: 'pt-br',
+ languageName: 'Portuguese (Brazil)'
+ }, {
+ locale: 'qps-ploc',
+ languageName: 'Pseudo Language'
+ }, {
+ locale: 'ru',
+ languageName: 'Russian'
+ }, {
+ locale: 'tr',
+ languageName: 'Turkish'
+ }, {
+ locale: 'zh-hans',
+ languageName: 'Chinese (Simplified)'
+ }, {
+ locale: 'zh-hant',
+ languageName: 'Chinese (Traditional)'
+ }, {
+ locale: 'en',
+ languageName: 'English'
+ }]
+ })
}
diff --git a/demo/src/setup.views.ts b/demo/src/setup.views.ts
index 893f980c..77bd0596 100644
--- a/demo/src/setup.views.ts
+++ b/demo/src/setup.views.ts
@@ -41,25 +41,6 @@ container.innerHTML = `
-
-
-
diff --git a/demo/src/setup.workbench.ts b/demo/src/setup.workbench.ts
index 29fd964b..cade7f10 100644
--- a/demo/src/setup.workbench.ts
+++ b/demo/src/setup.workbench.ts
@@ -22,25 +22,6 @@ buttons.innerHTML = `
-
-
-
`
document.body.append(buttons)
diff --git a/rollup/rollup.config.ts b/rollup/rollup.config.ts
index dae4140e..2f10d2fb 100644
--- a/rollup/rollup.config.ts
+++ b/rollup/rollup.config.ts
@@ -75,7 +75,7 @@ const REMOVE_WORKBENCH_CONTRIBUTIONS = new Set([
/**
* root files that should never be extracted from the main package to a service override package
*/
-const SHARED_ROOT_FILES_BETWEEN_PACKAGES = ['services.js', 'extensions.js', 'monaco.js', 'assets.js', 'lifecycle.js', 'workbench.js', 'missing-services.js']
+const SHARED_ROOT_FILES_BETWEEN_PACKAGES = ['services.js', 'extensions.js', 'monaco.js', 'assets.js', 'lifecycle.js', 'workbench.js', 'missing-services.js', 'l10n.js']
/**
* Files to expose in the editor-api package (just exporting everyting from the corresponding VSCode file)
* for compability with libraries that import internal monaco-editor modules
diff --git a/rollup/rollup.language-packs.ts b/rollup/rollup.language-packs.ts
index 4fd3f7a6..d347114f 100644
--- a/rollup/rollup.language-packs.ts
+++ b/rollup/rollup.language-packs.ts
@@ -82,7 +82,7 @@ export default rollup.defineConfig([
return `
import { registerLocalization } from 'vscode/l10n'
import content from '${path.resolve(id, mainTranslation.path)}'
-registerLocalization('${mainLocalization.languageId}', content, {
+registerLocalization('${packageJson.publisher}.${packageJson.name}', '${mainLocalization.languageId}', content, {
${Object.entries(translationAssets).map(([id, assetRef]) => ` '${id}': new URL(import.meta.ROLLUP_FILE_URL_${assetRef}, import.meta.url).href`).join(',\n')}
})
`
diff --git a/src/l10n.ts b/src/l10n.ts
index ad3e930a..99ca20e4 100644
--- a/src/l10n.ts
+++ b/src/l10n.ts
@@ -1,21 +1,39 @@
import { setLocale, isInitialized } from 'vs/nls'
const extensionTranslationsUri: Record> = {}
+let currentLocaleExtensionId: string | undefined
+let availableLocales: Set = new Set()
-function registerLocalization (language: string, main: Record>, extensionTranslationsUris: Record): void {
+function setAvailableLocales (locales: Set): void {
+ availableLocales = locales
+}
+
+function isLocaleAvailable (locale: string): boolean {
+ return availableLocales.has(locale)
+}
+
+function registerLocalization (extensionId: string, language: string, main: Record>, extensionTranslationsUris: Record): void {
if (isInitialized()) {
console.error('Some parts of VSCode are already initialized, make sure the language pack is loaded before anything else or some translations will be missing')
}
setLocale(language, main)
extensionTranslationsUri[language] = extensionTranslationsUris
+ currentLocaleExtensionId = extensionId
}
function getBuiltInExtensionTranslationsUris (language: string): Record | undefined {
return extensionTranslationsUri[language]
}
+function getExtensionIdProvidingCurrentLocale (): string | undefined {
+ return currentLocaleExtensionId
+}
+
export {
registerLocalization,
- getBuiltInExtensionTranslationsUris
+ getBuiltInExtensionTranslationsUris,
+ getExtensionIdProvidingCurrentLocale,
+ setAvailableLocales,
+ isLocaleAvailable
}
diff --git a/src/missing-services.ts b/src/missing-services.ts
index ca4d0689..00c57b4e 100644
--- a/src/missing-services.ts
+++ b/src/missing-services.ts
@@ -198,7 +198,9 @@ import { IAuthenticationAccessService } from 'vs/workbench/services/authenticati
import { IAuthenticationUsageService } from 'vs/workbench/services/authentication/browser/authenticationUsageService'
import { ICustomEditorLabelService } from 'vs/workbench/services/editor/common/customEditorLabelService'
import { IExtensionsProfileScannerService } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService'
-import { getBuiltInExtensionTranslationsUris } from './l10n'
+import { createInstanceCapabilityEventMultiplexer } from 'vs/workbench/contrib/terminal/browser/terminalEvents'
+import { TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'
+import { getBuiltInExtensionTranslationsUris, getExtensionIdProvidingCurrentLocale } from './l10n'
import { unsupported } from './tools'
registerSingleton(ILoggerService, class NullLoggerService extends AbstractLoggerService {
@@ -1656,10 +1658,8 @@ registerSingleton(ITerminalService, class TerminalService implements ITerminalSe
onAnyInstanceProcessIdReady = Event.None
onAnyInstanceSelectionChange = Event.None
onAnyInstanceTitleChange = Event.None
- createOnInstanceEvent = unsupported
- createOnInstanceCapabilityEvent = unsupported
- onInstanceEvent = unsupported
- onInstanceCapabilityEvent = unsupported
+ createOnInstanceEvent = () => Event.None
+ createOnInstanceCapabilityEvent = (capabilityId: T) => createInstanceCapabilityEventMultiplexer([], Event.None, Event.None, capabilityId, () => Event.None)
createDetachedTerminal = unsupported
onDidChangeSelection = Event.None
@@ -2466,8 +2466,8 @@ registerSingleton(IInteractiveDocumentService, class InteractiveDocumentService
registerSingleton(IActiveLanguagePackService, class ActiveLanguagePackService implements IActiveLanguagePackService {
readonly _serviceBrand: undefined
- getExtensionIdProvidingCurrentLocale () {
- return Promise.resolve(undefined)
+ async getExtensionIdProvidingCurrentLocale (): Promise {
+ return getExtensionIdProvidingCurrentLocale()
}
}, InstantiationType.Eager)
diff --git a/src/service-override/extensionGallery.ts b/src/service-override/extensionGallery.ts
index d309799c..cb9f0c4a 100644
--- a/src/service-override/extensionGallery.ts
+++ b/src/service-override/extensionGallery.ts
@@ -3,7 +3,8 @@ import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'
import { IExtensionGalleryService, IExtensionManagementService, IExtensionTipsService, IGlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement'
import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService'
import { GlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionEnablementService'
-import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'
+import { IExtension as IContribExtension, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'
+import { getLocale } from 'vs/platform/languagePacks/common/languagePacks'
import { ExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService'
import { IExtensionManagementServerService, IWebExtensionsScannerService, IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'
import { ExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagementServerService'
@@ -33,6 +34,7 @@ import { IExtensionFeaturesManagementService } from 'vs/workbench/services/exten
import { ExtensionFeaturesManagementService } from 'vs/workbench/services/extensionManagement/common/extensionFeaturesManagemetService'
import { registerAssets } from '../assets'
import { getExtensionManifests } from '../extensions'
+import { isLocaleAvailable } from '../l10n'
import 'vs/workbench/contrib/extensions/browser/extensions.contribution'
import 'vs/workbench/contrib/extensions/browser/extensions.web.contribution'
@@ -78,6 +80,16 @@ class CustomBuiltinExtensionsScannerService implements IBuiltinExtensionsScanner
}
}
+class ExtensionsWorkbenchServiceOverride extends ExtensionsWorkbenchService {
+ override canSetLanguage (extension: IContribExtension): boolean {
+ if (super.canSetLanguage(extension)) {
+ const locale = getLocale(extension.gallery!)!
+ return isLocaleAvailable(locale)
+ }
+ return false
+ }
+}
+
export interface ExtensionGalleryOptions {
/**
* Whether we should only allow for web extensions to be installed, this is generally
@@ -90,7 +102,7 @@ export default function getServiceOverride (options: ExtensionGalleryOptions = {
return {
[IExtensionGalleryService.toString()]: new SyncDescriptor(ExtensionGalleryService, [], true),
[IGlobalExtensionEnablementService.toString()]: new SyncDescriptor(GlobalExtensionEnablementService, [], true),
- [IExtensionsWorkbenchService.toString()]: new SyncDescriptor(ExtensionsWorkbenchService, [], true),
+ [IExtensionsWorkbenchService.toString()]: new SyncDescriptor(ExtensionsWorkbenchServiceOverride, [], true),
[IExtensionManagementServerService.toString()]: new SyncDescriptor(ExtensionManagementServerServiceOverride, [options.webOnly], true),
[IExtensionRecommendationsService.toString()]: new SyncDescriptor(ExtensionRecommendationsService, [], true),
[IExtensionRecommendationNotificationService.toString()]: new SyncDescriptor(ExtensionRecommendationNotificationService, [], true),
diff --git a/src/service-override/localization.ts b/src/service-override/localization.ts
new file mode 100644
index 00000000..cf38b0f5
--- /dev/null
+++ b/src/service-override/localization.ts
@@ -0,0 +1,122 @@
+import { IEditorOverrideServices } from 'vs/editor/standalone/browser/standaloneServices'
+import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'
+import { ILanguagePackItem, ILanguagePackService } from 'vs/platform/languagePacks/common/languagePacks'
+import { ILocaleService } from 'vs/workbench/services/localization/common/locale'
+import { URI } from 'vs/workbench/workbench.web.main'
+import { IDialogService } from 'vs/platform/dialogs/common/dialogs'
+import { IHostService } from 'vs/workbench/services/host/browser/host'
+import { IProductService } from 'vs/platform/product/common/productService'
+import { localize, localizeWithPath } from 'vs/nls'
+import { Language, language } from 'vs/base/common/platform'
+import { getBuiltInExtensionTranslationsUris, setAvailableLocales } from '../l10n'
+import 'vs/workbench/contrib/localization/common/localization.contribution'
+
+interface AvailableLanguage {
+ locale: string
+ languageName?: string
+}
+
+interface LocalizationOptions {
+ setLocale (id: string): Promise
+ clearLocale(): Promise
+ availableLanguages: AvailableLanguage[]
+}
+
+class LocaleService implements ILocaleService {
+ _serviceBrand: undefined
+
+ constructor (
+ private options: LocalizationOptions,
+ @IDialogService private readonly dialogService: IDialogService,
+ @IHostService private readonly hostService: IHostService,
+ @IProductService private readonly productService: IProductService
+ ) {
+ }
+
+ async setLocale (languagePackItem: ILanguagePackItem): Promise {
+ const locale = languagePackItem.id
+
+ if (locale === Language.value() || (locale == null && Language.value() === navigator.language.toLowerCase())) {
+ return
+ }
+
+ if (locale == null) {
+ await this.options.clearLocale()
+ } else {
+ await this.options.setLocale(locale)
+ }
+
+ const restartDialog = await this.dialogService.confirm({
+ type: 'info',
+ message: localizeWithPath('vs/workbench/services/localization/browser/localeService', 'relaunchDisplayLanguageMessage', 'To change the display language, {0} needs to reload', this.productService.nameLong),
+ detail: localizeWithPath('vs/workbench/services/localization/browser/localeService', 'relaunchDisplayLanguageDetail', 'Press the reload button to refresh the page and set the display language to {0}.', languagePackItem.label),
+ primaryButton: localize({ key: 'reload', comment: ['&& denotes a mnemonic character'] }, '&&Reload')
+ })
+
+ if (restartDialog.confirmed) {
+ await this.hostService.restart()
+ }
+ }
+
+ async clearLocalePreference (): Promise {
+ await this.options.clearLocale()
+
+ const restartDialog = await this.dialogService.confirm({
+ type: 'info',
+ message: localizeWithPath('vs/workbench/services/localization/browser/localeService', 'clearDisplayLanguageMessage', 'To change the display language, {0} needs to reload', this.productService.nameLong),
+ detail: localizeWithPath('vs/workbench/services/localization/browser/localeService', 'clearDisplayLanguageDetail', "Press the reload button to refresh the page and use your browser's language."),
+ primaryButton: localize({ key: 'reload', comment: ['&& denotes a mnemonic character'] }, '&&Reload')
+ })
+
+ if (restartDialog.confirmed) {
+ await this.hostService.restart()
+ }
+ }
+}
+
+class LanguagePackService implements ILanguagePackService {
+ _serviceBrand: undefined
+
+ constructor (
+ private options: LocalizationOptions
+ ) {
+ setAvailableLocales(new Set(options.availableLanguages.map(lang => lang.locale)))
+ }
+
+ async getAvailableLanguages (): Promise {
+ return this.options.availableLanguages.map(({ locale, languageName }) => {
+ const label = languageName ?? locale
+ let description: string | undefined
+ if (label !== locale) {
+ description = `(${locale})`
+ }
+
+ if (locale.toLowerCase() === language.toLowerCase()) {
+ description ??= ''
+ description += localizeWithPath('vs/platform/languagePacks/common/languagePacks', 'currentDisplayLanguage', ' (Current)')
+ }
+
+ return {
+ id: locale,
+ label,
+ description
+ }
+ })
+ }
+
+ async getInstalledLanguages (): Promise {
+ return []
+ }
+
+ async getBuiltInExtensionTranslationsUri (id: string, language: string): Promise {
+ const uri = getBuiltInExtensionTranslationsUris(language)?.[id]
+ return uri != null ? URI.parse(uri) : undefined
+ }
+}
+
+export default function getServiceOverride (options: LocalizationOptions): IEditorOverrideServices {
+ return {
+ [ILocaleService.toString()]: new SyncDescriptor(LocaleService, [options], true), // maybe custom impl
+ [ILanguagePackService.toString()]: new SyncDescriptor(LanguagePackService, [options], true)
+ }
+}
diff --git a/src/services.ts b/src/services.ts
index 5f483a83..3a52373c 100644
--- a/src/services.ts
+++ b/src/services.ts
@@ -203,6 +203,8 @@ export { IRemoteSocketFactoryService } from 'vs/platform/remote/common/remoteSoc
export { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'
export { ILabelService } from 'vs/platform/label/common/label'
export { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'
+export { ILanguagePackService } from 'vs/platform/languagePacks/common/languagePacks'
+export { ILocaleService } from 'vs/workbench/services/localization/common/locale'
// Export all Notification service parts
export {