Skip to content

Commit f123eeb

Browse files
committed
Get language translations partially loaded
1 parent a88c452 commit f123eeb

File tree

10 files changed

+194
-8
lines changed

10 files changed

+194
-8
lines changed

packages/protocol/src/browser/client.ts

+1
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ export class Client {
278278
shell: init.getShell(),
279279
extensionsDirectory: init.getExtensionsDirectory(),
280280
builtInExtensionsDirectory: init.getBuiltinExtensionsDir(),
281+
languageData: init.getLanguageData(),
281282
};
282283
this.initDataEmitter.emit(this._initData);
283284
break;

packages/protocol/src/common/connection.ts

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export interface InitData {
2525
readonly shell: string;
2626
readonly extensionsDirectory: string;
2727
readonly builtInExtensionsDirectory: string;
28+
readonly languageData: string;
2829
}
2930

3031
export interface SharedProcessData {

packages/protocol/src/node/server.ts

+24-3
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,18 @@ import { ChildProcessModuleProxy, ForkProvider, FsModuleProxy, NetModuleProxy, N
99

1010
// tslint:disable no-any
1111

12+
export interface LanguageConfiguration {
13+
locale: string;
14+
}
15+
1216
export interface ServerOptions {
1317
readonly workingDirectory: string;
1418
readonly dataDirectory: string;
1519
readonly cacheDirectory: string;
1620
readonly builtInExtensionsDirectory: string;
1721
readonly extensionsDirectory: string;
1822
readonly fork?: ForkProvider;
23+
readonly getLanguageData?: () => Promise<LanguageConfiguration>;
1924
}
2025

2126
interface ProxyData {
@@ -99,9 +104,25 @@ export class Server {
99104
initMsg.setTmpDirectory(os.tmpdir());
100105
initMsg.setOperatingSystem(platformToProto(os.platform()));
101106
initMsg.setShell(os.userInfo().shell || global.process.env.SHELL || "");
102-
const srvMsg = new ServerMessage();
103-
srvMsg.setInit(initMsg);
104-
connection.send(srvMsg.serializeBinary());
107+
108+
const getLanguageData = this.options.getLanguageData
109+
|| ((): Promise<LanguageConfiguration> => Promise.resolve({
110+
locale: "en",
111+
}));
112+
113+
getLanguageData().then((languageData) => {
114+
try {
115+
initMsg.setLanguageData(JSON.stringify(languageData));
116+
} catch (error) {
117+
logger.error("Unable to send language config", field("error", error));
118+
}
119+
120+
const srvMsg = new ServerMessage();
121+
srvMsg.setInit(initMsg);
122+
connection.send(srvMsg.serializeBinary());
123+
}).catch((error) => {
124+
logger.error(error.message, field("error", error));
125+
});
105126
}
106127

107128
/**

packages/protocol/src/proto/client.proto

+1
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,5 @@ message WorkingInit {
4242
string shell = 6;
4343
string builtin_extensions_dir = 7;
4444
string extensions_directory = 8;
45+
string language_data = 9;
4546
}

packages/protocol/src/proto/client_pb.d.ts

+4
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,9 @@ export class WorkingInit extends jspb.Message {
135135
getExtensionsDirectory(): string;
136136
setExtensionsDirectory(value: string): void;
137137

138+
getLanguageData(): string;
139+
setLanguageData(value: string): void;
140+
138141
serializeBinary(): Uint8Array;
139142
toObject(includeInstance?: boolean): WorkingInit.AsObject;
140143
static toObject(includeInstance: boolean, msg: WorkingInit): WorkingInit.AsObject;
@@ -155,6 +158,7 @@ export namespace WorkingInit {
155158
shell: string,
156159
builtinExtensionsDir: string,
157160
extensionsDirectory: string,
161+
languageData: string,
158162
}
159163

160164
export enum OperatingSystem {

packages/protocol/src/proto/client_pb.js

+28-1
Original file line numberDiff line numberDiff line change
@@ -795,7 +795,8 @@ proto.WorkingInit.toObject = function(includeInstance, msg) {
795795
operatingSystem: jspb.Message.getFieldWithDefault(msg, 5, 0),
796796
shell: jspb.Message.getFieldWithDefault(msg, 6, ""),
797797
builtinExtensionsDir: jspb.Message.getFieldWithDefault(msg, 7, ""),
798-
extensionsDirectory: jspb.Message.getFieldWithDefault(msg, 8, "")
798+
extensionsDirectory: jspb.Message.getFieldWithDefault(msg, 8, ""),
799+
languageData: jspb.Message.getFieldWithDefault(msg, 9, "")
799800
};
800801

801802
if (includeInstance) {
@@ -864,6 +865,10 @@ proto.WorkingInit.deserializeBinaryFromReader = function(msg, reader) {
864865
var value = /** @type {string} */ (reader.readString());
865866
msg.setExtensionsDirectory(value);
866867
break;
868+
case 9:
869+
var value = /** @type {string} */ (reader.readString());
870+
msg.setLanguageData(value);
871+
break;
867872
default:
868873
reader.skipField();
869874
break;
@@ -949,6 +954,13 @@ proto.WorkingInit.serializeBinaryToWriter = function(message, writer) {
949954
f
950955
);
951956
}
957+
f = message.getLanguageData();
958+
if (f.length > 0) {
959+
writer.writeString(
960+
9,
961+
f
962+
);
963+
}
952964
};
953965

954966

@@ -1081,4 +1093,19 @@ proto.WorkingInit.prototype.setExtensionsDirectory = function(value) {
10811093
};
10821094

10831095

1096+
/**
1097+
* optional string language_data = 9;
1098+
* @return {string}
1099+
*/
1100+
proto.WorkingInit.prototype.getLanguageData = function() {
1101+
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 9, ""));
1102+
};
1103+
1104+
1105+
/** @param {string} value */
1106+
proto.WorkingInit.prototype.setLanguageData = function(value) {
1107+
jspb.Message.setProto3StringField(this, 9, value);
1108+
};
1109+
1110+
10841111
goog.object.extend(exports, proto);

