From 2153e7d0ea4551f7b6c7653898027290b63ab346 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 12 Oct 2021 18:14:39 -0700 Subject: [PATCH] Localize strings on `github.dev` using VSCode FS API (#17711) * Change localization in the extension to be async and use the VS Code APIs * News entry * Modify error thrown * Move localization into separate module * Update news entry * Oops * Refactor so code is not duplicated * Fix tests * Oopsp * Fix lint --- news/2 Fixes/17712.md | 1 + src/client/browser/extension.ts | 3 +- src/client/browser/localize.ts | 22 +++ src/client/common/utils/localize.ts | 99 +------------ src/client/common/utils/localizeHelpers.ts | 133 ++++++++++++++++++ .../common/utils/localize.functional.test.ts | 7 +- 6 files changed, 168 insertions(+), 97 deletions(-) create mode 100644 news/2 Fixes/17712.md create mode 100644 src/client/browser/localize.ts create mode 100644 src/client/common/utils/localizeHelpers.ts diff --git a/news/2 Fixes/17712.md b/news/2 Fixes/17712.md new file mode 100644 index 000000000000..35ec2f5dfee5 --- /dev/null +++ b/news/2 Fixes/17712.md @@ -0,0 +1 @@ +Localize strings on `github.dev` using VSCode FS API. diff --git a/src/client/browser/extension.ts b/src/client/browser/extension.ts index 284bf1300ada..fb25d3d8765b 100644 --- a/src/client/browser/extension.ts +++ b/src/client/browser/extension.ts @@ -9,6 +9,7 @@ import { LanguageClientMiddlewareBase } from '../activation/languageClientMiddle import { ILSExtensionApi } from '../activation/node/languageServerFolderService'; import { LanguageServerType } from '../activation/types'; import { AppinsightsKey, PVSC_EXTENSION_ID, PYLANCE_EXTENSION_ID } from '../common/constants'; +import { loadLocalizedStringsForBrowser } from '../common/utils/localizeHelpers'; import { EventName } from '../telemetry/constants'; interface BrowserConfig { @@ -17,7 +18,7 @@ interface BrowserConfig { export async function activate(context: vscode.ExtensionContext): Promise { // Run in a promise and return early so that VS Code can go activate Pylance. - + await loadLocalizedStringsForBrowser(); const pylanceExtension = vscode.extensions.getExtension(PYLANCE_EXTENSION_ID); if (pylanceExtension) { runPylance(context, pylanceExtension); diff --git a/src/client/browser/localize.ts b/src/client/browser/localize.ts new file mode 100644 index 000000000000..5e13576e3e19 --- /dev/null +++ b/src/client/browser/localize.ts @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +/* eslint-disable @typescript-eslint/no-namespace */ + +// IMPORTANT: Do not import any node fs related modules here, as they do not work in browser. +import { getLocalizedString } from '../common/utils/localizeHelpers'; + +export namespace LanguageService { + export const statusItem = { + name: localize('LanguageService.statusItem.name', 'Python IntelliSense Status'), + text: localize('LanguageService.statusItem.text', 'Partial Mode'), + detail: localize('LanguageService.statusItem.detail', 'Limited IntelliSense provided by Pylance'), + }; +} + +function localize(key: string, defValue?: string) { + // Return a pointer to function so that we refetch it on each call. + return (): string => getLocalizedString(key, defValue); +} diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 2e0d9ebd9ff0..6e490da58dea 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -3,9 +3,8 @@ 'use strict'; -import * as path from 'path'; -import { EXTENSION_ROOT_DIR } from '../../constants'; import { FileSystem } from '../platform/fileSystem'; +import { getLocalizedString, loadLocalizedStringsUsingNodeFS, shouldLoadUsingNodeFS } from './localizeHelpers'; /* eslint-disable @typescript-eslint/no-namespace, no-shadow */ @@ -549,103 +548,17 @@ export namespace MPLSDeprecation { export const switchToJedi = localize('MPLSDeprecation.switchToJedi', 'Switch to Jedi (open source)'); } -// Skip using vscode-nls and instead just compute our strings based on key values. Key values -// can be loaded out of the nls..json files -let loadedCollection: Record | undefined; -let defaultCollection: Record | undefined; -let askedForCollection: Record = {}; -let loadedLocale: string; - -// This is exported only for testing purposes. -export function _resetCollections(): void { - loadedLocale = ''; - loadedCollection = undefined; - askedForCollection = {}; -} - -// This is exported only for testing purposes. -export function _getAskedForCollection(): Record { - return askedForCollection; -} - -// Return the effective set of all localization strings, by key. -// -// This should not be used for direct lookup. -export function getCollectionJSON(): string { - // Load the current collection - if (!loadedCollection || parseLocale() !== loadedLocale) { - load(); - } - - // Combine the default and loaded collections - return JSON.stringify({ ...defaultCollection, ...loadedCollection }); -} - -export function localize(key: string, defValue?: string) { +function localize(key: string, defValue?: string) { // Return a pointer to function so that we refetch it on each call. return (): string => getString(key, defValue); } -function parseLocale(): string { - // Attempt to load from the vscode locale. If not there, use english - const vscodeConfigString = process.env.VSCODE_NLS_CONFIG; - return vscodeConfigString ? JSON.parse(vscodeConfigString).locale : 'en-us'; -} - function getString(key: string, defValue?: string) { - // Load the current collection - if (!loadedCollection || parseLocale() !== loadedLocale) { - load(); - } - - // The default collection (package.nls.json) is the fallback. - // Note that we are guaranteed the following (during shipping) - // 1. defaultCollection was initialized by the load() call above - // 2. defaultCollection has the key (see the "keys exist" test) - let collection = defaultCollection!; - - // Use the current locale if the key is defined there. - if (loadedCollection && loadedCollection.hasOwnProperty(key)) { - collection = loadedCollection; - } - let result = collection[key]; - if (!result && defValue) { - // This can happen during development if you haven't fixed up the nls file yet or - // if for some reason somebody broke the functional test. - result = defValue; - } - askedForCollection[key] = result; - - return result; -} - -function load() { - const fs = new FileSystem(); - - // Figure out our current locale. - loadedLocale = parseLocale(); - - // Find the nls file that matches (if there is one) - const nlsFile = path.join(EXTENSION_ROOT_DIR, `package.nls.${loadedLocale}.json`); - if (fs.fileExistsSync(nlsFile)) { - const contents = fs.readFileSync(nlsFile); - loadedCollection = JSON.parse(contents); - } else { - // If there isn't one, at least remember that we looked so we don't try to load a second time - loadedCollection = {}; - } - - // Get the default collection if necessary. Strings may be in the default or the locale json - if (!defaultCollection) { - const defaultNlsFile = path.join(EXTENSION_ROOT_DIR, 'package.nls.json'); - if (fs.fileExistsSync(defaultNlsFile)) { - const contents = fs.readFileSync(defaultNlsFile); - defaultCollection = JSON.parse(contents); - } else { - defaultCollection = {}; - } + if (shouldLoadUsingNodeFS()) { + loadLocalizedStringsUsingNodeFS(new FileSystem()); } + return getLocalizedString(key, defValue); } // Default to loading the current locale -load(); +loadLocalizedStringsUsingNodeFS(new FileSystem()); diff --git a/src/client/common/utils/localizeHelpers.ts b/src/client/common/utils/localizeHelpers.ts new file mode 100644 index 000000000000..5a4eed6d98e6 --- /dev/null +++ b/src/client/common/utils/localizeHelpers.ts @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// IMPORTANT: Do not import any node fs related modules here, as they do not work in browser. + +import * as vscode from 'vscode'; +import * as path from 'path'; +import { EXTENSION_ROOT_DIR } from '../../constants'; +import { IFileSystem } from '../platform/types'; + +// Skip using vscode-nls and instead just compute our strings based on key values. Key values +// can be loaded out of the nls..json files +let loadedCollection: Record | undefined; +let defaultCollection: Record | undefined; +let askedForCollection: Record = {}; +let loadedLocale: string; + +// This is exported only for testing purposes. +export function _resetCollections(): void { + loadedLocale = ''; + loadedCollection = undefined; + askedForCollection = {}; +} + +// This is exported only for testing purposes. +export function _getAskedForCollection(): Record { + return askedForCollection; +} + +export function shouldLoadUsingNodeFS(): boolean { + return !loadedCollection || parseLocale() !== loadedLocale; +} + +declare let navigator: { language: string } | undefined; + +function parseLocale(): string { + try { + if (navigator?.language) { + return navigator.language.toLowerCase(); + } + } catch { + // Fall through + } + // Attempt to load from the vscode locale. If not there, use english + const vscodeConfigString = process.env.VSCODE_NLS_CONFIG; + return vscodeConfigString ? JSON.parse(vscodeConfigString).locale : 'en-us'; +} + +export function getLocalizedString(key: string, defValue?: string): string { + // The default collection (package.nls.json) is the fallback. + // Note that we are guaranteed the following (during shipping) + // 1. defaultCollection was initialized by the load() call above + // 2. defaultCollection has the key (see the "keys exist" test) + let collection = defaultCollection; + + // Use the current locale if the key is defined there. + if (loadedCollection && loadedCollection.hasOwnProperty(key)) { + collection = loadedCollection; + } + if (collection === undefined) { + throw new Error(`Localizations haven't been loaded yet for key: ${key}`); + } + let result = collection[key]; + if (!result && defValue) { + // This can happen during development if you haven't fixed up the nls file yet or + // if for some reason somebody broke the functional test. + result = defValue; + } + askedForCollection[key] = result; + + return result; +} + +/** + * Can be used to synchronously load localized strings, useful if we want localized strings at module level itself. + * Cannot be used in VSCode web or any browser. Must be called before any use of the locale. + */ +export function loadLocalizedStringsUsingNodeFS(fs: IFileSystem): void { + // Figure out our current locale. + loadedLocale = parseLocale(); + + // Find the nls file that matches (if there is one) + const nlsFile = path.join(EXTENSION_ROOT_DIR, `package.nls.${loadedLocale}.json`); + if (fs.fileExistsSync(nlsFile)) { + const contents = fs.readFileSync(nlsFile); + loadedCollection = JSON.parse(contents); + } else { + // If there isn't one, at least remember that we looked so we don't try to load a second time + loadedCollection = {}; + } + + // Get the default collection if necessary. Strings may be in the default or the locale json + if (!defaultCollection) { + const defaultNlsFile = path.join(EXTENSION_ROOT_DIR, 'package.nls.json'); + if (fs.fileExistsSync(defaultNlsFile)) { + const contents = fs.readFileSync(defaultNlsFile); + defaultCollection = JSON.parse(contents); + } else { + defaultCollection = {}; + } + } +} + +/** + * Only uses the VSCode APIs to query filesystem and not the node fs APIs, as + * they're not available in browser. Must be called before any use of the locale. + */ +export async function loadLocalizedStringsForBrowser(): Promise { + // Figure out our current locale. + loadedLocale = parseLocale(); + + loadedCollection = await parseNLS(loadedLocale); + + // Get the default collection if necessary. Strings may be in the default or the locale json + if (!defaultCollection) { + defaultCollection = await parseNLS(); + } +} + +async function parseNLS(locale?: string) { + try { + const filename = locale ? `package.nls.${locale}.json` : `package.nls.json`; + const nlsFile = vscode.Uri.joinPath(vscode.Uri.file(EXTENSION_ROOT_DIR), filename); + const buffer = await vscode.workspace.fs.readFile(nlsFile); + const contents = new TextDecoder().decode(buffer); + return JSON.parse(contents); + } catch { + // If there isn't one, at least remember that we looked so we don't try to load a second time. + return {}; + } +} diff --git a/src/test/common/utils/localize.functional.test.ts b/src/test/common/utils/localize.functional.test.ts index f165025cb695..1e1aa443400c 100644 --- a/src/test/common/utils/localize.functional.test.ts +++ b/src/test/common/utils/localize.functional.test.ts @@ -8,6 +8,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; import * as localize from '../../../client/common/utils/localize'; +import * as localizeHelpers from '../../../client/common/utils/localizeHelpers'; const defaultNLSFile = path.join(EXTENSION_ROOT_DIR, 'package.nls.json'); @@ -26,7 +27,7 @@ suite('Localization', () => { setLocale('en-us'); // Ensure each test starts fresh. - localize._resetCollections(); + localizeHelpers._resetCollections(); }); teardown(() => { @@ -102,7 +103,7 @@ suite('Localization', () => { useEveryLocalization(localize); // Now verify all of the asked for keys exist - const askedFor = localize._getAskedForCollection(); + const askedFor = localizeHelpers._getAskedForCollection(); const missing: Record = {}; Object.keys(askedFor).forEach((key: string) => { // Now check that this key exists somewhere in the nls collection @@ -133,7 +134,7 @@ suite('Localization', () => { useEveryLocalization(localize); // Now verify all of the asked for keys exist - const askedFor = localize._getAskedForCollection(); + const askedFor = localizeHelpers._getAskedForCollection(); const extra: Record = {}; Object.keys(nlsCollection).forEach((key: string) => { // Now check that this key exists somewhere in the nls collection