UNPKG

gpii-windows

Version:

Components of the GPII personalization infrastructure for use on Microsoft's "Windows" ™

328 lines (288 loc) 12.8 kB
/* * 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 } });