packages/server/src/cli.ts

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { field, logger } from "@coder/logger";
22
import { ServerMessage, SharedProcessActive } from "@coder/protocol/src/proto";
3+
import { LanguageConfiguration } from "@coder/protocol/src/node/server";
34
import { withEnv } from "@coder/protocol";
45
import { ChildProcess, fork, ForkOptions } from "child_process";
56
import { randomFillSync } from "crypto";
@@ -11,6 +12,7 @@ import * as WebSocket from "ws";
1112
import { buildDir, cacheHome, dataHome, isCli, serveStatic } from "./constants";
1213
import { createApp } from "./server";
1314
import { forkModule, requireModule } from "./vscode/bootstrapFork";
15+
import { getNlsConfiguration } from "./vscode/language";
1416
import { SharedProcess, SharedProcessState } from "./vscode/sharedProcess";
1517
import opn = require("opn");
1618

@@ -257,6 +259,7 @@ const bold = (text: string | number): string | number => {
257259

258260
return fork(modulePath, args, options);
259261
},
262+
getLanguageData: (): Promise<LanguageConfiguration> => getNlsConfiguration(dataDir, builtInExtensionsDir),
260263
},
261264
password,
262265
httpsOptions: hasCustomHttps ? {
+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import * as fs from "fs";
2+
import * as path from "path";
3+
import * as util from "util";
4+
import { logger } from "@coder/logger";
5+
import * as lp from "vs/base/node/languagePacks";
6+
7+
// NOTE: This code was pulled from lib/vscode/src/main.js.
8+
9+
const stripComments = (content: string): string => {
10+
const regexp = /("(?:[^\\"]*(?:\\.)?)*")|('(?:[^\\']*(?:\\.)?)*')|(\/\*(?:\r?\n|.)*?\*\/)|(\/{2,}.*?(?:(?:\r?\n)|$))/g;
11+
12+
return content.replace(regexp, (match, _m1, _m2, m3, m4) => {
13+
if (m3) { // Only one of m1, m2, m3, m4 matches.
14+
return ""; // A block comment. Replace with nothing.
15+
} else if (m4) { // A line comment. If it ends in \r?\n then keep it.
16+
const length_1 = m4.length;
17+
if (length_1 > 2 && m4[length_1 - 1] === "\n") {
18+
return m4[length_1 - 2] === "\r" ? "\r\n" : "\n";
19+
} else {
20+
return "";
21+
}
22+
} else {
23+
return match; // We match a string.
24+
}
25+
});
26+
};
27+
28+
/**
29+
* Get the locale from the locale file.
30+
*/
31+
const getUserDefinedLocale = async (userDataPath: string): Promise<string | undefined> => {
32+
const localeConfig = path.join(userDataPath, "User/locale.json");
33+
34+
try {
35+
const content = stripComments(await util.promisify(fs.readFile)(localeConfig, "utf8"));
36+
const value = JSON.parse(content).locale;
37+
38+
return value && typeof value === "string" ? value.toLowerCase() : undefined;
39+
} catch (e) {
40+
return undefined;
41+
}
42+
};
43+
44+
export const getNlsConfiguration = (userDataPath: string, builtInDirectory: string): Promise<lp.NLSConfiguration> => {
45+
const defaultConfig = { locale: "en", availableLanguages: {} };
46+
47+
return new Promise(async (resolve): Promise<void> => {
48+
try {
49+
const metaDataFile = require("path").join(builtInDirectory, "nls.metadata.json");
50+
const locale = await getUserDefinedLocale(userDataPath);
51+
if (!locale) {
52+
logger.debug("No locale, using default");
53+
54+
return resolve(defaultConfig);
55+
}
56+
57+
const config = (await lp.getNLSConfiguration(
58+
process.env.VERSION || "development", userDataPath,
59+
metaDataFile, locale,
60+
)) || defaultConfig;
61+
62+
(config as lp.InternalNLSConfiguration)._languagePackSupport = true;
63+
logger.debug(`Locale is ${locale}`, config);
64+
resolve(config);
65+
} catch (error) {
66+
logger.error(error.message);
67+
resolve(defaultConfig);
68+
}
69+
});
70+
};

