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

Localize strings on github.dev using VSCode FS API #17711

Merged
merged 10 commits into from
Oct 13, 2021
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
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