Merge pull request #188 from vector-im/gsouquet-i18n-td
This commit is contained in:
@@ -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,6 +57,8 @@ try {
|
||||
}
|
||||
}
|
||||
|
||||
const { _t, AppLocalization } = require('./language-helper');
|
||||
|
||||
let seshatSupported = false;
|
||||
let Seshat;
|
||||
let SeshatRecovery;
|
||||
@@ -84,6 +86,7 @@ let vectorConfig;
|
||||
let iconPath;
|
||||
let trayConfig;
|
||||
let launcher;
|
||||
let appLocalization;
|
||||
|
||||
if (argv["help"]) {
|
||||
console.log("Options:");
|
||||
@@ -266,8 +269,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;
|
||||
@@ -364,6 +367,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;
|
||||
@@ -915,7 +921,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
|
||||
@@ -964,6 +969,14 @@ app.on('ready', async () => {
|
||||
}
|
||||
|
||||
webContentsHandler(mainWindow.webContents);
|
||||
|
||||
appLocalization = new AppLocalization({
|
||||
store,
|
||||
components: [
|
||||
() => tray.initApplicationMenu(),
|
||||
() => Menu.setApplicationMenu(buildMenuTemplate()),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
|
||||
1
src/i18n/strings/basefile.json
Normal file
1
src/i18n/strings/basefile.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
46
src/i18n/strings/en_EN.json
Normal file
46
src/i18n/strings/en_EN.json
Normal file
@@ -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
src/language-helper.js
Normal file
129
src/language-helper.js
Normal file
@@ -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
src/tray.js
62
src/tray.js
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -15,129 +15,212 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
const {app, shell, Menu} = require('electron');
|
||||
const { _t } = require('./language-helper');
|
||||
|
||||
// Menu template from http://electron.atom.io/docs/api/menu/, edited
|
||||
const template = [
|
||||
{
|
||||
label: '&Edit',
|
||||
submenu: [
|
||||
{ role: 'undo' },
|
||||
{ role: 'redo' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'cut' },
|
||||
{ role: 'copy' },
|
||||
{ role: 'paste' },
|
||||
{ role: 'pasteandmatchstyle' },
|
||||
{ role: 'delete' },
|
||||
{ role: 'selectall' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '&View',
|
||||
submenu: [
|
||||
{ type: 'separator' },
|
||||
{ role: 'resetzoom' },
|
||||
{ role: 'zoomin', accelerator: 'CommandOrControl+=' },
|
||||
{ role: 'zoomout' },
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Preferences',
|
||||
accelerator: 'Command+,', // Mac-only accelerator
|
||||
click() { global.mainWindow.webContents.send('preferences'); },
|
||||
},
|
||||
{ role: 'togglefullscreen' },
|
||||
{ role: 'toggledevtools' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '&Window',
|
||||
role: 'window',
|
||||
submenu: [
|
||||
{ role: 'minimize' },
|
||||
{ role: 'close' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '&Help',
|
||||
role: 'help',
|
||||
submenu: [
|
||||
{
|
||||
label: '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: 'Speech',
|
||||
label: _t('Edit'),
|
||||
accelerator: 'e',
|
||||
submenu: [
|
||||
{ role: 'startspeaking' },
|
||||
{ role: 'stopspeaking' },
|
||||
{
|
||||
role: 'undo',
|
||||
label: _t('Undo'),
|
||||
},
|
||||
{
|
||||
role: 'redo',
|
||||
label: _t('Redo'),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
role: 'cut',
|
||||
label: _t('Cut'),
|
||||
},
|
||||
{
|
||||
role: 'copy',
|
||||
label: _t('Copy'),
|
||||
},
|
||||
{
|
||||
role: 'paste',
|
||||
label: _t('Paste'),
|
||||
},
|
||||
{
|
||||
role: 'pasteandmatchstyle',
|
||||
label: _t('Paste and Match Style'),
|
||||
},
|
||||
{
|
||||
role: 'delete',
|
||||
label: _t('Delete'),
|
||||
},
|
||||
{
|
||||
role: 'selectall',
|
||||
label: _t('Select All'),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Window menu.
|
||||
// This also has specific functionality on macOS
|
||||
template[3].submenu = [
|
||||
{
|
||||
label: 'Close',
|
||||
accelerator: 'CmdOrCtrl+W',
|
||||
role: 'close',
|
||||
},
|
||||
{
|
||||
label: 'Minimize',
|
||||
accelerator: 'CmdOrCtrl+M',
|
||||
role: 'minimize',
|
||||
label: _t('View'),
|
||||
accelerator: 'V',
|
||||
submenu: [
|
||||
{ type: 'separator' },
|
||||
{
|
||||
role: 'resetzoom',
|
||||
label: _t('Actual Size'),
|
||||
},
|
||||
{
|
||||
role: 'zoomin',
|
||||
accelerator: 'CommandOrControl+=',
|
||||
label: _t('Zoom In'),
|
||||
},
|
||||
{
|
||||
role: 'zoomout',
|
||||
label: _t('Zoom Out'),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: _t('Preferences'),
|
||||
accelerator: 'Command+,', // Mac-only accelerator
|
||||
click() { global.mainWindow.webContents.send('preferences'); },
|
||||
},
|
||||
{
|
||||
role: 'togglefullscreen',
|
||||
label: _t('Toggle Full Screen'),
|
||||
},
|
||||
{
|
||||
role: 'toggledevtools',
|
||||
label: _t('Toggle Developer Tools'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Zoom',
|
||||
role: 'zoom',
|
||||
label: _t('Window'),
|
||||
accelerator: 'w',
|
||||
role: 'window',
|
||||
submenu: [
|
||||
{
|
||||
role: 'minimize',
|
||||
label: _t('Minimize'),
|
||||
},
|
||||
{
|
||||
role: 'close',
|
||||
label: _t('Close'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'separator',
|
||||
},
|
||||
{
|
||||
label: 'Bring All to Front',
|
||||
role: 'front',
|
||||
label: _t('Help'),
|
||||
accelerator: 'h',
|
||||
role: 'help',
|
||||
submenu: [
|
||||
{
|
||||
label: _t('Element Help'),
|
||||
click() { shell.openExternal('https://element.io/help'); },
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
} else {
|
||||
template.unshift({
|
||||
label: '&File',
|
||||
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
|
||||
role: 'appMenu',
|
||||
label: app.name,
|
||||
submenu: [
|
||||
{
|
||||
role: 'about',
|
||||
label: _t('About'),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
role: 'services',
|
||||
label: _t('Services'),
|
||||
submenu: [],
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
role: 'hide',
|
||||
label: _t('Hide'),
|
||||
},
|
||||
{
|
||||
role: 'hideothers',
|
||||
label: _t('Hide Others'),
|
||||
},
|
||||
{
|
||||
role: 'unhide',
|
||||
label: _t('Unhide'),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
role: 'quit',
|
||||
label: _t('Quit'),
|
||||
},
|
||||
],
|
||||
});
|
||||
// Edit menu.
|
||||
// This has a 'speech' section on macOS
|
||||
template[1].submenu.push(
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: _t('Speech'),
|
||||
submenu: [
|
||||
{
|
||||
role: 'startspeaking',
|
||||
label: _t('Start Speaking'),
|
||||
},
|
||||
{
|
||||
role: 'stopspeaking',
|
||||
label: _t('Stop Speaking'),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Window menu.
|
||||
// This also has specific functionality on macOS
|
||||
template[3].submenu = [
|
||||
{
|
||||
label: _t('Close'),
|
||||
accelerator: 'CmdOrCtrl+W',
|
||||
role: 'close',
|
||||
},
|
||||
{
|
||||
label: _t('Minimize'),
|
||||
accelerator: 'CmdOrCtrl+M',
|
||||
role: 'minimize',
|
||||
},
|
||||
{
|
||||
label: _t('Zoom'),
|
||||
role: 'zoom',
|
||||
},
|
||||
{
|
||||
type: 'separator',
|
||||
},
|
||||
{
|
||||
label: _t('Bring All to Front'),
|
||||
role: 'front',
|
||||
},
|
||||
];
|
||||
} else {
|
||||
template.unshift({
|
||||
label: _t('File'),
|
||||
accelerator: 'f',
|
||||
submenu: [
|
||||
// For some reason, 'about' does not seem to work on windows.
|
||||
/*{
|
||||
role: 'about',
|
||||
label: _t('About'),
|
||||
},*/
|
||||
{
|
||||
role: 'quit',
|
||||
label: _t('Quit'),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
return Menu.buildFromTemplate(template);
|
||||
}
|
||||
|
||||
module.exports = Menu.buildFromTemplate(template);
|
||||
module.exports = buildMenuTemplate;
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ const url = require('url');
|
||||
const fs = require('fs');
|
||||
const request = require('request');
|
||||
const path = require('path');
|
||||
const { _t } = require('./language-helper');
|
||||
|
||||
const MAILTO_PREFIX = "mailto:";
|
||||
|
||||
@@ -73,7 +74,8 @@ function onLinkContextMenu(ev, params) {
|
||||
|
||||
if (params.hasImageContents) {
|
||||
popupMenu.append(new MenuItem({
|
||||
label: '&Copy image',
|
||||
label: _t('Copy image'),
|
||||
accelerator: 'c',
|
||||
click() {
|
||||
ev.sender.copyImageAt(params.x, params.y);
|
||||
},
|
||||
@@ -85,14 +87,16 @@ function onLinkContextMenu(ev, params) {
|
||||
// Special-case e-mail URLs to strip the `mailto:` like modern browsers do
|
||||
if (url.startsWith(MAILTO_PREFIX)) {
|
||||
popupMenu.append(new MenuItem({
|
||||
label: 'Copy email &address',
|
||||
label: _t('Copy email address'),
|
||||
accelerator: 'a',
|
||||
click() {
|
||||
clipboard.writeText(url.substr(MAILTO_PREFIX.length));
|
||||
},
|
||||
}));
|
||||
} else {
|
||||
popupMenu.append(new MenuItem({
|
||||
label: 'Copy link &address',
|
||||
label: _t('Copy link address'),
|
||||
accelerator: 'a',
|
||||
click() {
|
||||
clipboard.writeText(url);
|
||||
},
|
||||
@@ -104,7 +108,8 @@ function onLinkContextMenu(ev, params) {
|
||||
// only the renderer can resolve them so don't give the user an option to.
|
||||
if (params.hasImageContents && !url.startsWith('blob:')) {
|
||||
popupMenu.append(new MenuItem({
|
||||
label: 'Sa&ve image as...',
|
||||
label: _t('Save image as...'),
|
||||
accelerator: 'a',
|
||||
async click() {
|
||||
const targetFileName = params.titleText || "image.png";
|
||||
const {filePath} = await dialog.showSaveDialog({
|
||||
@@ -123,8 +128,8 @@ function onLinkContextMenu(ev, params) {
|
||||
console.error(err);
|
||||
dialog.showMessageBox({
|
||||
type: "error",
|
||||
title: "Failed to save image",
|
||||
message: "The image failed to save",
|
||||
title: _t("Failed to save image"),
|
||||
message: _t("The image failed to save"),
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -151,7 +156,7 @@ function _CutCopyPasteSelectContextMenus(params) {
|
||||
options.push({
|
||||
type: 'separator',
|
||||
}, {
|
||||
label: 'Add to dictionary',
|
||||
label: _t('Add to dictionary'),
|
||||
click: (menuItem, browserWindow) => {
|
||||
browserWindow.webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord);
|
||||
},
|
||||
@@ -162,22 +167,26 @@ function _CutCopyPasteSelectContextMenus(params) {
|
||||
|
||||
options.push({
|
||||
role: 'cut',
|
||||
label: 'Cu&t',
|
||||
label: _t('Cut'),
|
||||
accelerator: 't',
|
||||
enabled: params.editFlags.canCut,
|
||||
}, {
|
||||
role: 'copy',
|
||||
label: '&Copy',
|
||||
label: _t('Copy'),
|
||||
accelerator: 'c',
|
||||
enabled: params.editFlags.canCopy,
|
||||
}, {
|
||||
role: 'paste',
|
||||
label: '&Paste',
|
||||
label: _t('Paste'),
|
||||
accelerator: 'p',
|
||||
enabled: params.editFlags.canPaste,
|
||||
}, {
|
||||
role: 'pasteandmatchstyle',
|
||||
enabled: params.editFlags.canPaste,
|
||||
}, {
|
||||
role: 'selectall',
|
||||
label: "Select &All",
|
||||
label: _t("Select All"),
|
||||
accelerator: 'a',
|
||||
enabled: params.editFlags.canSelectAll,
|
||||
});
|
||||
return options;
|
||||
|
||||
Reference in New Issue
Block a user