packages/vscode/src/client.ts

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class VSClient extends IdeClient {
2222
paths._paths.initialize(data, sharedData);
2323
product.initialize(data);
2424
process.env.SHELL = data.shell;
25+
process.env.VSCODE_NLS_CONFIG = data.languageData;
2526
// At this point everything should be filled, including `os`. `os` also
2627
// relies on `initData` but it listens first so it initialize before this
2728
// callback, meaning we are safe to include everything from VS Code now.

packages/vscode/src/fill/platform.ts

+61-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,64 @@
1+
import * as fs from "fs";
12
import * as os from "os";
3+
import * as path from "path";
4+
import * as util from "util";
25
import * as platform from "vs/base/common/platform";
36
import * as browser from "vs/base/browser/browser";
7+
import * as nls from "vs/nls";
8+
import * as lp from "vs/base/node/languagePacks";
49

510
// tslint:disable no-any to override const
611

7-
// Use en instead of en-US since that's vscode default and it uses
8-
// that to determine whether to output aliases which will be redundant.
12+
interface IBundledStrings {
13+
[moduleId: string]: string[];
14+
}
15+
16+
const rawNlsConfig = process.env.VSCODE_NLS_CONFIG;
17+
if (rawNlsConfig) {
18+
const nlsConfig = JSON.parse(rawNlsConfig) as lp.InternalNLSConfiguration;
19+
const resolved = nlsConfig.availableLanguages["*"];
20+
(platform as any).locale = nlsConfig.locale;
21+
(platform as any).language = resolved ? resolved : "en";
22+
(platform as any).translationsConfigFile = nlsConfig._translationsConfigFile;
23+
24+
// TODO: Each time this is imported, VS Code's loader creates a different
25+
// module with an array of all the translations for that file. We don't use
26+
// their loader (we're using Webpack) so we need to figure out a good way to
27+
// handle that.
28+
if (nlsConfig._resolvedLanguagePackCoreLocation) {
29+
const bundles = Object.create(null);
30+
(nls as any).load("nls", undefined, (mod: any) => {
31+
Object.keys(mod).forEach((k) => (nls as any)[k] = mod[k]);
32+
}, {
33+
"vs/nls": {
34+
...nlsConfig,
35+
// Taken from lib/vscode/src/bootstrap.js.
36+
loadBundle: async (
37+
bundle: string, language: string,
38+
cb: (error?: Error, messages?: string[] | IBundledStrings) => void,
39+
): Promise<void> => {
40+
let result = bundles[bundle];
41+
if (result) {
42+
return cb(undefined, result);
43+
}
44+
45+
const bundleFile = path.join(nlsConfig._resolvedLanguagePackCoreLocation, bundle.replace(/\//g, "!") + ".nls.json");
46+
47+
try {
48+
const content = await util.promisify(fs.readFile)(bundleFile, "utf8");
49+
bundles[bundle] = JSON.parse(content);
50+
cb(undefined, bundles[bundle]);
51+
} catch (error) {
52+
cb(error, undefined);
53+
}
54+
},
55+
},
56+
});
57+
}
58+
}
59+
60+
// Anything other than "en" will cause English aliases to display, which ends up
61+
// just displaying a lot of redundant text when the locale is en-US.
962
if (platform.locale === "en-US") {
1063
(platform as any).locale = "en";
1164
}
@@ -20,8 +73,12 @@ if (platform.language === "en-US") {
2073
(platform as any).isLinux = os.platform() === "linux";
2174
(platform as any).isWindows = os.platform() === "win32";
2275
(platform as any).isMacintosh = os.platform() === "darwin";
23-
(platform as any).platform = os.platform() === "linux" ? platform.Platform.Linux : os.platform() === "win32" ? platform.Platform.Windows : platform.Platform.Mac;
76+
(platform as any).platform = os.platform() === "linux"
77+
? platform.Platform.Linux : os.platform() === "win32"
78+
? platform.Platform.Windows : platform.Platform.Mac;
2479

2580
// This is used for keybindings, and in one place to choose between \r\n and \n
2681
// (which we change to use platform.isWindows instead).
27-
(platform as any).OS = (browser.isMacintosh ? platform.OperatingSystem.Macintosh : (browser.isWindows ? platform.OperatingSystem.Windows : platform.OperatingSystem.Linux));
82+
(platform as any).OS = browser.isMacintosh
83+
? platform.OperatingSystem.Macintosh : browser.isWindows
84+
? platform.OperatingSystem.Windows : platform.OperatingSystem.Linux;

0 commit comments

Comments
 (0)