Skip to content

Commit

Permalink
Merge pull request #417 from CodinGame/optimize-localization
Browse files Browse the repository at this point in the history
Optimize localization
  • Loading branch information
CGNonofr authored May 3, 2024
2 parents e31c9a0 + 35b66ee commit 656c48c
Show file tree
Hide file tree
Showing 12 changed files with 760 additions and 393 deletions.
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

1 comment on commit 656c48c

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.