Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,15 @@ $PROFILE` in which case it becomes `Element-$PROFILE`, or it is using one of
the above created by a pre-1.7 install, in which case it will be `Riot` or
`Riot-$PROFILE`.

Translations
==========================

To add a new translation, head to the [translating doc](https://github.com/vector-im/element-web/blob/develop/docs/translating.md).

For a developer guide, see the [translating dev doc](https://github.com/vector-im/element-web/blob/develop/docs/translating-dev.md).

[<img src="https://translate.element.io/widgets/element-desktop/-/multi-auto.svg" alt="translationsstatus" width="340">](https://translate.element.io/engage/element-desktop/?utm_source=widget)

Report bugs & give feedback
==========================

Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
"license": "Apache-2.0",
"files": [],
"scripts": {
"i18n": "matrix-gen-i18n",
"prunei18n": "matrix-prune-i18n",
"diff-i18n": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && matrix-gen-i18n && matrix-compare-i18n-files src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json",
"mkdirs": "mkdirp packages deploys",
"fetch": "yarn run mkdirs && node scripts/fetch-package.js",
"asar-webapp": "asar p webapp webapp.asar",
Expand All @@ -31,6 +34,7 @@
},
"dependencies": {
"auto-launch": "^5.0.5",
"counterpart": "^0.18.6",
"electron-store": "^6.0.1",
"electron-window-state": "^5.0.3",
"minimist": "^1.2.3",
Expand All @@ -48,6 +52,7 @@
"find-npm-prefix": "^1.0.2",
"fs-extra": "^8.1.0",
"glob": "^7.1.6",
"matrix-web-i18n": "github:matrix-org/matrix-web-i18n",
"mkdirp": "^1.0.3",
"needle": "^2.5.0",
"node-pre-gyp": "^0.15.0",
Expand Down
21 changes: 17 additions & 4 deletions src/electron-main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -57,6 +57,8 @@ try {
}
}

const { _t, AppLocalization } = require('./language-helper');

let seshatSupported = false;
let Seshat;
let SeshatRecovery;
Expand Down Expand Up @@ -86,6 +88,7 @@ let vectorConfig;
let iconPath;
let trayConfig;
let launcher;
let appLocalization;

if (argv["help"]) {
console.log("Options:");
Expand Down Expand Up @@ -268,8 +271,8 @@ const warnBeforeExit = (event, input) => {
if (shouldWarnBeforeExit && exitShortcutPressed) {
const shouldCancelCloseRequest = dialog.showMessageBoxSync(mainWindow, {
type: "question",
buttons: ["Cancel", "Close Element"],
message: "Are you sure you want to quit?",
buttons: [_t("Cancel"), _t("Close Element")],
message: _t("Are you sure you want to quit?"),
defaultId: 1,
cancelId: 0,
}) === 0;
Expand Down Expand Up @@ -366,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;
Expand Down Expand Up @@ -940,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
Expand Down Expand Up @@ -989,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', () => {
Expand Down
1 change: 1 addition & 0 deletions src/i18n/strings/basefile.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
46 changes: 46 additions & 0 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"Cancel": "Cancel",
"Close Element": "Close Element",
"Are you sure you want to quit?": "Are you sure you want to quit?",
"Show/Hide": "Show/Hide",
"Quit": "Quit",
"Edit": "Edit",
"Undo": "Undo",
"Redo": "Redo",
"Cut": "Cut",
"Copy": "Copy",
"Paste": "Paste",
"Paste and Match Style": "Paste and Match Style",
"Delete": "Delete",
"Select All": "Select All",
"View": "View",
"Actual Size": "Actual Size",
"Zoom In": "Zoom In",
"Zoom Out": "Zoom Out",
"Preferences": "Preferences",
"Toggle Full Screen": "Toggle Full Screen",
"Toggle Developer Tools": "Toggle Developer Tools",
"Window": "Window",
"Minimize": "Minimize",
"Close": "Close",
"Help": "Help",
"Element Help": "Element Help",
"About": "About",
"Services": "Services",
"Hide": "Hide",
"Hide Others": "Hide Others",
"Unhide": "Unhide",
"Speech": "Speech",
"Start Speaking": "Start Speaking",
"Stop Speaking": "Stop Speaking",
"Zoom": "Zoom",
"Bring All to Front": "Bring All to Front",
"File": "File",
"Copy image": "Copy image",
"Copy email address": "Copy email address",
"Copy link address": "Copy link address",
"Save image as...": "Save image as...",
"Failed to save image": "Failed to save image",
"The image failed to save": "The image failed to save",
"Add to dictionary": "Add to dictionary"
}
129 changes: 129 additions & 0 deletions src/language-helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
Copyright 2021 New Vector Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

const counterpart = require('counterpart');

const DEFAULT_LOCALE = "en";

function _td(text) {
return 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,
};
62 changes: 36 additions & 26 deletions src/tray.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const {app, Tray, Menu, nativeImage} = require('electron');
const pngToIco = require('png-to-ico');
const path = require('path');
const fs = require('fs');
const { _t } = require('./language-helper');

let trayIcon = null;

Expand All @@ -33,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: `Show/Hide ${config.brand}`,
click: toggleWin,
},
{ type: 'separator' },
{
label: '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;
Expand Down Expand Up @@ -104,3 +90,27 @@ exports.create = function(config) {
trayIcon.setToolTip(title);
});
};

function initApplicationMenu() {
if (!trayIcon) {
return;
}

const contextMenu = Menu.buildFromTemplate([
{
label: _t('Show/Hide'),
click: toggleWin,
},
{ type: 'separator' },
{
label: _t('Quit'),
click: function() {
app.quit();
},
},
]);

trayIcon.setContextMenu(contextMenu);
}

exports.initApplicationMenu = initApplicationMenu;
Loading