Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimize localization #417

Merged
merged 8 commits into from
May 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 104 additions & 68 deletions rollup/rollup.config.ts

Large diffs are not rendered by default.

22 changes: 11 additions & 11 deletions rollup/rollup.language-packs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import * as rollup from 'rollup'
import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'
import { dataToEsm } from '@rollup/pluginutils'
import { PackageJson } from 'type-fest'
import glob from 'fast-glob'
import * as fs from 'fs'
import * as path from 'path'
import { fileURLToPath } from 'url'
Expand All @@ -22,12 +21,7 @@ const locExtensions = fs.readdirSync(LOC_PATH, { withFileTypes: true })
.filter(f => f.isDirectory() && fs.existsSync(path.resolve(LOC_PATH, f.name, 'package.json')))
.map(f => f.name)

const vscodeModules = (await glob('**/vscode/src/**/*.js', {
cwd: DIST_DIR,
onlyFiles: true
})).map(fileName => /vscode\/src\/(.*).js$/.exec(fileName)![1]!)

const usedModules = new Set<string>([...vscodeModules, 'vs/workbench/browser/web.main'])
const nlsMetadata: Record<string, string[]> = JSON.parse((await fs.promises.readFile(path.resolve(DIST_DIR, 'nls.metadata.json'))).toString())

