gpii-windows
Version:
Components of the GPII personalization infrastructure for use on Microsoft's "Windows" ™
328 lines (288 loc) • 12.8 kB
JavaScript
/*
* Windows Language
*
* Copyright 2018 Raising the Floor - International
*
* Licensed under the New BSD license. You may not use this file except in
* compliance with this License.
*
* The R&D leading to these results received funding from the
* Department of Education - Grant H421A150005 (GPII-APCP). However,
* these results do not necessarily represent the policy of the
* Department of Education, and you should not assume endorsement by the
* Federal Government.
*
* You may obtain a copy of the License at
* https://github.com/GPII/universal/blob/master/LICENSE.txt
*/
"use strict";
var fluid = require("gpii-universal"),
child_process = require("child_process");
var gpii = fluid.registerNamespace("gpii");
fluid.registerNamespace("gpii.windows.language");
fluid.defaults("gpii.windows.language", {
gradeNames: ["fluid.component", "fluid.modelComponent"],
invokers: {
getInstalledLanguages: {
funcName: "gpii.windows.language.getInstalled",
args: [ "{that}" ]
},
getLanguageNames: {
funcName: "gpii.windows.language.getLanguageNames",
args: [ "{that}", "{arguments}.0" ]
},
getDisplayLanguage: {
funcName: "gpii.windows.language.getDisplayLanguage"
},
startMessages: "{gpii.windows.messages}.start({that})",
stopMessages: "{gpii.windows.messages}.stop({that})"
},
listeners: {
"onCreate.update": "{that}.getInstalledLanguages()",
"onCreate.messages": "{that}.startMessages()",
"{gpii.windows.messages}.events.onMessage": {
funcName: "gpii.windows.language.windowMessage",
// that, hwnd, msg, wParam, lParam
args: [ "{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2", "{arguments}.3" ]
}
},
// The model gets updated whenever getInstalledLanguages is called.
model: {
/** @type {Map<String,InstalledLanguage>} */
installedLanguages: null,
/** Currently configured display language */
configuredLanguage: null
},
members: {
/** code=>name map of language names in english */
englishNames: {}
}
});
/**
* Language names.
* @typedef {Object} LanguageNames
* @property {String} english The language name in English.
* @property {String} local The language name (and country), in the current display language.
* @property {String} native The language name (and country), in its native language.
*/
/**
* An installed language
* @typedef {LanguageNames} InstalledLanguage
* @property {String} code The language code, `lang[-COUNTRY]`.
* @property {Boolean} current true if this is the current display language.
*/
/**
* Fixes the casing of a language code, so the language is lowercase and the region is uppercase.
*
* @param {String} langCode The language code.
* @return {String} The language code, in the correct casing.
*/
gpii.windows.language.fixCodeCase = function (langCode) {
var parts = (langCode && langCode.split) ? langCode.split("-") : [];
// [RFC-5646] Language is always the first section - "2*3ALPHA ; shortest ISO 639 code"
if (parts.length > 0 && (parts[0].length === 2 || parts[0].length === 3)) {
parts[0] = parts[0].toLowerCase();
// [RFC-5646] describes a language tag as having a varying number of sections before and after the region. The
// region is the only section after the language that is 2 letters (may also be 3 digits, but for casing that's
// irrelevant)
var region;
for (var n = 1; n < parts.length; n++) {
if (parts[n].length === 2) {
region = n;
}
}
if (region) {
parts[region] = parts[region].toUpperCase();
}
}
return parts.join("-");
};
/**
* Gets the display languages that are installed on the system, and updates the model.
*
* These are listed in HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\MUI\UILanguages
*
* @param {Component} that The gpii.windows.language instance.
* @return {Promise<Map<String,InstalledLanguage>>} A promise, resolving with either the language names if the list has
* changed, or null if there was no change.
*/
gpii.windows.language.getInstalled = function (that) {
var langCodes = gpii.windows.enumRegistryKeys(
"HKEY_LOCAL_MACHINE", "SYSTEM\\CurrentControlSet\\Control\\MUI\\UILanguages");
langCodes = langCodes.map(gpii.windows.language.fixCodeCase);
var current = gpii.windows.language.getDisplayLanguage();
// Because this function gets called in the off-chance that the language has changed (maybe multiple times during
// key-in), and getting the translated languages requires a new process, perform some checks upfront.
var changed = current !== fluid.get(that.model, "configuredLanguage");
that.applier.change("configuredLanguage", current);
if (!changed) {
var knownLanguages = Object.keys(fluid.get(that.model, "installedLanguages") || {});
changed = langCodes.length !== knownLanguages.length;
if (!changed) {
changed = !knownLanguages.every(function (elem) {
return langCodes.includes(elem);
});
}
}
var promise;
if (changed) {
// Update the language names only if required.
promise = that.getLanguageNames(langCodes).then(function (languages) {
that.applier.change("installedLanguages", languages);
});
} else {
promise = fluid.promise().resolve();
}
return promise;
};
/**
* Gets the language names of the given languages, identified by their IETF language codes (`en`, `es-MX`).
*
* It returns an object containing the name in English, the current display language, and native language.
*
* If only the language identifier (first 2 characters) are passed, then the language name is returned.
* If the country code is also given, then the country is also returned in brackets:
* - If the country is code is unknown, or the country-specific language isn't recognised, then the language code is
* used instead of the country.
* - If the language is only spoken in a single country (eg, Bulgarian), then the country is not returned, unless a
* different country was passed (eg, bg-GB).
* If the language is unknown, then an empty string is used. If the language code is invalid, null each field is null.
*
* Examples:
*```
* "es-MX" => { english: "Spanish (Mexico)", local: "Spanish (Mexico)", native: "Español (México)" }
* "en" => { english: "English", local: "English", native: "English" }
* "en-GB" => { "english": "English (United Kingdom)", "local": "English (United Kingdom)", "native": "English (United Kingdom)" }
*```
* When the current display language is French:
* ```
* "nl-NL" => { english: "Dutch (Netherlands)", local: "Néerlandais (Pays-Bas)", native: "Nederlands (Nederland)" }
* ```
* @param {Component} that The gpii.windows.language instance.
* @param {String|Array<String>} langCodes The language code(s), in the form of `lang[-COUNTRY]`.
* @return {Promise<LanguageNames>} A promise, resolving with the language names.
*/
gpii.windows.language.getLanguageNames = function (that, langCodes) {
var options = {
env: {
GPII_LANG_CODES: JSON.stringify(fluid.makeArray(langCodes))
},
execArgv: []
};
// In order for the localised names to be in the current display language, the language name routine needs to be
// called in a new process. An attempt had been made to load and unload the winlangdb library in this process,
// however the locale remained the same as it was the first time it was loaded.
var child = child_process.fork(__dirname + "/languageNames.js", options);
var promise = fluid.promise();
var timer = setTimeout(function () {
child.kill();
timer = null;
}, 10000);
child.on("message", function (result) {
var current = gpii.windows.language.getDisplayLanguage();
var currentlyEnglish = current.startsWith("en");
var languages = fluid.transform(result, function (names, code) {
// The english field only contains the language, without the country - but the full name is desired.
// Get this from the .local field if the current language is English. It is hoped that the first time this
// is called, the current language is English. Otherwise, the "english" value will only contain the
// country until this is called when the current language is English.
var englishName = that.englishNames[code];
if (currentlyEnglish && !englishName) {
englishName = names.local;
// Save it for when the current language isn't English
that.englishNames[code] = englishName;
}
if (englishName) {
names.english = englishName;
}
names.code = code;
if (code === current) {
names.current = true;
}
return names;
});
promise.resolve(languages);
});
child.on("exit", function (code) {
if (timer) {
clearTimeout(timer);
}
if (!promise.disposition) {
fluid.log("languageNames failed");
promise.reject({
isError: true,
message: timer
? "language name translation failed (" + code + ")"
: "Timed out waiting for the language names"
});
}
});
return promise;
};
/**
* Called when an event has been received by the message window.
*
* When a relevant message is received, the installed languages model will be updated. The current language can't be
* changed during a session, however the drop-down list in control panel still broadcasts WM_SETTINGCHANGE.
*
* @param {Component} that The gpii.windows.language component.
* @param {Number} hwnd The window handle of the message window.
* @param {Number} msg The message identifier.
* #param {Number} wParam Message specific data. (unused)
* #param {Buffer} lParam Additional message specific data. (unused)
*/
gpii.windows.language.windowMessage = function (that, hwnd, msg) {
if (msg === gpii.windows.API_constants.WM_SETTINGCHANGE
|| msg === gpii.windows.API_constants.WM_INPUTLANGCHANGE) {
that.getInstalledLanguages();
}
};
/**
* Gets the currently configured display language.
*
* This is the language which new processes will use.
*
* @return {String} The language code of the currently configured display language.
*/
gpii.windows.language.getDisplayLanguage = function () {
var langCode = gpii.windows.readRegistryKey(
"HKEY_CURRENT_USER", "Control Panel\\Desktop", "PreferredUILanguages", "REG_SZ").value;
// This setting could be an empty string (perhaps if the language has never been changed?)
if (!langCode) {
// Get the thread's locale, then get the corresponding language code.
var LOCALE_NAME_MAX_LENGTH = 85;
var lcid = gpii.windows.kernel32.GetThreadUILanguage();
var codeBuffer = Buffer.alloc(LOCALE_NAME_MAX_LENGTH);
var result = gpii.windows.kernel32.LCIDToLocaleName(lcid, codeBuffer, codeBuffer.length, 0);
if (result > 0) {
langCode = gpii.windows.stringFromWideChar(codeBuffer);
} else {
fluid.log(gpii.windows.win32errorText("LCIDToLocaleName failed", result));
}
}
return gpii.windows.language.fixCodeCase(langCode);
};
/**
* Updates the Windows display language, by restarting explorer if the language has changed since the last time
* this was called.
*
* @param {String} currentLanguage [optional] The current (new) language.
* @return {Promise|undefined} Resolves when explorer has restarted, or null if the language has not changed.
*/
gpii.windows.updateLanguage = function (currentLanguage) {
var lang = currentLanguage || gpii.windows.language.getDisplayLanguage();
if (gpii.windows.updateLanguage.lastLanguage !== lang) {
// Update the state.
var languageInstances = fluid.queryIoCSelector(fluid.rootComponent, "gpii.windows.language");
fluid.each(languageInstances, gpii.windows.language.getInstalled);
gpii.windows.updateLanguage.lastLanguage = lang;
return gpii.windows.restartExplorer();
}
};
gpii.windows.updateLanguage.lastLanguage = gpii.windows.language.getDisplayLanguage();
fluid.defaults("gpii.windows.updateLanguage", {
gradeNames: "fluid.function",
argumentMap: {
currentLanguage: 0
}
});