Make application react to element-web language change

This commit is contained in:
Germain Souquet
2021-04-26 13:58:29 +01:00
parent 658304cc51
commit 105070716e
6 changed files with 315 additions and 150 deletions
+14 -3
View File
@@ -34,7 +34,7 @@ const AutoLaunch = require('auto-launch');
const path = require('path');
const tray = require('./tray');
const vectorMenu = require('./vectormenu');
const buildMenuTemplate = require('./vectormenu');
const webContentsHandler = require('./webcontents-handler');
const updater = require('./updater');
const {getProfileFromDeeplink, protocolInit, recordSSOSession} = require('./protocol');
@@ -57,7 +57,7 @@ try {
}
}
const { _td } = require('./language-helper');
const { _td, AppLocalization } = require('./language-helper');
let seshatSupported = false;
let Seshat;
@@ -88,6 +88,7 @@ let vectorConfig;
let iconPath;
let trayConfig;
let launcher;
let appLocalization;
if (argv["help"]) {
console.log("Options:");
@@ -368,6 +369,9 @@ ipcMain.on('ipcCall', async function(ev, payload) {
launcher.disable();
}
break;
case 'setLanguage':
appLocalization.setAppLocale(args[0]);
break;
case 'shouldWarnBeforeExit':
ret = store.get('warnBeforeExit', true);
break;
@@ -942,7 +946,6 @@ app.on('ready', async () => {
},
});
mainWindow.loadURL('vector://vector/webapp/');
Menu.setApplicationMenu(vectorMenu);
// Handle spellchecker
// For some reason spellCheckerEnabled isn't persisted so we have to use the store here
@@ -991,6 +994,14 @@ app.on('ready', async () => {
}
webContentsHandler(mainWindow.webContents);
appLocalization = new AppLocalization({
store,
components: [
() => tray.initApplicationMenu(),
() => Menu.setApplicationMenu(buildMenuTemplate()),
],
});
});
app.on('window-all-closed', () => {
+106 -4
View File
@@ -1,11 +1,113 @@
// Function which only purpose is to mark that a string is translatable
// Does not actually do anything. It's helpful for automatic extraction of translatable strings
const counterpart = require('counterpart');
function _td(s) {
return s;
const DEFAULT_LOCALE = "en";
function _td(text) {
return _t(text);
}
function _t(text, variables = {}) {
const args = Object.assign({ interpolate: false }, variables);
const { count } = args;
// Horrible hack to avoid https://github.com/vector-im/element-web/issues/4191
// The interpolation library that counterpart uses does not support undefined/null
// values and instead will throw an error. This is a problem since everywhere else
// in JS land passing undefined/null will simply stringify instead, and when converting
// valid ES6 template strings to i18n strings it's extremely easy to pass undefined/null
// if there are no existing null guards. To avoid this making the app completely inoperable,
// we'll check all the values for undefined/null and stringify them here.
Object.keys(args).forEach((key) => {
if (args[key] === undefined) {
console.warn("safeCounterpartTranslate called with undefined interpolation name: " + key);
args[key] = 'undefined';
}
if (args[key] === null) {
console.warn("safeCounterpartTranslate called with null interpolation name: " + key);
args[key] = 'null';
}
});
let translated = counterpart.translate(text, args);
if (translated === undefined && count !== undefined) {
// counterpart does not do fallback if no pluralisation exists
// in the preferred language, so do it here
translated = counterpart.translate(text, Object.assign({}, args, {locale: DEFAULT_LOCALE}));
}
// The translation returns text so there's no XSS vector here (no unsafe HTML, no code execution)
return translated;
}
class AppLocalization {
static STORE_KEY = "locale"
#store = null
constructor({ store, components = [] }) {
counterpart.registerTranslations("en", this.fetchTranslationJson("en_EN"));
counterpart.setFallbackLocale('en');
counterpart.setSeparator('|');
if (Array.isArray(components)) {
this.localizedComponents = new Set(components);
}
this.#store = store;
if (this.#store.has(AppLocalization.STORE_KEY)) {
const locales = this.#store.get(AppLocalization.STORE_KEY);
this.setAppLocale(locales);
}
this.resetLocalizedUI();
}
fetchTranslationJson(locale) {
try {
console.log("Fetching translation json for locale: " + locale);
return require(`./i18n/strings/${locale}.json`);
} catch (e) {
console.log(`Could not fetch translation json for locale: '${locale}'`, e);
return null;
}
}
get languageTranslationJson() {
return this.translationJsonMap.get(this.language);
}
setAppLocale(locales) {
console.log(`Changing application language to ${locales}`);
if (!Array.isArray(locales)) {
locales = [locales];
}
locales.forEach(locale => {
const translations = this.fetchTranslationJson(locale);
if (translations !== null) {
counterpart.registerTranslations(locale, translations);
}
});
counterpart.setLocale(locales);
this.#store.set(AppLocalization.STORE_KEY, locales);
this.resetLocalizedUI();
}
resetLocalizedUI() {
console.log("Resetting the UI components after locale change");
this.localizedComponents.forEach(componentSetup => {
if (typeof componentSetup === "function") {
componentSetup();
}
});
}
}
module.exports = {
AppLocalization,
_t,
_td,
};
+35 -26
View File
@@ -34,39 +34,24 @@ exports.destroy = function() {
}
};
const toggleWin = function() {
if (global.mainWindow.isVisible() && !global.mainWindow.isMinimized()) {
global.mainWindow.hide();
} else {
if (global.mainWindow.isMinimized()) global.mainWindow.restore();
if (!global.mainWindow.isVisible()) global.mainWindow.show();
global.mainWindow.focus();
}
};
exports.create = function(config) {
// no trays on darwin
if (process.platform === 'darwin' || trayIcon) return;
const toggleWin = function() {
if (global.mainWindow.isVisible() && !global.mainWindow.isMinimized()) {
global.mainWindow.hide();
} else {
if (global.mainWindow.isMinimized()) global.mainWindow.restore();
if (!global.mainWindow.isVisible()) global.mainWindow.show();
global.mainWindow.focus();
}
};
const contextMenu = Menu.buildFromTemplate([
{
label: _td('Show/Hide'),
click: toggleWin,
},
{ type: 'separator' },
{
label: _td('Quit'),
click: function() {
app.quit();
},
},
]);
const defaultIcon = nativeImage.createFromPath(config.icon_path);
trayIcon = new Tray(defaultIcon);
trayIcon.setToolTip(config.brand);
trayIcon.setContextMenu(contextMenu);
initApplicationMenu();
trayIcon.on('click', toggleWin);
let lastFavicon = null;
@@ -105,3 +90,27 @@ exports.create = function(config) {
trayIcon.setToolTip(title);
});
};
function initApplicationMenu() {
if (!trayIcon) {
return;
}
const contextMenu = Menu.buildFromTemplate([
{
label: _td('Show/Hide'),
click: toggleWin,
},
{ type: 'separator' },
{
label: _td('Quit'),
click: function() {
app.quit();
},
},
]);
trayIcon.setContextMenu(contextMenu);
}
exports.initApplicationMenu = initApplicationMenu;
+120 -116
View File
@@ -17,133 +17,137 @@ limitations under the License.
const {app, shell, Menu} = require('electron');
const { _td } = require('./language-helper');
// Menu template from http://electron.atom.io/docs/api/menu/, edited
const template = [
{
label: _td('Edit'),
accelerator: 'e',
submenu: [
{ role: 'undo' },
{ role: 'redo' },
{ type: 'separator' },
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' },
{ role: 'pasteandmatchstyle' },
{ role: 'delete' },
{ role: 'selectall' },
],
},
{
label: _td('View'),
accelerator: 'V',
submenu: [
{ type: 'separator' },
{ role: 'resetzoom' },
{ role: 'zoomin', accelerator: 'CommandOrControl+=' },
{ role: 'zoomout' },
{ type: 'separator' },
{
label: _td('Preferences'),
accelerator: 'Command+,', // Mac-only accelerator
click() { global.mainWindow.webContents.send('preferences'); },
},
{ role: 'togglefullscreen' },
{ role: 'toggledevtools' },
],
},
{
label: _td('Window'),
accelerator: 'w',
role: 'window',
submenu: [
{ role: 'minimize' },
{ role: 'close' },
],
},
{
label: _td('Help'),
accelerator: 'h',
role: 'help',
submenu: [
{
label: _td('Element Help'),
click() { shell.openExternal('https://element.io/help'); },
},
],
},
];
// macOS has specific menu conventions...
if (process.platform === 'darwin') {
template.unshift({
// first macOS menu is the name of the app
label: app.name,
submenu: [
{ role: 'about' },
{ type: 'separator' },
{
role: 'services',
submenu: [],
},
{ type: 'separator' },
{ role: 'hide' },
{ role: 'hideothers' },
{ role: 'unhide' },
{ type: 'separator' },
{ role: 'quit' },
],
});
// Edit menu.
// This has a 'speech' section on macOS
template[1].submenu.push(
{ type: 'separator' },
function buildMenuTemplate() {
// Menu template from http://electron.atom.io/docs/api/menu/, edited
const template = [
{
label: _td('Speech'),
label: _td('Edit'),
accelerator: 'e',
submenu: [
{ role: 'startspeaking' },
{ role: 'stopspeaking' },
{ role: 'undo' },
{ role: 'redo' },
{ type: 'separator' },
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' },
{ role: 'pasteandmatchstyle' },
{ role: 'delete' },
{ role: 'selectall' },
],
});
// Window menu.
// This also has specific functionality on macOS
template[3].submenu = [
{
label: _td('Close'),
accelerator: 'CmdOrCtrl+W',
role: 'close',
},
{
label: _td('Minimize'),
accelerator: 'CmdOrCtrl+M',
role: 'minimize',
label: _td('View'),
accelerator: 'V',
submenu: [
{ type: 'separator' },
{ role: 'resetzoom' },
{ role: 'zoomin', accelerator: 'CommandOrControl+=' },
{ role: 'zoomout' },
{ type: 'separator' },
{
label: _td('Preferences'),
accelerator: 'Command+,', // Mac-only accelerator
click() { global.mainWindow.webContents.send('preferences'); },
},
{ role: 'togglefullscreen' },
{ role: 'toggledevtools' },
],
},
{
label: _td('Zoom'),
role: 'zoom',
label: _td('Window'),
accelerator: 'w',
role: 'window',
submenu: [
{ role: 'minimize' },
{ role: 'close' },
],
},
{
type: 'separator',
},
{
label: _td('Bring All to Front'),
role: 'front',
label: _td('Help'),
accelerator: 'h',
role: 'help',
submenu: [
{
label: _td('Element Help'),
click() { shell.openExternal('https://element.io/help'); },
},
],
},
];
} else {
template.unshift({
label: _td('File'),
accelerator: 'f',
submenu: [
// For some reason, 'about' does not seem to work on windows.
/*{
role: 'about'
},*/
{ role: 'quit' },
],
});
// macOS has specific menu conventions...
if (process.platform === 'darwin') {
template.unshift({
// first macOS menu is the name of the app
label: app.name,
submenu: [
{ role: 'about' },
{ type: 'separator' },
{
role: 'services',
submenu: [],
},
{ type: 'separator' },
{ role: 'hide' },
{ role: 'hideothers' },
{ role: 'unhide' },
{ type: 'separator' },
{ role: 'quit' },
],
});
// Edit menu.
// This has a 'speech' section on macOS
template[1].submenu.push(
{ type: 'separator' },
{
label: _td('Speech'),
submenu: [
{ role: 'startspeaking' },
{ role: 'stopspeaking' },
],
});
// Window menu.
// This also has specific functionality on macOS
template[3].submenu = [
{
label: _td('Close'),
accelerator: 'CmdOrCtrl+W',
role: 'close',
},
{
label: _td('Minimize'),
accelerator: 'CmdOrCtrl+M',
role: 'minimize',
},
{
label: _td('Zoom'),
role: 'zoom',
},
{
type: 'separator',
},
{
label: _td('Bring All to Front'),
role: 'front',
},
];
} else {
template.unshift({
label: _td('File'),
accelerator: 'f',
submenu: [
// For some reason, 'about' does not seem to work on windows.
/*{
role: 'about'
},*/
{ role: 'quit' },
],
});
}
return Menu.buildFromTemplate(template);
}
module.exports = Menu.buildFromTemplate(template);
module.exports = buildMenuTemplate;