Skip to content

Commit 2153e7d

Browse files
Kartik Rajkarthiknadig
Kartik Raj
authored andcommitted
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
1 parent 18f9dbb commit 2153e7d

File tree

6 files changed

+168
-97
lines changed

6 files changed

+168
-97
lines changed

news/2 Fixes/17712.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Localize strings on `github.dev` using VSCode FS API.

src/client/browser/extension.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { LanguageClientMiddlewareBase } from '../activation/languageClientMiddle
99
import { ILSExtensionApi } from '../activation/node/languageServerFolderService';
1010
import { LanguageServerType } from '../activation/types';
1111
import { AppinsightsKey, PVSC_EXTENSION_ID, PYLANCE_EXTENSION_ID } from '../common/constants';
12+
import { loadLocalizedStringsForBrowser } from '../common/utils/localizeHelpers';
1213
import { EventName } from '../telemetry/constants';
1314

1415
interface BrowserConfig {
@@ -17,7 +18,7 @@ interface BrowserConfig {
1718

1819
export async function activate(context: vscode.ExtensionContext): Promise<void> {
1920
// Run in a promise and return early so that VS Code can go activate Pylance.
20-
21+
await loadLocalizedStringsForBrowser();
2122
const pylanceExtension = vscode.extensions.getExtension<ILSExtensionApi>(PYLANCE_EXTENSION_ID);
2223
if (pylanceExtension) {
2324
runPylance(context, pylanceExtension);

src/client/browser/localize.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
/* eslint-disable @typescript-eslint/no-namespace */
7+
8+
// IMPORTANT: Do not import any node fs related modules here, as they do not work in browser.
9+
import { getLocalizedString } from '../common/utils/localizeHelpers';
10+
11+
export namespace LanguageService {
12+
export const statusItem = {
13+
name: localize('LanguageService.statusItem.name', 'Python IntelliSense Status'),
14+
text: localize('LanguageService.statusItem.text', 'Partial Mode'),
15+
detail: localize('LanguageService.statusItem.detail', 'Limited IntelliSense provided by Pylance'),
16+
};
17+
}
18+
19+
function localize(key: string, defValue?: string) {
20+
// Return a pointer to function so that we refetch it on each call.
21+
return (): string => getLocalizedString(key, defValue);
22+
}

src/client/common/utils/localize.ts

Lines changed: 6 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@
33

44
'use strict';
55

6-
import * as path from 'path';
7-
import { EXTENSION_ROOT_DIR } from '../../constants';
86
import { FileSystem } from '../platform/fileSystem';
7+
import { getLocalizedString, loadLocalizedStringsUsingNodeFS, shouldLoadUsingNodeFS } from './localizeHelpers';
98

109
/* eslint-disable @typescript-eslint/no-namespace, no-shadow */
1110

@@ -549,103 +548,17 @@ export namespace MPLSDeprecation {
549548
export const switchToJedi = localize('MPLSDeprecation.switchToJedi', 'Switch to Jedi (open source)');
550549
}
551550

552-
// Skip using vscode-nls and instead just compute our strings based on key values. Key values
553-
// can be loaded out of the nls.<locale>.json files
554-
let loadedCollection: Record<string, string> | undefined;
555-
let defaultCollection: Record<string, string> | undefined;
556-
let askedForCollection: Record<string, string> = {};
557-
let loadedLocale: string;
558-
559-
// This is exported only for testing purposes.
560-
export function _resetCollections(): void {
561-
loadedLocale = '';
562-
loadedCollection = undefined;
563-
askedForCollection = {};
564-
}
565-
566-
// This is exported only for testing purposes.
567-
export function _getAskedForCollection(): Record<string, string> {
568-
return askedForCollection;
569-
}
570-
571-
// Return the effective set of all localization strings, by key.
572-
//
573-
// This should not be used for direct lookup.
574-
export function getCollectionJSON(): string {
575-
// Load the current collection
576-
if (!loadedCollection || parseLocale() !== loadedLocale) {
577-
load();
578-
}
579-
580-
// Combine the default and loaded collections
581-
return JSON.stringify({ ...defaultCollection, ...loadedCollection });
582-
}
583-
584-
export function localize(key: string, defValue?: string) {
551+
function localize(key: string, defValue?: string) {
585552
// Return a pointer to function so that we refetch it on each call.
586553
return (): string => getString(key, defValue);
587554
}
588555

589-
function parseLocale(): string {
590-
// Attempt to load from the vscode locale. If not there, use english
591-
const vscodeConfigString = process.env.VSCODE_NLS_CONFIG;
592-
return vscodeConfigString ? JSON.parse(vscodeConfigString).locale : 'en-us';
593-
}
594-
595556
function getString(key: string, defValue?: string) {
596-
// Load the current collection
597-
if (!loadedCollection || parseLocale() !== loadedLocale) {
598-
load();
599-
}
600-
601-
// The default collection (package.nls.json) is the fallback.
602-
// Note that we are guaranteed the following (during shipping)
603-
// 1. defaultCollection was initialized by the load() call above
604-
// 2. defaultCollection has the key (see the "keys exist" test)
605-
let collection = defaultCollection!;
606-
607-
// Use the current locale if the key is defined there.
608-
if (loadedCollection && loadedCollection.hasOwnProperty(key)) {
609-
collection = loadedCollection;
610-
}
611-
let result = collection[key];
612-
if (!result && defValue) {
613-
// This can happen during development if you haven't fixed up the nls file yet or
614-
// if for some reason somebody broke the functional test.
615-
result = defValue;
616-
}
617-
askedForCollection[key] = result;
618-
619-
return result;
620-
}
621-
622-
function load() {
623-
const fs = new FileSystem();
624-
625-
// Figure out our current locale.
626-
loadedLocale = parseLocale();
627-
628-
// Find the nls file that matches (if there is one)
629-
const nlsFile = path.join(EXTENSION_ROOT_DIR, `package.nls.${loadedLocale}.json`);
630-
if (fs.fileExistsSync(nlsFile)) {
631-
const contents = fs.readFileSync(nlsFile);
632-
loadedCollection = JSON.parse(contents);
633-
} else {
634-
// If there isn't one, at least remember that we looked so we don't try to load a second time
635-
loadedCollection = {};
636-
}
637-
638-
// Get the default collection if necessary. Strings may be in the default or the locale json
639-
if (!defaultCollection) {
640-
const defaultNlsFile = path.join(EXTENSION_ROOT_DIR, 'package.nls.json');
641-
if (fs.fileExistsSync(defaultNlsFile)) {
642-
const contents = fs.readFileSync(defaultNlsFile);
643-
defaultCollection = JSON.parse(contents);
644-
} else {
645-
defaultCollection = {};
646-
}
557+
if (shouldLoadUsingNodeFS()) {
558+
loadLocalizedStringsUsingNodeFS(new FileSystem());
647559
}
560+
return getLocalizedString(key, defValue);
648561
}
649562

650563
// Default to loading the current locale
651-
load();
564+
loadLocalizedStringsUsingNodeFS(new FileSystem());
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
// IMPORTANT: Do not import any node fs related modules here, as they do not work in browser.
7+
8+
import * as vscode from 'vscode';
9+
import * as path from 'path';
10+
import { EXTENSION_ROOT_DIR } from '../../constants';
11+
import { IFileSystem } from '../platform/types';
12+
13+
// Skip using vscode-nls and instead just compute our strings based on key values. Key values
14+
// can be loaded out of the nls.<locale>.json files
15+
let loadedCollection: Record<string, string> | undefined;
16+
let defaultCollection: Record<string, string> | undefined;
17+
let askedForCollection: Record<string, string> = {};
18+
let loadedLocale: string;
19+
20+
// This is exported only for testing purposes.
21+
export function _resetCollections(): void {
22+
loadedLocale = '';
23+
loadedCollection = undefined;
24+
askedForCollection = {};
25+
}
26+
27+
// This is exported only for testing purposes.
28+
export function _getAskedForCollection(): Record<string, string> {
29+
return askedForCollection;
30+
}
31+
32+
export function shouldLoadUsingNodeFS(): boolean {
33+
return !loadedCollection || parseLocale() !== loadedLocale;
34+
}
35+
36+
declare let navigator: { language: string } | undefined;
37+
38+
function parseLocale(): string {
39+
try {
40+
if (navigator?.language) {
41+
return navigator.language.toLowerCase();
42+
}
43+
} catch {
44+
// Fall through
45+
}
46+
// Attempt to load from the vscode locale. If not there, use english
47+
const vscodeConfigString = process.env.VSCODE_NLS_CONFIG;
48+
return vscodeConfigString ? JSON.parse(vscodeConfigString).locale : 'en-us';
49+
}
50+
51+
export function getLocalizedString(key: string, defValue?: string): string {
52+
// The default collection (package.nls.json) is the fallback.
53+
// Note that we are guaranteed the following (during shipping)
54+
// 1. defaultCollection was initialized by the load() call above
55+
// 2. defaultCollection has the key (see the "keys exist" test)
56+
let collection = defaultCollection;
57+
58+
// Use the current locale if the key is defined there.
59+
if (loadedCollection && loadedCollection.hasOwnProperty(key)) {
60+
collection = loadedCollection;
61+
}
62+
if (collection === undefined) {
63+
throw new Error(`Localizations haven't been loaded yet for key: ${key}`);
64+
}
65+
let result = collection[key];
66+
if (!result && defValue) {
67+
// This can happen during development if you haven't fixed up the nls file yet or
68+
// if for some reason somebody broke the functional test.
69+
result = defValue;
70+
}
71+
askedForCollection[key] = result;
72+
73+
return result;
74+
}
75+
76+
/**
77+
* Can be used to synchronously load localized strings, useful if we want localized strings at module level itself.
78+
* Cannot be used in VSCode web or any browser. Must be called before any use of the locale.
79+
*/
80+
export function loadLocalizedStringsUsingNodeFS(fs: IFileSystem): void {
81+
// Figure out our current locale.
82+
loadedLocale = parseLocale();
83+
84+
// Find the nls file that matches (if there is one)
85+
const nlsFile = path.join(EXTENSION_ROOT_DIR, `package.nls.${loadedLocale}.json`);
86+
if (fs.fileExistsSync(nlsFile)) {
87+
const contents = fs.readFileSync(nlsFile);
88+
loadedCollection = JSON.parse(contents);
89+
} else {
90+
// If there isn't one, at least remember that we looked so we don't try to load a second time
91+
loadedCollection = {};
92+
}
93+
94+
// Get the default collection if necessary. Strings may be in the default or the locale json
95+
if (!defaultCollection) {
96+
const defaultNlsFile = path.join(EXTENSION_ROOT_DIR, 'package.nls.json');
97+
if (fs.fileExistsSync(defaultNlsFile)) {
98+
const contents = fs.readFileSync(defaultNlsFile);
99+
defaultCollection = JSON.parse(contents);
100+
} else {
101+
defaultCollection = {};
102+
}
103+
}
104+
}
105+
106+
/**
107+
* Only uses the VSCode APIs to query filesystem and not the node fs APIs, as
108+
* they're not available in browser. Must be called before any use of the locale.
109+
*/
110+
export async function loadLocalizedStringsForBrowser(): Promise<void> {
111+
// Figure out our current locale.
112+
loadedLocale = parseLocale();
113+
114+
loadedCollection = await parseNLS(loadedLocale);
115+
116+
// Get the default collection if necessary. Strings may be in the default or the locale json
117+
if (!defaultCollection) {
118+
defaultCollection = await parseNLS();
119+
}
120+
}
121+
122+
async function parseNLS(locale?: string) {
123+
try {
124+
const filename = locale ? `package.nls.${locale}.json` : `package.nls.json`;
125+
const nlsFile = vscode.Uri.joinPath(vscode.Uri.file(EXTENSION_ROOT_DIR), filename);
126+
const buffer = await vscode.workspace.fs.readFile(nlsFile);
127+
const contents = new TextDecoder().decode(buffer);
128+
return JSON.parse(contents);
129+
} catch {
130+
// If there isn't one, at least remember that we looked so we don't try to load a second time.
131+
return {};
132+
}
133+
}

src/test/common/utils/localize.functional.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import * as fs from 'fs';
88
import * as path from 'path';
99
import { EXTENSION_ROOT_DIR } from '../../../client/common/constants';
1010
import * as localize from '../../../client/common/utils/localize';
11+
import * as localizeHelpers from '../../../client/common/utils/localizeHelpers';
1112

1213
const defaultNLSFile = path.join(EXTENSION_ROOT_DIR, 'package.nls.json');
1314

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

2829
// Ensure each test starts fresh.
29-
localize._resetCollections();
30+
localizeHelpers._resetCollections();
3031
});
3132

3233
teardown(() => {
@@ -102,7 +103,7 @@ suite('Localization', () => {
102103
useEveryLocalization(localize);
103104

104105
// Now verify all of the asked for keys exist
105-
const askedFor = localize._getAskedForCollection();
106+
const askedFor = localizeHelpers._getAskedForCollection();
106107
const missing: Record<string, string> = {};
107108
Object.keys(askedFor).forEach((key: string) => {
108109
// Now check that this key exists somewhere in the nls collection
@@ -133,7 +134,7 @@ suite('Localization', () => {
133134
useEveryLocalization(localize);
134135

135136
// Now verify all of the asked for keys exist
136-
const askedFor = localize._getAskedForCollection();
137+
const askedFor = localizeHelpers._getAskedForCollection();
137138
const extra: Record<string, string> = {};
138139
Object.keys(nlsCollection).forEach((key: string) => {
139140
// Now check that this key exists somewhere in the nls collection

0 commit comments

Comments
 (0)