Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Localize OS desktop menus #21938

Merged
merged 13 commits into from
Jul 18, 2023
277 changes: 152 additions & 125 deletions desktop/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ let downloadedVersion;
// Note that we have to subscribe to this separately and cannot use Localize.translateLocal,
// because the only way code can be shared between the main and renderer processes at runtime is via the context bridge
// So we track preferredLocale separately via ELECTRON_EVENTS.LOCALE_UPDATED
let preferredLocale = CONST.LOCALES.DEFAULT;
const preferredLocale = CONST.LOCALES.DEFAULT;

const appProtocol = CONST.DEEPLINK_BASE_URL.replace('://', '');

Expand Down Expand Up @@ -150,8 +150,8 @@ const electronUpdater = (browserWindow) => ({
autoUpdater.on(ELECTRON_EVENTS.UPDATE_DOWNLOADED, (info) => {
const systemMenu = Menu.getApplicationMenu();
downloadedVersion = info.version;
systemMenu.getMenuItemById(`updateAppMenuItem-${preferredLocale}`).visible = true;
systemMenu.getMenuItemById(`checkForUpdateMenuItem-${preferredLocale}`).visible = false;
systemMenu.getMenuItemById(`update`).visible = true;
systemMenu.getMenuItemById(`checkForUpdates`).visible = false;
if (browserWindow.isVisible()) {
browserWindow.webContents.send(ELECTRON_EVENTS.UPDATE_DOWNLOADED, info.version);
} else {
Expand All @@ -170,67 +170,21 @@ const electronUpdater = (browserWindow) => ({
/*
* @param {Menu} systemMenu
*/
const localizeMenuItems = (browserWindow, systemMenu) => {
// List the Expensify Chat instance under the Window menu, even when it's hidden
systemMenu.insert(
4,
new MenuItem({
id: `historyMenuItem-${preferredLocale}`,
label: Localize.translate(preferredLocale, 'desktopApplicationMenu.history'),
submenu: [
{
id: `backMenuItem-${preferredLocale}`,
label: Localize.translate(preferredLocale, 'historyMenu.back'),
accelerator: process.platform === 'darwin' ? 'Cmd+[' : 'Shift+[',
click: () => {
browserWindow.webContents.goBack();
},
},
{
id: `forwardMenuItem-${preferredLocale}`,
label: Localize.translate(preferredLocale, 'historyMenu.forward'),
accelerator: process.platform === 'darwin' ? 'Cmd+]' : 'Shift+]',
click: () => {
browserWindow.webContents.goForward();
},
},
],
}),
);

// Defines the system-level menu item to manually apply an update
// This menu item should become visible after an update is downloaded and ready to be applied
const updateAppMenuItem = new MenuItem({
id: `updateAppMenuItem-${preferredLocale}`,
label: Localize.translate(preferredLocale, 'desktopApplicationMenu.updateExpensify'),
visible: false,
click: quitAndInstallWithUpdate,
});

// System-level menu item to manually check for App updates
const checkForUpdateMenuItem = new MenuItem({
id: `checkForUpdateMenuItem-${preferredLocale}`,
label: Localize.translate(preferredLocale, 'desktopApplicationMenu.checkForUpdates'),
visible: true,
click: manuallyCheckForUpdates,
});

// Defines the system-level menu item for opening keyboard shortcuts modal
const keyboardShortcutsMenuItem = new MenuItem({
id: `keyboardShortcutsMenuItem-${preferredLocale}`,
label: Localize.translate(preferredLocale, 'initialSettingsPage.aboutPage.viewKeyboardShortcuts'),
accelerator: 'CmdOrCtrl+J',
click: () => {
showKeyboardShortcutsModal(browserWindow);
},
const localizeMenuItems = (submenu, updatedLocale) =>
_.map(submenu, (menu) => {
const newMenu = _.clone(menu);
if (menu.id) {
const labelTranslation = Localize.translate(updatedLocale, `desktopApplicationMenu.${menu.id}`);
if (labelTranslation) {
newMenu.label = labelTranslation;
}
}
if (menu.submenu) {
newMenu.submenu = localizeMenuItems(menu.submenu, updatedLocale);
}
return newMenu;
});

const appMenu = _.find(systemMenu.items, (item) => item.role === 'appmenu');
appMenu.submenu.insert(1, updateAppMenuItem);
appMenu.submenu.insert(2, checkForUpdateMenuItem);
appMenu.submenu.insert(3, keyboardShortcutsMenuItem);
};

const mainWindow = () => {
let deeplinkUrl;
let browserWindow;
Expand Down Expand Up @@ -328,41 +282,141 @@ const mainWindow = () => {
browserWindow.setTitle('New Expensify');
}

const systemMenu = Menu.getApplicationMenu();

// Register the custom Paste and Match Style command and place it near the default shortcut of the same role.
const editMenu = _.find(systemMenu.items, (item) => item.role === 'editmenu');
editMenu.submenu.insert(
6,
new MenuItem({
role: 'pasteAndMatchStyle',
accelerator: 'CmdOrCtrl+Shift+V',
}),
);

// Append custom context menu items using the preferred language of the user.
localizeMenuItems(browserWindow, systemMenu);

// On mac, pressing cmd++ actually sends a cmd+=. cmd++ is generally the zoom in shortcut, but this is
// not properly listened for by electron. Adding in an invisible cmd+= listener fixes this.
const viewWindow = _.find(systemMenu.items, (item) => item.role === 'viewmenu');
viewWindow.submenu.append(
new MenuItem({
role: 'zoomin',
accelerator: 'CommandOrControl+=',
visible: false,
}),
);
const windowMenu = _.find(systemMenu.items, (item) => item.role === 'windowmenu');
windowMenu.submenu.append(new MenuItem({type: 'separator'}));
windowMenu.submenu.append(
new MenuItem({
label: 'New Expensify',
accelerator: 'CmdOrCtrl+1',
click: () => browserWindow.show(),
}),
);
Menu.setApplicationMenu(systemMenu);
const initialMenuTemplate = [
{
id: 'mainMenu',
label: Localize.translate(preferredLocale, `desktopApplicationMenu.mainMenu`),
submenu: [
{id: 'about', role: 'about'},
{id: 'update', label: Localize.translate(preferredLocale, `desktopApplicationMenu.update`), click: quitAndInstallWithUpdate, visible: false},
{id: 'checkForUpdates', label: Localize.translate(preferredLocale, `desktopApplicationMenu.checkForUpdates`), click: manuallyCheckForUpdates},
{
id: 'viewShortcuts',
label: Localize.translate(preferredLocale, `desktopApplicationMenu.viewShortcuts`),
accelerator: 'CmdOrCtrl+I',
click: () => {
showKeyboardShortcutsModal(browserWindow);
},
},
{type: 'separator'},
{id: 'services', role: 'services'},
{type: 'separator'},
{id: 'hide', role: 'hide'},
{id: 'hideOthers', role: 'hideOthers'},
{id: 'showAll', role: 'unhide'},
{type: 'separator'},
{id: 'quit', role: 'quit'},
],
},
{
id: 'fileMenu',
label: Localize.translate(preferredLocale, `desktopApplicationMenu.fileMenu`),
submenu: [{id: 'closeWindow', role: 'close', accelerator: 'Cmd+w'}],
},
{
id: 'editMenu',
label: Localize.translate(preferredLocale, `desktopApplicationMenu.editMenu`),
submenu: [
{id: 'undo', role: 'undo'},
{id: 'redo', role: 'redo'},
{type: 'separator'},
{id: 'cut', role: 'cut'},
{id: 'copy', role: 'copy'},
{id: 'paste', role: 'paste'},
{id: 'pasteAndMatchStyle', role: 'pasteAndMatchStyle'},
{id: 'delete', role: 'delete'},
{id: 'selectAll', role: 'selectAll'},
{type: 'separator'},
{
id: 'speechSubmenu',
label: Localize.translate(preferredLocale, `desktopApplicationMenu.speechSubmenu`),
submenu: [
{id: 'startSpeaking', role: 'startSpeaking'},
{id: 'stopSpeaking', role: 'stopSpeaking'},
],
},
],
},
{
id: 'viewMenu',
label: Localize.translate(preferredLocale, `desktopApplicationMenu.viewMenu`),
submenu: [
{id: 'reload', role: 'reload'},
{id: 'forceReload', role: 'forceReload'},
{type: 'separator'},
{id: 'resetZoom', role: 'resetZoom'},
{id: 'zoomIn', role: 'zoomIn'},
{id: 'zoomOut', role: 'zoomOut'},
{type: 'separator'},
{id: 'togglefullscreen', role: 'togglefullscreen'},
],
},
{
id: 'historyMenu',
label: Localize.translate(preferredLocale, `desktopApplicationMenu.historyMenu`),
submenu: [
{
id: 'back',
role: 'back',
accelerator: process.platform === 'darwin' ? 'Cmd+[' : 'Shift+[',
click: () => {
browserWindow.webContents.goBack();
},
},
{
id: 'forward',
role: 'forward',
accelerator: process.platform === 'darwin' ? 'Cmd+]' : 'Shift+]',
click: () => {
browserWindow.webContents.goForward();
},
},
],
},
{
id: 'windowMenu',
role: 'windowMenu',
},
{
id: 'helpMenu',
label: Localize.translate(preferredLocale, `desktopApplicationMenu.helpMenu`),
role: 'help',
submenu: [
{
id: 'learnMore',
label: Localize.translate(preferredLocale, `desktopApplicationMenu.learnMore`),
click: () => {
shell.openExternal(CONST.MENU_HELP_URLS.LEARN_MORE);
},
},
{
id: 'documentation',
label: Localize.translate(preferredLocale, `desktopApplicationMenu.documentation`),
click: () => {
shell.openExternal(CONST.MENU_HELP_URLS.DOCUMENTATION);
},
},
{
id: 'communityDiscussions',
label: Localize.translate(preferredLocale, `desktopApplicationMenu.communityDiscussions`),
click: () => {
shell.openExternal(CONST.MENU_HELP_URLS.COMMUNITY_DISCUSSIONS);
},
},
{
id: 'searchIssues',
label: Localize.translate(preferredLocale, `desktopApplicationMenu.searchIssues`),
click: () => {
shell.openExternal(CONST.MENU_HELP_URLS.SEARCH_ISSUES);
},
},
],
},
];

// Build and set the initial menu
const initialMenu = Menu.buildFromTemplate(localizeMenuItems(initialMenuTemplate, preferredLocale));
Menu.setApplicationMenu(initialMenu);

// When the user clicks a link that has target="_blank" (which is all external links)
// open the default browser instead of a new electron window
Expand Down Expand Up @@ -439,34 +493,7 @@ const mainWindow = () => {
}

ipcMain.on(ELECTRON_EVENTS.LOCALE_UPDATED, (event, updatedLocale) => {
// Store the old locale so we can hide/remove these items after adding/showing the new ones.
const outdatedLocale = preferredLocale;
preferredLocale = updatedLocale;

const currentHistoryMenuItem = systemMenu.getMenuItemById(`historyMenuItem-${outdatedLocale}`);
const currentUpdateAppMenuItem = systemMenu.getMenuItemById(`updateAppMenuItem-${outdatedLocale}`);
const currentCheckForUpdateMenuItem = systemMenu.getMenuItemById(`checkForUpdateMenuItem-${outdatedLocale}`);
const currentKeyboardShortcutsMenuItem = systemMenu.getMenuItemById(`keyboardShortcutsMenuItem-${outdatedLocale}`);

// If we have previously added those languages, don't add new menu items, reshow them.
if (!systemMenu.getMenuItemById(`updateAppMenuItem-${updatedLocale}`)) {
// Update the labels and ids to use the translations.
localizeMenuItems(browserWindow, systemMenu);
}

// Show the localized menu items if there were visible before we updated the locale.
systemMenu.getMenuItemById(`updateAppMenuItem-${updatedLocale}`).visible = currentUpdateAppMenuItem.visible;
systemMenu.getMenuItemById(`checkForUpdateMenuItem-${updatedLocale}`).visible = currentCheckForUpdateMenuItem.visible;
systemMenu.getMenuItemById(`keyboardShortcutsMenuItem-${updatedLocale}`).visible = currentKeyboardShortcutsMenuItem.visible;
systemMenu.getMenuItemById(`historyMenuItem-${updatedLocale}`).visible = currentHistoryMenuItem.visible;

// Since we can't remove menu items, we hide the old ones.
currentUpdateAppMenuItem.visible = false;
currentCheckForUpdateMenuItem.visible = false;
currentKeyboardShortcutsMenuItem.visible = false;
currentHistoryMenuItem.visible = false;

Menu.setApplicationMenu(systemMenu);
Menu.setApplicationMenu(Menu.buildFromTemplate(localizeMenuItems(initialMenuTemplate, updatedLocale)));
});

ipcMain.on(ELECTRON_EVENTS.REQUEST_VISIBILITY, (event) => {
Expand Down
7 changes: 7 additions & 0 deletions src/CONST.js
Original file line number Diff line number Diff line change
Expand Up @@ -2450,6 +2450,13 @@ const CONST = {
NUMBER_OF_TAPS: 4,
},

MENU_HELP_URLS: {
LEARN_MORE: 'https://www.expensify.com',
DOCUMENTATION: 'https://github.com/Expensify/App/blob/main/README.md',
COMMUNITY_DISCUSSIONS: 'https://expensify.slack.com/archives/C01GTK53T8Q',
SEARCH_ISSUES: 'https://github.com/Expensify/App/issues',
},

PAYPAL_SUPPORTED_CURRENCIES: [
'AUD',
'BRL',
Expand Down
42 changes: 39 additions & 3 deletions src/languages/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -1337,10 +1337,46 @@ export default {
},
},
desktopApplicationMenu: {
aboutExpensify: 'About New Expensify',
updateExpensify: 'Update New Expensify',
mainMenu: 'New Expensify',
about: 'About New Expensify',
update: 'Update New Expensify',
checkForUpdates: 'Check for updates',
history: 'History',
viewShortcuts: 'View keyboard shortcuts',
services: 'Services',
hide: 'Hide New Expensify',
hideOthers: 'Hide Others',
showAll: 'Show All',
quit: 'Quit New Expensify',
fileMenu: 'File',
closeWindow: 'Close Window',
editMenu: 'Edit',
undo: 'Undo',
redo: 'Redo',
cut: 'Cut',
copy: 'Copy',
paste: 'Paste',
pasteAndMatchStyle: 'Paste and Match Style',
delete: 'Delete',
selectAll: 'Select All',
speechSubmenu: 'Speech',
startSpeaking: 'Start Speaking',
stopSpeaking: 'Stop Speaking',
viewMenu: 'View',
reload: 'Reload',
forceReload: 'Force Reload',
resetZoom: 'Actual Size',
zoomIn: 'Zoom In',
zoomOut: 'Zoom Out',
togglefullscreen: 'Toggle Full Screen',
historyMenu: 'History',
back: 'Back',
forward: 'Forward',
windowMenu: 'Window',
helpMenu: 'Help',
learnMore: 'Learn more',
documentation: 'Documentation',
communityDiscussions: 'Community Discussions',
searchIssues: 'Search Issues',
},
historyMenu: {
forward: 'Forward',
Expand Down
Loading
Loading