From 74c76137932ae6f72ff27a3c4852a039d785805f Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 12 Oct 2021 14:41:55 -0700 Subject: [PATCH 01/10] Change localization in the extension to be async and use the VS Code APIs --- src/client/browser/extension.ts | 3 +- src/client/common/utils/localize.ts | 71 ++++++++++++----------------- src/client/extension.ts | 3 +- 3 files changed, 34 insertions(+), 43 deletions(-) diff --git a/src/client/browser/extension.ts b/src/client/browser/extension.ts index 284bf1300ada..cb24c5f0f610 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 { loadLocalizedStrings } from '../common/utils/localize'; 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 loadLocalizedStrings(); const pylanceExtension = vscode.extensions.getExtension(PYLANCE_EXTENSION_ID); if (pylanceExtension) { runPylance(context, pylanceExtension); diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 2e0d9ebd9ff0..636d8fa9a073 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -3,9 +3,9 @@ 'use strict'; -import * as path from 'path'; +// IMPORTANT: Do not import any node fs related modules here, as they do not work in browser. +import * as vscode from 'vscode'; import { EXTENSION_ROOT_DIR } from '../../constants'; -import { FileSystem } from '../platform/fileSystem'; /* eslint-disable @typescript-eslint/no-namespace, no-shadow */ @@ -568,36 +568,27 @@ 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) { // Return a pointer to function so that we refetch it on each call. return (): string => getString(key, defValue); } +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'; } 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 @@ -619,33 +610,31 @@ function getString(key: string, defValue?: string) { return result; } -function load() { - const fs = new FileSystem(); - +/** + * 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 loadLocalizedStrings(): Promise { // 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 = {}; - } + loadedCollection = await parseNLS(loadedLocale); // 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 = {}; - } + defaultCollection = await parseNLS(); } } -// Default to loading the current locale -load(); +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/client/extension.ts b/src/client/extension.ts index 6505da54f5f3..4db393212c27 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -32,7 +32,7 @@ import { IApplicationShell, IWorkspaceService } from './common/application/types import { traceError } from './common/logger'; import { IAsyncDisposableRegistry, IExperimentService, IExtensionContext } from './common/types'; import { createDeferred } from './common/utils/async'; -import { Common } from './common/utils/localize'; +import { Common, loadLocalizedStrings } from './common/utils/localize'; import { activateComponents } from './extensionActivation'; import { initializeStandard, initializeComponents, initializeGlobals } from './extensionInit'; import { IServiceContainer } from './ioc/types'; @@ -99,6 +99,7 @@ async function activateUnsafe( //=============================================== // activation starts here + await loadLocalizedStrings(); // First we initialize. const ext = initializeGlobals(context); activatedServiceContainer = ext.legacyIOC.serviceContainer; From 776bb16c7a838f56e05737a45df9721ca3034b4a Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 12 Oct 2021 14:43:45 -0700 Subject: [PATCH 02/10] News entry --- news/2 Fixes/17711.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/2 Fixes/17711.md diff --git a/news/2 Fixes/17711.md b/news/2 Fixes/17711.md new file mode 100644 index 000000000000..5f106932fc0f --- /dev/null +++ b/news/2 Fixes/17711.md @@ -0,0 +1 @@ +Change localization in the extension to be async and use the VS Code APIs. From 965bc561d260042b7b1f39430372265048dcb3e8 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 12 Oct 2021 15:49:10 -0700 Subject: [PATCH 03/10] Modify error thrown --- src/client/common/utils/localize.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 636d8fa9a073..54ba7978c861 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -593,12 +593,15 @@ function getString(key: string, defValue?: string) { // 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!; + 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"); + } let result = collection[key]; if (!result && defValue) { // This can happen during development if you haven't fixed up the nls file yet or From 476ede74a9fe7e5cfa1cf1cae203c6c547940138 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 12 Oct 2021 16:29:04 -0700 Subject: [PATCH 04/10] Move localization into separate module --- src/client/browser/localize.ts | 111 ++++++++++++++++++++++++++++ src/client/common/utils/localize.ts | 76 ++++++++++--------- 2 files changed, 153 insertions(+), 34 deletions(-) create mode 100644 src/client/browser/localize.ts diff --git a/src/client/browser/localize.ts b/src/client/browser/localize.ts new file mode 100644 index 000000000000..9d5b6f7c7baa --- /dev/null +++ b/src/client/browser/localize.ts @@ -0,0 +1,111 @@ +// 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 * as vscode from 'vscode'; +import { EXTENSION_ROOT_DIR } from '../constants'; + +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'), + }; +} + +// 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 localize(key: string, defValue?: string) { + // Return a pointer to function so that we refetch it on each call. + return (): string => getString(key, defValue); +} + +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'; +} + +function getString(key: string, defValue?: 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"); + } + 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; +} + +/** + * 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 loadLocalizedStrings(): 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/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 54ba7978c861..2e0d9ebd9ff0 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -3,9 +3,9 @@ '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 { FileSystem } from '../platform/fileSystem'; /* eslint-disable @typescript-eslint/no-namespace, no-shadow */ @@ -568,40 +568,46 @@ 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) { // Return a pointer to function so that we refetch it on each call. return (): string => getString(key, defValue); } -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'; } 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; + 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"); - } let result = collection[key]; if (!result && defValue) { // This can happen during development if you haven't fixed up the nls file yet or @@ -613,31 +619,33 @@ function getString(key: string, defValue?: string) { return result; } -/** - * 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 loadLocalizedStrings(): Promise { +function load() { + const fs = new FileSystem(); + // Figure out our current locale. loadedLocale = parseLocale(); - loadedCollection = await parseNLS(loadedLocale); + // 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) { - defaultCollection = await parseNLS(); + 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 = {}; + } } } -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 {}; - } -} +// Default to loading the current locale +load(); From a6a7340df7c9eb56c3d34aad43b68b04b2694bc2 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 12 Oct 2021 16:31:57 -0700 Subject: [PATCH 05/10] Update news entry --- news/2 Fixes/17711.md | 1 - news/2 Fixes/17712.md | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 news/2 Fixes/17711.md create mode 100644 news/2 Fixes/17712.md diff --git a/news/2 Fixes/17711.md b/news/2 Fixes/17711.md deleted file mode 100644 index 5f106932fc0f..000000000000 --- a/news/2 Fixes/17711.md +++ /dev/null @@ -1 +0,0 @@ -Change localization in the extension to be async and use the VS Code APIs. 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. From b6b171d90efe184be825a66f93ef5e5b95417ed0 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 12 Oct 2021 16:33:08 -0700 Subject: [PATCH 06/10] Oops --- src/client/browser/extension.ts | 2 +- src/client/extension.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/client/browser/extension.ts b/src/client/browser/extension.ts index cb24c5f0f610..91c7c455918b 100644 --- a/src/client/browser/extension.ts +++ b/src/client/browser/extension.ts @@ -9,8 +9,8 @@ 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 { loadLocalizedStrings } from '../common/utils/localize'; import { EventName } from '../telemetry/constants'; +import { loadLocalizedStrings } from './localize'; interface BrowserConfig { distUrl: string; // URL to Pylance's dist folder. diff --git a/src/client/extension.ts b/src/client/extension.ts index 4db393212c27..6505da54f5f3 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -32,7 +32,7 @@ import { IApplicationShell, IWorkspaceService } from './common/application/types import { traceError } from './common/logger'; import { IAsyncDisposableRegistry, IExperimentService, IExtensionContext } from './common/types'; import { createDeferred } from './common/utils/async'; -import { Common, loadLocalizedStrings } from './common/utils/localize'; +import { Common } from './common/utils/localize'; import { activateComponents } from './extensionActivation'; import { initializeStandard, initializeComponents, initializeGlobals } from './extensionInit'; import { IServiceContainer } from './ioc/types'; @@ -99,7 +99,6 @@ async function activateUnsafe( //=============================================== // activation starts here - await loadLocalizedStrings(); // First we initialize. const ext = initializeGlobals(context); activatedServiceContainer = ext.legacyIOC.serviceContainer; From 40fa419cae8d82cf465a7d4a9b524efb94c2b36a Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 12 Oct 2021 17:07:23 -0700 Subject: [PATCH 07/10] Refactor so code is not duplicated --- src/client/browser/extension.ts | 4 +- src/client/browser/localize.ts | 96 +----------- src/client/common/utils/localize.ts | 103 +------------ src/client/common/utils/localizeHelpers.ts | 139 ++++++++++++++++++ .../common/utils/localize.functional.test.ts | 7 +- 5 files changed, 152 insertions(+), 197 deletions(-) create mode 100644 src/client/common/utils/localizeHelpers.ts diff --git a/src/client/browser/extension.ts b/src/client/browser/extension.ts index 91c7c455918b..fb25d3d8765b 100644 --- a/src/client/browser/extension.ts +++ b/src/client/browser/extension.ts @@ -9,8 +9,8 @@ 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'; -import { loadLocalizedStrings } from './localize'; interface BrowserConfig { distUrl: string; // URL to Pylance's dist folder. @@ -18,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 loadLocalizedStrings(); + 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 index 9d5b6f7c7baa..d88171de672f 100644 --- a/src/client/browser/localize.ts +++ b/src/client/browser/localize.ts @@ -6,8 +6,7 @@ /* eslint-disable @typescript-eslint/no-namespace */ // IMPORTANT: Do not import any node fs related modules here, as they do not work in browser. -import * as vscode from 'vscode'; -import { EXTENSION_ROOT_DIR } from '../constants'; +import { localize } from '../common/utils/localizeHelpers'; export namespace LanguageService { export const statusItem = { @@ -16,96 +15,3 @@ export namespace LanguageService { detail: localize('LanguageService.statusItem.detail', 'Limited IntelliSense provided by Pylance'), }; } - -// 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 localize(key: string, defValue?: string) { - // Return a pointer to function so that we refetch it on each call. - return (): string => getString(key, defValue); -} - -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'; -} - -function getString(key: string, defValue?: 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"); - } - 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; -} - -/** - * 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 loadLocalizedStrings(): 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/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 2e0d9ebd9ff0..82a029233df8 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 { loadLocalizedStringsUsingNodeFS, localize as localizeUtil, shouldLoadUsingFS } from './localizeHelpers'; /* eslint-disable @typescript-eslint/no-namespace, no-shadow */ @@ -549,103 +548,13 @@ 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) { - // 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) { +function localize(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 (shouldLoadUsingFS()) { + loadLocalizedStringsUsingNodeFS(new FileSystem()); } + return localizeUtil(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..ffc48b02c58c --- /dev/null +++ b/src/client/common/utils/localizeHelpers.ts @@ -0,0 +1,139 @@ +// 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 localize(key: string, defValue?: string) { + // Return a pointer to function so that we refetch it on each call. + return (): string => getString(key, defValue); +} + +export function shouldLoadUsingFS(): boolean { + // Return a pointer to function so that we refetch it on each call. + 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'; +} + +function getString(key: string, defValue?: 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 From 26c489281939c162e0fe6218cda38ed7d04ab0d3 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 12 Oct 2021 17:26:24 -0700 Subject: [PATCH 08/10] Fix tests --- src/client/browser/localize.ts | 7 ++++++- src/client/common/utils/localize.ts | 10 +++++++--- src/client/common/utils/localizeHelpers.ts | 9 ++------- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/client/browser/localize.ts b/src/client/browser/localize.ts index d88171de672f..5e13576e3e19 100644 --- a/src/client/browser/localize.ts +++ b/src/client/browser/localize.ts @@ -6,7 +6,7 @@ /* eslint-disable @typescript-eslint/no-namespace */ // IMPORTANT: Do not import any node fs related modules here, as they do not work in browser. -import { localize } from '../common/utils/localizeHelpers'; +import { getLocalizedString } from '../common/utils/localizeHelpers'; export namespace LanguageService { export const statusItem = { @@ -15,3 +15,8 @@ export namespace LanguageService { 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 82a029233df8..dd0f421238a8 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -4,7 +4,7 @@ 'use strict'; import { FileSystem } from '../platform/fileSystem'; -import { loadLocalizedStringsUsingNodeFS, localize as localizeUtil, shouldLoadUsingFS } from './localizeHelpers'; +import { getLocalizedString, loadLocalizedStringsUsingNodeFS, shouldLoadUsingFS } from './localizeHelpers'; /* eslint-disable @typescript-eslint/no-namespace, no-shadow */ @@ -549,11 +549,15 @@ export namespace MPLSDeprecation { } function localize(key: string, defValue?: string) { - // Load the current collection + // Return a pointer to function so that we refetch it on each call. + return (): string => getString(key, defValue); +} + +function getString(key: string, defValue?: string) { if (shouldLoadUsingFS()) { loadLocalizedStringsUsingNodeFS(new FileSystem()); } - return localizeUtil(key, defValue); + return getLocalizedString(key, defValue); } // Default to loading the current locale diff --git a/src/client/common/utils/localizeHelpers.ts b/src/client/common/utils/localizeHelpers.ts index ffc48b02c58c..64fb733013c9 100644 --- a/src/client/common/utils/localizeHelpers.ts +++ b/src/client/common/utils/localizeHelpers.ts @@ -29,11 +29,6 @@ export function _getAskedForCollection(): Record { return askedForCollection; } -export function localize(key: string, defValue?: string) { - // Return a pointer to function so that we refetch it on each call. - return (): string => getString(key, defValue); -} - export function shouldLoadUsingFS(): boolean { // Return a pointer to function so that we refetch it on each call. return !loadedCollection || parseLocale() !== loadedLocale; @@ -54,12 +49,12 @@ function parseLocale(): string { return vscodeConfigString ? JSON.parse(vscodeConfigString).locale : 'en-us'; } -function getString(key: string, defValue?: string) { +export function getLocalizedString(key: string, defValue?: 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!; + let collection = defaultCollection; // Use the current locale if the key is defined there. if (loadedCollection && loadedCollection.hasOwnProperty(key)) { From 5543553ca2611291665a2e64edb32f7ae2af3ea5 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 12 Oct 2021 17:27:50 -0700 Subject: [PATCH 09/10] Oopsp --- src/client/common/utils/localize.ts | 4 ++-- src/client/common/utils/localizeHelpers.ts | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index dd0f421238a8..6e490da58dea 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -4,7 +4,7 @@ 'use strict'; import { FileSystem } from '../platform/fileSystem'; -import { getLocalizedString, loadLocalizedStringsUsingNodeFS, shouldLoadUsingFS } from './localizeHelpers'; +import { getLocalizedString, loadLocalizedStringsUsingNodeFS, shouldLoadUsingNodeFS } from './localizeHelpers'; /* eslint-disable @typescript-eslint/no-namespace, no-shadow */ @@ -554,7 +554,7 @@ function localize(key: string, defValue?: string) { } function getString(key: string, defValue?: string) { - if (shouldLoadUsingFS()) { + if (shouldLoadUsingNodeFS()) { loadLocalizedStringsUsingNodeFS(new FileSystem()); } return getLocalizedString(key, defValue); diff --git a/src/client/common/utils/localizeHelpers.ts b/src/client/common/utils/localizeHelpers.ts index 64fb733013c9..80d06aca969c 100644 --- a/src/client/common/utils/localizeHelpers.ts +++ b/src/client/common/utils/localizeHelpers.ts @@ -29,8 +29,7 @@ export function _getAskedForCollection(): Record { return askedForCollection; } -export function shouldLoadUsingFS(): boolean { - // Return a pointer to function so that we refetch it on each call. +export function shouldLoadUsingNodeFS(): boolean { return !loadedCollection || parseLocale() !== loadedLocale; } From 224f549def33d96b9b50a142f691c669b4d276b1 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 12 Oct 2021 18:01:00 -0700 Subject: [PATCH 10/10] Fix lint --- src/client/common/utils/localizeHelpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/common/utils/localizeHelpers.ts b/src/client/common/utils/localizeHelpers.ts index 80d06aca969c..5a4eed6d98e6 100644 --- a/src/client/common/utils/localizeHelpers.ts +++ b/src/client/common/utils/localizeHelpers.ts @@ -48,7 +48,7 @@ function parseLocale(): string { return vscodeConfigString ? JSON.parse(vscodeConfigString).locale : 'en-us'; } -export function getLocalizedString(key: string, defValue?: string) { +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