export default rollup.defineConfig([
...locExtensions.map(name => (<rollup.RollupOptions>{
Expand Down Expand Up @@ -92,12 +86,18 @@ ${Object.entries(translationAssets).map(([id, assetRef]) => ` '${id}': new URL(
transform (code, id) {
if (!id.endsWith('.json')) return null

const parsed = JSON.parse(code).contents
// remove keys that we don't use
const filtered = Object.fromEntries(Object.entries(parsed).filter(([key]) => usedModules.has(key)))
const parsed: Record<string, Record<string, string>> = JSON.parse(code).contents

const encoded = Object.fromEntries(Object.entries(nlsMetadata).map(([moduleId, keys]) => {
const values = parsed[moduleId] ?? {}
return [
moduleId,
keys.map(key => values[key])
]
}))

return {
code: dataToEsm(filtered, {
code: dataToEsm(encoded, {
preferConst: true
}),
map: { mappings: '' }
Expand Down
1 change: 1 addition & 0 deletions src/contributions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'vs/workbench/api/common/jsonValidationExtensionPoint'
import 'vs/workbench/services/themes/common/colorExtensionPoint'
import 'vs/workbench/services/themes/common/iconExtensionPoint'
import 'vs/platform/actions/common/actions.contribution'
// Selectively comes from vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts
import 'vs/workbench/contrib/codeEditor/browser/workbenchReferenceSearch'
import 'vs/workbench/contrib/codeEditor/browser/menuPreventer'
Expand Down
12 changes: 10 additions & 2 deletions src/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ import { IFileService } from 'vs/platform/files/common/files.service'
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'
import { IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement.service'
import { StandaloneServices } from 'vs/editor/standalone/browser/standaloneServices'
import { ExtensionManifestTranslator, NlsConfiguration } from 'vs/platform/extensionManagement/common/extensionsScannerService'
import * as platform from 'vs/base/common/platform'
import { IExtensionWithExtHostKind, ExtensionServiceOverride } from './service-override/extensions'
import { CustomSchemas, registerExtensionFile } from './service-override/files'
import { waitServicesReady } from './lifecycle'
import { ExtensionManifestTranslator } from './tools/l10n'
import { throttle, memoized } from './tools'
import { setDefaultApi } from './extension.api'
import { getBuiltInExtensionTranslationsUris } from './l10n'

export type ApiFactory = (extensionId?: string) => Promise<typeof vscode>

Expand Down Expand Up @@ -126,7 +128,13 @@ export function registerExtension (manifest: IExtensionManifest, extHostKind?: E
const instantiationService = StandaloneServices.get(IInstantiationService)
const translator = instantiationService.createInstance(ExtensionManifestTranslator)

const localizedManifest = await translator.translateManifest(realLocation, manifest)
const nlsConfiguration: NlsConfiguration = {
devMode: false,
language: platform.language,
pseudo: platform.language === 'pseudo',
translations: getBuiltInExtensionTranslationsUris(platform.language) ?? {}
}
const localizedManifest = await translator.translateManifest(realLocation, manifest, nlsConfiguration)

const extension: IExtensionWithExtHostKind = {
manifest: localizedManifest,
Expand Down
3 changes: 3 additions & 0 deletions src/override/vs/workbench/browser/web.main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { localizeWithPath } from 'vs/nls'
const _moduleId = 'vs/workbench/browser/web.main'
export const rendererLogLabel = localizeWithPath(_moduleId, 'rendererLog', 'Window')
54 changes: 3 additions & 51 deletions src/service-override/files.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { IEditorOverrideServices, StandaloneServices } from 'vs/editor/standalone/browser/standaloneServices'
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'
import { FileService } from 'vs/platform/files/common/fileService'
import { FileService, mkdirp } from 'vs/platform/files/common/fileService'
import { LogLevel } from 'vs/platform/log/common/log'
import { ILogService } from 'vs/platform/log/common/log.service'
import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'
import { URI } from 'vs/base/common/uri'
import { IFileService } from 'vs/platform/files/common/files.service'
import { FileChangeType, FilePermission, FileSystemProviderCapabilities, FileType, IFileSystemProvider, toFileSystemProviderErrorCode, createFileSystemProviderError, FileSystemProviderError, FileSystemProviderErrorCode, IFileChange, IFileDeleteOptions, IFileOverwriteOptions, IFileSystemProviderWithFileReadWriteCapability, IFileWriteOptions, IStat, IWatchOptions } from 'vs/platform/files/common/files'
import { FileChangeType, FilePermission, FileSystemProviderCapabilities, FileType, IFileSystemProvider, createFileSystemProviderError, FileSystemProviderError, FileSystemProviderErrorCode, IFileChange, IFileDeleteOptions, IFileOverwriteOptions, IFileSystemProviderWithFileReadWriteCapability, IFileWriteOptions, IStat, IWatchOptions } from 'vs/platform/files/common/files'
import { DisposableStore, IDisposable, Disposable, toDisposable } from 'vs/base/common/lifecycle'
import { extUri, joinPath } from 'vs/base/common/resources'
import { Emitter, Event } from 'vs/base/common/event'
Expand All @@ -16,7 +16,6 @@ import { IndexedDBFileSystemProvider, IndexedDBFileSystemProviderErrorData, Inde
import { IndexedDB } from 'vs/base/browser/indexedDB'
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry.service'
import { BufferLogger } from 'vs/platform/log/common/bufferLog'
import { localizeWithPath } from 'vs/nls'
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles.service'
import { BrowserTextFileService } from 'vs/workbench/services/textfile/browser/browserTextFileService'
import { FilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'
Expand Down Expand Up @@ -414,58 +413,11 @@ class OverlayFileSystemProvider implements IFileSystemProviderWithFileReadWriteC
}
}

function resourceForError (resource: URI): string {
if (resource.scheme === Schemas.file) {
return resource.fsPath
}

return resource.toString(true)
}
async function mkdirp (provider: IFileSystemProviderWithFileReadWriteCapability, directory: URI) {
const directoriesToCreate: string[] = []

// mkdir until we reach root
while (!extUri.isEqual(directory, extUri.dirname(directory))) {
try {
const stat = await provider.stat(directory)
if ((stat.type & FileType.Directory) === 0) {
throw new Error(localizeWithPath('mkdirExistsError', "Unable to create folder '{0}' that already exists but is not a directory", resourceForError(directory)))
}

break // we have hit a directory that exists -> good
} catch (error) {
// Bubble up any other error that is not file not found
if (toFileSystemProviderErrorCode(error as Error) !== FileSystemProviderErrorCode.FileNotFound) {
throw error
}

// Upon error, remember directories that need to be created
directoriesToCreate.push(extUri.basename(directory))

// Continue up
directory = extUri.dirname(directory)
}
}

// Create directories as needed
for (let i = directoriesToCreate.length - 1; i >= 0; i--) {
directory = extUri.joinPath(directory, directoriesToCreate[i]!)

try {
await provider.mkdir(directory)
} catch (error) {
if (toFileSystemProviderErrorCode(error as Error) !== FileSystemProviderErrorCode.FileExists) {
throw error
}
}
}
}

class MkdirpOnWriteInMemoryFileSystemProvider extends InMemoryFileSystemProvider {
override async writeFile (resource: URI, content: Uint8Array, opts: IFileWriteOptions): Promise<void> {
// when using overlay providers, the fileservice won't be able to detect that the parent directory doesn't exist
// if another provider has this directory. So it won't create the parent directories on this memory file system.
await mkdirp(this, extUri.dirname(resource))
await mkdirp(extUri, this, extUri.dirname(resource))

return super.writeFile(resource, content, opts)
}
Expand Down
76 changes: 16 additions & 60 deletions src/service-override/localization.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { IEditorOverrideServices } from 'vs/editor/standalone/browser/standaloneServices'
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'
import { ILanguagePackItem } from 'vs/platform/languagePacks/common/languagePacks'
import { ILanguagePackItem, LanguagePackBaseService } from 'vs/platform/languagePacks/common/languagePacks'
import { ILanguagePackService } from 'vs/platform/languagePacks/common/languagePacks.service'
import { ILocaleService } from 'vs/workbench/services/localization/common/locale.service'
import { IDialogService } from 'vs/platform/dialogs/common/dialogs.service'
import { IHostService } from 'vs/workbench/services/host/browser/host.service'
import { IProductService } from 'vs/platform/product/common/productService.service'
import { localize, localizeWithPath } from 'vs/nls'
import { Language, language } from 'vs/base/common/platform'
import { URI } from 'vs/base/common/uri'
import { AbstractLocaleService } from 'vs/workbench/services/localization/browser/localeService'
import { IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement.service'
import { getBuiltInExtensionTranslationsUris, setAvailableLocales } from '../l10n'
import 'vs/workbench/contrib/localization/browser/localization.contribution'

Expand All @@ -23,85 +23,41 @@ export interface LocalizationOptions {
availableLanguages: AvailableLanguage[]
}

class LocaleService implements ILocaleService {
_serviceBrand: undefined

class LocaleService extends AbstractLocaleService {
constructor (
private options: LocalizationOptions,
@IDialogService private readonly dialogService: IDialogService,
@IHostService private readonly hostService: IHostService,
@IProductService private readonly productService: IProductService
@IDialogService dialogService: IDialogService,
@IHostService hostService: IHostService,
@IProductService productService: IProductService
) {
super(dialogService, hostService, productService)
}

async setLocale (languagePackItem: ILanguagePackItem): Promise<void> {
const locale = languagePackItem.id

if (locale === Language.value() || (locale == null && Language.value() === navigator.language.toLowerCase())) {
return
}

override async storeLocale (locale: string | undefined): Promise<void> {
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<void> {
override async clearLocale (): Promise<void> {
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

class LanguagePackService extends LanguagePackBaseService implements ILanguagePackService {
constructor (
private options: LocalizationOptions
private options: LocalizationOptions,
@IExtensionGalleryService extensionGalleryService: IExtensionGalleryService
) {
super(extensionGalleryService)
setAvailableLocales(new Set(options.availableLanguages.map(lang => lang.locale)))
}

async getAvailableLanguages (): Promise<ILanguagePackItem[]> {
override async getAvailableLanguages (): Promise<ILanguagePackItem[]> {
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
}
return this.createQuickPickItem(locale, languageName)
})
}

Expand Down
4 changes: 2 additions & 2 deletions src/service-override/log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { ILogService, ILoggerService } from 'vs/platform/log/common/log.service'
import { FileLoggerService } from 'vs/platform/log/common/fileLog'
import { LogService } from 'vs/platform/log/common/logService'
import { IEnvironmentService } from 'vs/platform/environment/common/environment.service'
import { localizeWithPath } from 'vs/nls'
import { windowLogId } from 'vs/workbench/services/log/common/logConstants'
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService.service'
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'
Expand All @@ -15,6 +14,7 @@ import { IDefaultLogLevelsService } from 'vs/workbench/contrib/logs/common/defau
import getEnvironmentServiceOverride from './environment'
import { logsPath } from '../workbench'
import { checkServicesNotInitialized } from '../lifecycle'
import { rendererLogLabel } from '../override/vs/workbench/browser/web.main'
import 'vs/workbench/contrib/logs/common/logs.contribution'

class _FileLoggerService extends FileLoggerService {
Expand All @@ -26,7 +26,7 @@ class _FileLoggerService extends FileLoggerService {
const otherLoggers: ILogger[] = []
class _LogService extends LogService {
constructor (@ILoggerService loggerService: ILoggerService, @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService) {
const logger = loggerService.createLogger(environmentService.logFile, { id: windowLogId, name: localizeWithPath('vs/workbench/browser/web.main', 'rendererLog', 'Window') })
const logger = loggerService.createLogger(environmentService.logFile, { id: windowLogId, name: rendererLogLabel })
super(logger, otherLoggers)
}
}
Expand Down
Loading