Skip to content

Commit

Permalink
Localize strings on github.dev using VSCode FS API (#17711)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Kartik Raj authored and karthiknadig committed Oct 13, 2021
1 parent 18f9dbb commit 2153e7d
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 97 deletions.
1 change: 1 addition & 0 deletions news/2 Fixes/17712.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Localize strings on `github.dev` using VSCode FS API.
3 changes: 2 additions & 1 deletion src/client/browser/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -17,7 +18,7 @@ interface BrowserConfig {

export async function activate(context: vscode.ExtensionContext): Promise<void> {
// Run in a promise and return early so that VS Code can go activate Pylance.

await loadLocalizedStringsForBrowser();
const pylanceExtension = vscode.extensions.getExtension<ILSExtensionApi>(PYLANCE_EXTENSION_ID);
if (pylanceExtension) {
runPylance(context, pylanceExtension);
Expand Down
22 changes: 22 additions & 0 deletions src/client/browser/localize.ts
Original file line number Diff line number Diff line change
@@ -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);
}
99 changes: 6 additions & 93 deletions src/client/common/utils/localize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */

Expand Down Expand Up @@ -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.<locale>.json files
let loadedCollection: Record<string, string> | undefined;
let defaultCollection: Record<string, string> | undefined;
let askedForCollection: Record<string, string> = {};
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<string, string> {
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());
133 changes: 133 additions & 0 deletions src/client/common/utils/localizeHelpers.ts
Original file line number Diff line number Diff line change
@@ -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.<locale>.json files
let loadedCollection: Record<string, string> | undefined;
let defaultCollection: Record<string, string> | undefined;
let askedForCollection: Record<string, string> = {};
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<string, string> {
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<void> {
// 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 {};
}
}
7 changes: 4 additions & 3 deletions src/test/common/utils/localize.functional.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand All @@ -26,7 +27,7 @@ suite('Localization', () => {
setLocale('en-us');

// Ensure each test starts fresh.
localize._resetCollections();
localizeHelpers._resetCollections();
});

teardown(() => {
Expand Down Expand Up @@ -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<string, string> = {};
Object.keys(askedFor).forEach((key: string) => {
// Now check that this key exists somewhere in the nls collection
Expand Down Expand Up @@ -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<string, string> = {};
Object.keys(nlsCollection).forEach((key: string) => {
// Now check that this key exists somewhere in the nls collection
Expand Down

0 comments on commit 2153e7d

Please sign in to comment.