[MM-14058] Add support for i18n (#2190)

* Add language files

* Add react-intl, mmjstool, setup for adding translations

* Translated main module

* Translations for renderer

* A few minor fixes

* More fixes

* Add CI, add missing menu translations, other cleanup

* Added setting to manually select the language of the app

* Force English for E2e

* Unit tests

* Fix mmjstool

* Move set language to before update menu

* PR feedback
This commit is contained in:
Devin Binnie
2022-07-14 11:04:18 -04:00
committed by GitHub
parent 22c97591d5
commit 59e4e7e516
92 changed files with 3554 additions and 2375 deletions

View File

@@ -14,6 +14,10 @@ jest.mock('electron', () => ({
jest.mock('electron-is-dev', () => false);
jest.mock('main/i18nManager', () => ({
localizeMessage: jest.fn(),
}));
describe('main/AutoLauncher', () => {
let autoLauncher;
const isEnabled = jest.fn();

View File

@@ -22,7 +22,7 @@ export class AutoLauncher {
return;
}
const appLauncher = new AutoLaunch({
name: 'Mattermost',
name: app.name,
});
const enabled = await appLauncher.isEnabled();
if (enabled) {

View File

@@ -36,6 +36,10 @@ jest.mock('child_process', () => ({
spawn: jest.fn(),
}));
jest.mock('main/i18nManager', () => ({
localizeMessage: jest.fn(),
}));
describe('main/CriticalErrorHandler', () => {
const criticalErrorHandler = new CriticalErrorHandler();
beforeEach(() => {

View File

@@ -8,12 +8,9 @@ import os from 'os';
import path from 'path';
import {app, BrowserWindow, dialog} from 'electron';
import log from 'electron-log';
const BUTTON_OK = 'OK';
const BUTTON_SHOW_DETAILS = 'Show Details';
const BUTTON_REOPEN = 'Reopen';
import {localizeMessage} from 'main/i18nManager';
function createErrorReport(err: Error) {
// eslint-disable-next-line no-undef
@@ -52,8 +49,11 @@ export class CriticalErrorHandler {
dialog.showMessageBox(this.mainWindow, {
type: 'warning',
title: app.name,
message: 'The window is no longer responsive.\nDo you wait until the window becomes responsive again?',
buttons: ['No', 'Yes'],
message: localizeMessage('main.CriticalErrorHandler.unresponsive.dialog.message', 'The window is no longer responsive.\nDo you wait until the window becomes responsive again?'),
buttons: [
localizeMessage('label.no', 'No'),
localizeMessage('label.yes', 'Yes'),
],
defaultId: 0,
}).then(({response}) => {
if (response === 0) {
@@ -69,9 +69,17 @@ export class CriticalErrorHandler {
fs.writeFileSync(file, report.replace(new RegExp('\\n', 'g'), os.EOL));
if (app.isReady()) {
const buttons = [BUTTON_SHOW_DETAILS, BUTTON_OK, BUTTON_REOPEN];
const buttons = [
localizeMessage('main.CriticalErrorHandler.uncaughtException.button.showDetails', 'Show Details'),
localizeMessage('label.ok', 'OK'),
localizeMessage('main.CriticalErrorHandler.uncaughtException.button.reopen', 'Reopen'),
];
let indexOfReopen = 2;
let indexOfShowDetails = 0;
if (process.platform === 'darwin') {
buttons.reverse();
indexOfReopen = 0;
indexOfShowDetails = 2;
}
if (!this.mainWindow?.isVisible) {
return;
@@ -81,15 +89,24 @@ export class CriticalErrorHandler {
{
type: 'error',
title: app.name,
message: `The ${app.name} app quit unexpectedly. Click "Show Details" to learn more or "Reopen" to open the application again.\n\nInternal error: ${err.message}`,
message: localizeMessage(
'main.CriticalErrorHandler.uncaughtException.dialog.message',
'The {appName} app quit unexpectedly. Click "{showDetails}" to learn more or "{reopen}" to open the application again.\n\nInternal error: {err}',
{
appName: app.name,
showDetails: localizeMessage('main.CriticalErrorHandler.uncaughtException.button.showDetails', 'Show Details'),
reopen: localizeMessage('main.CriticalErrorHandler.uncaughtException.button.reopen', 'Reopen'),
err: err.message,
},
),
buttons,
defaultId: buttons.indexOf(BUTTON_REOPEN),
defaultId: indexOfReopen,
noLink: true,
},
).then(({response}) => {
let child;
switch (response) {
case buttons.indexOf(BUTTON_SHOW_DETAILS):
case indexOfShowDetails:
child = openDetachedExternal(file);
if (child) {
child.on(
@@ -101,7 +118,7 @@ export class CriticalErrorHandler {
child.unref();
}
break;
case buttons.indexOf(BUTTON_REOPEN):
case indexOfReopen:
app.relaunch();
break;
}

View File

@@ -28,6 +28,9 @@ function triageArgs(args: string[]) {
// Note that yargs is able to exit the node process when handling
// certain flags, like version or help.
// https://github.com/yargs/yargs/blob/main/docs/api.md#exitprocessenable
// TODO: Translations?
function parseArgs(args: string[]) {
return yargs.
alias('dataDir', 'd').

View File

@@ -131,6 +131,7 @@ const configDataSchemaV3 = Joi.object<ConfigV3>({
alwaysMinimize: Joi.boolean(),
alwaysClose: Joi.boolean(),
logLevel: Joi.string().default('info'),
appLanguage: Joi.string().allow(''),
});
// eg. data['community.mattermost.com'] = { data: 'certificate data', issuerName: 'COMODO RSA Domain Validation Secure Server CA'};

View File

@@ -49,6 +49,10 @@ jest.mock('./windows/windowManager', () => ({
getMainWindow: jest.fn(),
}));
jest.mock('main/i18nManager', () => ({
localizeMessage: jest.fn(),
}));
describe('main/allowProtocolDialog', () => {
describe('init', () => {
it('should copy data from file when no error', () => {

View File

@@ -8,6 +8,8 @@ import fs from 'fs';
import {dialog, shell} from 'electron';
import log from 'electron-log';
import {localizeMessage} from 'main/i18nManager';
import {protocols} from '../../electron-builder.json';
import * as Validator from './Validator';
@@ -54,15 +56,15 @@ export class AllowProtocolDialog {
return;
}
dialog.showMessageBox(mainWindow, {
title: 'Non http(s) protocol',
message: `${protocol} link requires an external application.`,
detail: `The requested link is ${URL} . Do you want to continue?`,
title: localizeMessage('main.allowProtocolDialog.title', 'Non http(s) protocol'),
message: localizeMessage('main.allowProtocolDialog.message', '{protocol} link requires an external application.', {protocol}),
detail: localizeMessage('main.allowProtocolDialog.detail', 'The requested link is {URL}. Do you want to continue?', {URL}),
defaultId: 2,
type: 'warning',
buttons: [
'Yes',
`Yes (Save ${protocol} as allowed)`,
'No',
localizeMessage('label.yes', 'Yes'),
localizeMessage('main.allowProtocolDialog.button.saveProtocolAsAllowed', 'Yes (Save {protocol} as allowed)', {protocol}),
localizeMessage('label.no', 'No'),
],
cancelId: 2,
noLink: true,

View File

@@ -33,6 +33,9 @@ jest.mock('main/certificateStore', () => ({
add: jest.fn(),
save: jest.fn(),
}));
jest.mock('main/i18nManager', () => ({
localizeMessage: jest.fn(),
}));
jest.mock('main/tray/tray', () => ({}));
jest.mock('main/windows/windowManager', () => ({
getMainWindow: jest.fn(),

View File

@@ -8,6 +8,7 @@ import urlUtils from 'common/utils/url';
import updateManager from 'main/autoUpdater';
import CertificateStore from 'main/certificateStore';
import {localizeMessage} from 'main/i18nManager';
import {destroyTray} from 'main/tray/tray';
import WindowManager from 'main/windows/windowManager';
@@ -96,8 +97,8 @@ export async function handleAppCertificateError(event: Event, webContents: WebCo
certificateErrorCallbacks.set(errorID, callback);
return;
}
const extraDetail = CertificateStore.isExisting(origin) ? 'Certificate is different from previous one.\n\n' : '';
const detail = `${extraDetail}origin: ${origin}\nError: ${error}`;
const extraDetail = CertificateStore.isExisting(origin) ? localizeMessage('main.app.app.handleAppCertificateError.dialog.extraDetail', 'Certificate is different from previous one.\n\n') : '';
const detail = localizeMessage('main.app.app.handleAppCertificateError.certError.dialog.detail', '{extraDetail}origin: {origin}\nError: {error}', {extraDetail, origin, error});
certificateErrorCallbacks.set(errorID, callback);
@@ -109,21 +110,27 @@ export async function handleAppCertificateError(event: Event, webContents: WebCo
try {
let result = await dialog.showMessageBox(mainWindow, {
title: 'Certificate Error',
message: 'There is a configuration issue with this Mattermost server, or someone is trying to intercept your connection. You also may need to sign into the Wi-Fi you are connected to using your web browser.',
title: localizeMessage('main.app.app.handleAppCertificateError.certError.dialog.title', 'Certificate Error'),
message: localizeMessage('main.app.app.handleAppCertificateError.certError.dialog.message', 'There is a configuration issue with this Mattermost server, or someone is trying to intercept your connection. You also may need to sign into the Wi-Fi you are connected to using your web browser.'),
type: 'error',
detail,
buttons: ['More Details', 'Cancel Connection'],
buttons: [
localizeMessage('main.app.app.handleAppCertificateError.certError.button.moreDetails', 'More Details'),
localizeMessage('main.app.app.handleAppCertificateError.certError.button.cancelConnection', 'Cancel Connection'),
],
cancelId: 1,
});
if (result.response === 0) {
result = await dialog.showMessageBox(mainWindow, {
title: 'Certificate Not Trusted',
message: `Certificate from "${certificate.issuerName}" is not trusted.`,
title: localizeMessage('main.app.app.handleAppCertificateError.certNotTrusted.dialog.title', 'Certificate Not Trusted'),
message: localizeMessage('main.app.app.handleAppCertificateError.certNotTrusted.dialog.message', 'Certificate from "{issuerName}" is not trusted.', {issuerName: certificate.issuerName}),
detail: extraDetail,
type: 'error',
buttons: ['Trust Insecure Certificate', 'Cancel Connection'],
buttons: [
localizeMessage('main.app.app.handleAppCertificateError.certNotTrusted.button.trustInsecureCertificate', 'Trust Insecure Certificate'),
localizeMessage('main.app.app.handleAppCertificateError.certNotTrusted.button.cancelConnection', 'Cancel Connection'),
],
cancelId: 1,
checkboxChecked: false,
checkboxLabel: "Don't ask again",

View File

@@ -39,6 +39,8 @@ jest.mock('electron', () => ({
setAppUserModelId: jest.fn(),
getVersion: jest.fn(),
whenReady: jest.fn(),
getLocale: jest.fn(),
getLocaleCountryCode: jest.fn(),
},
ipcMain: {
on: jest.fn(),
@@ -54,6 +56,11 @@ jest.mock('electron', () => ({
},
}));
jest.mock('main/i18nManager', () => ({
localizeMessage: jest.fn(),
setLocale: jest.fn(),
}));
jest.mock('electron-devtools-installer', () => {
return () => ({
REACT_DEVELOPER_TOOLS: 'react-developer-tools',

View File

@@ -47,6 +47,7 @@ import {setupBadge} from 'main/badge';
import CertificateManager from 'main/certificateManager';
import {updatePaths} from 'main/constants';
import CriticalErrorHandler from 'main/CriticalErrorHandler';
import i18nManager, {localizeMessage} from 'main/i18nManager';
import {displayDownloadCompleted} from 'main/notifications';
import parseArgs from 'main/ParseArgs';
import TrustedOriginsStore from 'main/trustedOrigins';
@@ -359,7 +360,7 @@ function initializeAfterAppReady() {
const filters = [];
if (fileElements.length > 1) {
filters.push({
name: 'All files',
name: localizeMessage('main.app.initialize.downloadBox.allFiles', 'All files'),
extensions: ['*'],
});
}
@@ -376,6 +377,14 @@ function initializeAfterAppReady() {
});
});
// needs to be done after app ready
// must be done before update menu
if (Config.appLanguage) {
i18nManager.setLocale(Config.appLanguage);
} else if (!i18nManager.setLocale(app.getLocale())) {
i18nManager.setLocale(app.getLocaleCountryCode());
}
handleUpdateMenuEvent();
ipcMain.emit('update-dict');

View File

@@ -49,6 +49,9 @@ jest.mock('main/autoUpdater', () => ({}));
jest.mock('main/constants', () => ({
updatePaths: jest.fn(),
}));
jest.mock('main/i18nManager', () => ({
localizeMessage: jest.fn(),
}));
jest.mock('main/menus/app', () => ({}));
jest.mock('main/menus/tray', () => ({}));
jest.mock('main/server/serverInfo', () => ({

View File

@@ -21,6 +21,7 @@ import Utils from 'common/utils/util';
import updateManager from 'main/autoUpdater';
import {migrationInfoPath, updatePaths} from 'main/constants';
import {localizeMessage} from 'main/i18nManager';
import {createMenu as createAppMenu} from 'main/menus/app';
import {createMenu as createTrayMenu} from 'main/menus/tray';
import {ServerInfo} from 'main/server/serverInfo';
@@ -224,11 +225,14 @@ export function migrateMacAppStore() {
}
const cancelImport = dialog.showMessageBoxSync({
title: 'Mattermost',
message: 'Import Existing Configuration',
detail: 'It appears that an existing Mattermost configuration exists, would you like to import it? You will be asked to pick the correct configuration directory.',
title: app.name,
message: localizeMessage('main.app.utils.migrateMacAppStore.dialog.message', 'Import Existing Configuration'),
detail: localizeMessage('main.app.utils.migrateMacAppStore.dialog.detail', 'It appears that an existing {appName} configuration exists, would you like to import it? You will be asked to pick the correct configuration directory.', {appName: app.name}),
icon: appIcon,
buttons: ['Select Directory and Import', 'Don\'t Import'],
buttons: [
localizeMessage('main.app.utils.migrateMacAppStore.button.selectAndImport', 'Select Directory and Import'),
localizeMessage('main.app.utils.migrateMacAppStore.button.dontImport', 'Don\'t Import'),
],
type: 'info',
defaultId: 0,
cancelId: 1,

View File

@@ -45,6 +45,10 @@ jest.mock('main/windows/windowManager', () => ({
sendToRenderer: jest.fn(),
}));
jest.mock('main/i18nManager', () => ({
localizeMessage: jest.fn(),
}));
describe('main/autoUpdater', () => {
describe('constructor', () => {
afterEach(() => {

View File

@@ -8,6 +8,7 @@ import log from 'electron-log';
import {autoUpdater, ProgressInfo, UpdateInfo} from 'electron-updater';
import {localizeMessage} from 'main/i18nManager';
import {displayUpgrade, displayRestartToUpgrade} from 'main/notifications';
import {CANCEL_UPGRADE, UPDATE_AVAILABLE, UPDATE_DOWNLOADED, CHECK_FOR_UPDATES, UPDATE_SHORTCUT_MENU, UPDATE_PROGRESS} from 'common/communication';
@@ -108,11 +109,14 @@ export class UpdateManager {
clearTimeout(this.lastCheck);
}
dialog.showMessageBox({
title: 'Mattermost',
message: 'New desktop version available',
detail: 'A new version of the Mattermost Desktop app is available for you to download and install now.',
title: app.name,
message: localizeMessage('main.autoUpdater.download.dialog.message', 'New desktop version available'),
detail: localizeMessage('main.autoUpdater.download.dialog.detail', 'A new version of the {appName} Desktop App is available for you to download and install now.', {appName: app.name}),
icon: appIcon,
buttons: ['Download', 'Remind me Later'],
buttons: [
localizeMessage('main.autoUpdater.download.dialog.button.download', 'Download'),
localizeMessage('main.autoUpdater.download.dialog.button.remindMeLater', 'Remind me Later'),
],
type: 'info',
defaultId: 0,
cancelId: 1,
@@ -131,11 +135,14 @@ export class UpdateManager {
handleUpdate = (): void => {
dialog.showMessageBox({
title: 'Mattermost',
message: 'A new version is ready to install',
detail: 'A new version of the Mattermost Desktop app is ready to install.',
title: app.name,
message: localizeMessage('main.autoUpdater.update.dialog.message', 'A new version is ready to install'),
detail: localizeMessage('main.autoUpdater.update.dialog.detail', 'A new version of the {appName} Desktop App is ready to install.', {appName: app.name}),
icon: appIcon,
buttons: ['Restart and Update', 'Remind me Later'],
buttons: [
localizeMessage('main.autoUpdater.update.dialog.button.restartAndUpdate', 'Restart and Update'),
localizeMessage('main.autoUpdater.update.dialog.button.remindMeLater', 'Remind me Later'),
],
type: 'info',
defaultId: 0,
cancelId: 1,
@@ -149,12 +156,12 @@ export class UpdateManager {
displayNoUpgrade = (): void => {
const version = app.getVersion();
dialog.showMessageBox({
title: 'Mattermost',
title: app.name,
icon: appIcon,
message: 'You\'re up to date',
message: localizeMessage('main.autoUpdater.noUpdate.message', 'You\'re up to date'),
type: 'info',
buttons: ['OK'],
detail: `You are using the latest version of the Mattermost Desktop App (version ${version}). You'll be notified when a new version is available to install.`,
buttons: [localizeMessage('label.ok', 'OK')],
detail: localizeMessage('main.autoUpdater.noUpdate.detail', 'You are using the latest version of the {appName} Desktop App (version {version}). You\'ll be notified when a new version is available to install.', {appName: app.name, version}),
});
}

View File

@@ -24,6 +24,10 @@ jest.mock('./windows/windowManager', () => ({
setOverlayIcon: jest.fn(),
}));
jest.mock('main/i18nManager', () => ({
localizeMessage: jest.fn().mockReturnValue(''),
}));
describe('main/badge', () => {
describe('showBadgeWindows', () => {
it('should show dot when session expired', () => {

View File

@@ -7,6 +7,8 @@ import log from 'electron-log';
import {UPDATE_BADGE} from 'common/communication';
import {localizeMessage} from 'main/i18nManager';
import WindowManager from './windows/windowManager';
import * as AppState from './appState';
@@ -15,17 +17,17 @@ const MAX_WIN_COUNT = 99;
let showUnreadBadgeSetting: boolean;
export function showBadgeWindows(sessionExpired: boolean, mentionCount: number, showUnreadBadge: boolean) {
let description = 'You have no unread messages';
let description = localizeMessage('main.badge.noUnreads', 'You have no unread messages');
let text;
if (mentionCount > 0) {
text = (mentionCount > MAX_WIN_COUNT) ? `${MAX_WIN_COUNT}+` : mentionCount.toString();
description = `You have unread mentions (${mentionCount})`;
description = localizeMessage('main.badge.unreadMentions', 'You have unread mentions ({mentionCount})', {mentionCount});
} else if (showUnreadBadge && showUnreadBadgeSetting) {
text = '•';
description = 'You have unread channels';
description = localizeMessage('main.badge.unreadChannels', 'You have unread channels');
} else if (sessionExpired) {
text = '•';
description = 'Session Expired: Please sign in to continue receiving notifications.';
description = localizeMessage('main.badge.sessionExpired', 'Session Expired: Please sign in to continue receiving notifications.');
}
WindowManager.setOverlayIcon(text, description, mentionCount > 99);
}

View File

@@ -0,0 +1,72 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import i18nManager, {I18nManager, localizeMessage} from 'main/i18nManager';
jest.mock('electron', () => ({
ipcMain: {
handle: jest.fn(),
},
}));
jest.mock('electron-log', () => ({
debug: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
}));
describe('main/i18nManager', () => {
it('should default to English', () => {
const i18n = new I18nManager();
expect(i18n.currentLanguage.value).toBe('en');
});
it('should set locale only if available', () => {
const i18n = new I18nManager();
expect(i18n.setLocale('fr')).toBe(true);
expect(i18n.currentLanguage.value).toBe('fr');
expect(i18n.setLocale('zz')).toBe(false);
expect(i18n.currentLanguage.value).toBe('fr');
});
});
describe('main/i18nManager/localizeMessage', () => {
i18nManager.currentLanguage = {
url: {
simple_key: 'simple_translation',
simple_replace_key: 'simple_translation {key}',
replace_two_key: '{replace} {replace_again}',
nested_braces: '{{replace}}',
multiple_same: '{replace} {replace} {key}',
},
};
it('should get a simple translation', () => {
expect(localizeMessage('simple_key', 'different_translation')).toBe('simple_translation');
});
it('should default if does not exist', () => {
expect(localizeMessage('unsimple_key', 'different_translation')).toBe('different_translation');
});
it('should replace', () => {
expect(localizeMessage('simple_replace_key', null, {key: 'replacement'})).toBe('simple_translation replacement');
});
it('should not replace if key is missing', () => {
expect(localizeMessage('simple_replace_key', null, {})).toBe('simple_translation {key}');
});
it('should replace twice', () => {
expect(localizeMessage('replace_two_key', null, {replace: 'replacement1', replace_again: 'replacement2'})).toBe('replacement1 replacement2');
});
it('should ignore nested braces', () => {
expect(localizeMessage('nested_braces', null, {replace: 'replacement'})).toBe('{replacement}');
});
it('should replace multiple of the same', () => {
expect(localizeMessage('multiple_same', null, {replace: 'replacement', key: 'key1'})).toBe('replacement replacement key1');
});
});

60
src/main/i18nManager.ts Normal file
View File

@@ -0,0 +1,60 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {ipcMain} from 'electron';
import log from 'electron-log';
import {GET_AVAILABLE_LANGUAGES, GET_LANGUAGE_INFORMATION} from 'common/communication';
import {Language, languages} from '../../i18n/i18n';
export function localizeMessage(s: string, defaultString = '', values: any = {}) {
let str = i18nManager.currentLanguage.url[s] || defaultString;
for (const key of Object.keys(values)) {
str = str.replace(new RegExp(`{${key}}`, 'g'), values[key]);
}
return str;
}
export class I18nManager {
currentLanguage: Language;
constructor() {
this.currentLanguage = this.getLanguages().en;
ipcMain.handle(GET_LANGUAGE_INFORMATION, this.getCurrentLanguage);
ipcMain.handle(GET_AVAILABLE_LANGUAGES, this.getAvailableLanguages);
}
setLocale = (locale: string) => {
log.debug('i18nManager.setLocale', locale);
if (this.isLanguageAvailable(locale)) {
this.currentLanguage = this.getLanguages()[locale];
log.info('Set new language', locale);
return true;
}
log.warn('Failed to set new language', locale);
return false;
}
getLanguages = () => {
return languages;
}
getAvailableLanguages = () => {
return Object.keys(languages);
}
isLanguageAvailable = (locale: string) => {
return Boolean(this.getLanguages()[locale]);
}
getCurrentLanguage = () => {
return this.currentLanguage;
}
}
const i18nManager = new I18nManager();
export default i18nManager;

View File

@@ -3,6 +3,7 @@
'use strict';
import {localizeMessage} from 'main/i18nManager';
import WindowManager from 'main/windows/windowManager';
import {createTemplate} from './app';
@@ -14,6 +15,10 @@ jest.mock('electron', () => ({
},
}));
jest.mock('main/i18nManager', () => ({
localizeMessage: jest.fn(),
}));
jest.mock('main/windows/windowManager', () => ({
getCurrentTeamName: jest.fn(),
}));
@@ -97,6 +102,12 @@ describe('main/menus/app', () => {
});
it('should include About <appname> in menu on mac', () => {
localizeMessage.mockImplementation((id) => {
if (id === 'main.menus.app.file.about') {
return 'About AppName';
}
return id;
});
const menu = createTemplate(config);
const appNameMenu = menu.find((item) => item.label === '&AppName');
const menuItem = appNameMenu.submenu.find((item) => item.label === 'About AppName');
@@ -105,23 +116,45 @@ describe('main/menus/app', () => {
});
it('should contain hide options', () => {
localizeMessage.mockImplementation((id) => {
if (id === 'main.menus.app.file') {
return '&AppName';
}
return id;
});
const menu = createTemplate(config);
const appNameMenu = menu.find((item) => item.label === '&AppName');
expect(appNameMenu.submenu).toContainEqual({role: 'hide'});
expect(appNameMenu.submenu).toContainEqual({role: 'unhide'});
expect(appNameMenu.submenu).toContainEqual({role: 'hideOthers'});
expect(appNameMenu.submenu).toContainEqual(expect.objectContaining({role: 'hide'}));
expect(appNameMenu.submenu).toContainEqual(expect.objectContaining({role: 'unhide'}));
expect(appNameMenu.submenu).toContainEqual(expect.objectContaining({role: 'hideOthers'}));
});
it('should contain zoom and front options in Window', () => {
localizeMessage.mockImplementation((id) => {
if (id === 'main.menus.app.window') {
return '&Window';
}
return id;
});
const menu = createTemplate(config);
const windowMenu = menu.find((item) => item.label === '&Window');
expect(windowMenu.role).toBe('windowMenu');
expect(windowMenu.submenu).toContainEqual({role: 'zoom'});
expect(windowMenu.submenu).toContainEqual({role: 'front'});
expect(windowMenu.submenu).toContainEqual(expect.objectContaining({role: 'zoom'}));
expect(windowMenu.submenu).toContainEqual(expect.objectContaining({role: 'front'}));
});
});
it('should show `Sign in to Another Server` if `enableServerManagement` is true', () => {
localizeMessage.mockImplementation((id) => {
switch (id) {
case 'main.menus.app.file':
return '&File';
case 'main.menus.app.file.signInToAnotherServer':
return 'Sign in to Another Server';
default:
return id;
}
});
const menu = createTemplate(config);
const fileMenu = menu.find((item) => item.label === '&AppName' || item.label === '&File');
const signInOption = fileMenu.submenu.find((item) => item.label === 'Sign in to Another Server');
@@ -129,6 +162,16 @@ describe('main/menus/app', () => {
});
it('should not show `Sign in to Another Server` if `enableServerManagement` is false', () => {
localizeMessage.mockImplementation((id) => {
switch (id) {
case 'main.menus.app.file':
return '&File';
case 'main.menus.app.file.signInToAnotherServer':
return 'Sign in to Another Server';
default:
return '';
}
});
const modifiedConfig = {
...config,
enableServerManagement: false,
@@ -140,6 +183,12 @@ describe('main/menus/app', () => {
});
it('should show the first 9 servers (using order) in the Window menu', () => {
localizeMessage.mockImplementation((id) => {
if (id === 'main.menus.app.window') {
return '&Window';
}
return id;
});
const modifiedConfig = {
data: {
...config.data,
@@ -174,6 +223,15 @@ describe('main/menus/app', () => {
});
it('should show the first 9 tabs (using order) in the Window menu', () => {
localizeMessage.mockImplementation((id) => {
if (id === 'main.menus.app.window') {
return '&Window';
}
if (id.startsWith('common.tabs')) {
return id.replace('common.tabs.', '');
}
return id;
});
WindowManager.getCurrentTeamName.mockImplementation(() => config.data.teams[0].name);
const modifiedConfig = {

View File

@@ -6,9 +6,11 @@
import {app, ipcMain, Menu, MenuItemConstructorOptions, MenuItem, session, shell, WebContents, clipboard} from 'electron';
import {BROWSER_HISTORY_BUTTON, OPEN_TEAMS_DROPDOWN, SHOW_NEW_SERVER_MODAL} from 'common/communication';
import {t} from 'common/utils/util';
import {getTabDisplayName, TabType} from 'common/tabs/TabView';
import {Config} from 'common/config';
import {TabType, getTabDisplayName} from 'common/tabs/TabView';
import {localizeMessage} from 'main/i18nManager';
import WindowManager from 'main/windows/windowManager';
import {UpdateManager} from 'main/autoUpdater';
@@ -19,16 +21,16 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
const isMac = process.platform === 'darwin';
const appName = app.name;
const firstMenuName = isMac ? appName : 'File';
const firstMenuName = isMac ? '&' + appName : localizeMessage('main.menus.app.file', '&File');
const template = [];
const settingsLabel = isMac ? 'Preferences...' : 'Settings...';
const settingsLabel = isMac ? localizeMessage('main.menus.app.file.preferences', 'Preferences...') : localizeMessage('main.menus.app.file.settings', 'Settings...');
let platformAppMenu = [];
if (isMac) {
platformAppMenu.push(
{
label: 'About ' + appName,
label: localizeMessage('main.menus.app.file.about', 'About {appName}', {appName}),
role: 'about',
},
);
@@ -44,7 +46,7 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
if (config.data?.enableServerManagement === true) {
platformAppMenu.push({
label: 'Sign in to Another Server',
label: localizeMessage('main.menus.app.file.signInToAnotherServer', 'Sign in to Another Server'),
click() {
ipcMain.emit(SHOW_NEW_SERVER_MODAL);
},
@@ -55,67 +57,79 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
platformAppMenu = platformAppMenu.concat([
separatorItem, {
role: 'hide',
label: localizeMessage('main.menus.app.file.hide', 'Hide {appName}', {appName}),
}, {
role: 'hideOthers',
label: localizeMessage('main.menus.app.file.hideOthers', 'Hide Others'),
}, {
role: 'unhide',
label: localizeMessage('main.menus.app.file.unhide', 'Show All'),
}, separatorItem, {
role: 'quit',
label: localizeMessage('main.menus.app.file.quit', 'Quit {appName}', {appName}),
}]);
} else {
platformAppMenu = platformAppMenu.concat([
separatorItem, {
role: 'quit',
label: localizeMessage('main.menus.app.file.exit', 'Exit'),
accelerator: 'CmdOrCtrl+Q',
}]);
}
template.push({
label: '&' + firstMenuName,
label: firstMenuName,
submenu: [
...platformAppMenu,
],
});
template.push({
label: '&Edit',
label: localizeMessage('main.menus.app.edit', '&Edit'),
submenu: [{
role: 'undo',
label: localizeMessage('main.menus.app.edit.undo', 'Undo'),
accelerator: 'CmdOrCtrl+Z',
}, {
role: 'Redo',
label: localizeMessage('main.menus.app.edit.redo', 'Redo'),
accelerator: 'CmdOrCtrl+SHIFT+Z',
}, separatorItem, {
role: 'cut',
label: localizeMessage('main.menus.app.edit.cut', 'Cut'),
accelerator: 'CmdOrCtrl+X',
}, {
role: 'copy',
label: localizeMessage('main.menus.app.edit.copy', 'Copy'),
accelerator: 'CmdOrCtrl+C',
}, {
role: 'paste',
label: localizeMessage('main.menus.app.edit.paste', 'Paste'),
accelerator: 'CmdOrCtrl+V',
}, {
role: 'pasteAndMatchStyle',
label: localizeMessage('main.menus.app.edit.pasteAndMatchStyle', 'Paste and Match Style'),
accelerator: 'CmdOrCtrl+SHIFT+V',
}, {
role: 'selectall',
label: localizeMessage('main.menus.app.edit.selectAll', 'Select All'),
accelerator: 'CmdOrCtrl+A',
}],
});
const viewSubMenu = [{
label: 'Find..',
label: localizeMessage('main.menus.app.view.find', 'Find..'),
accelerator: 'CmdOrCtrl+F',
click() {
WindowManager.sendToFind();
},
}, {
label: 'Reload',
label: localizeMessage('main.menus.app.view.reload', 'Reload'),
accelerator: 'CmdOrCtrl+R',
click() {
WindowManager.reload();
},
}, {
label: 'Clear Cache and Reload',
label: localizeMessage('main.menus.app.view.clearCacheAndReload', 'Clear Cache and Reload'),
accelerator: 'Shift+CmdOrCtrl+R',
click() {
session.defaultSession.clearCache();
@@ -123,13 +137,15 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
},
}, {
role: 'togglefullscreen',
label: localizeMessage('main.menus.app.view.fullscreen', 'Toggle Full Screen'),
accelerator: isMac ? 'Ctrl+Cmd+F' : 'F11',
}, separatorItem, {
label: 'Actual Size',
label: localizeMessage('main.menus.app.view.actualSize', 'Actual Size'),
role: 'resetZoom',
accelerator: 'CmdOrCtrl+0',
}, {
role: 'zoomIn',
label: localizeMessage('main.menus.app.view.zoomIn', 'Zoom In'),
accelerator: 'CmdOrCtrl+=',
}, {
role: 'zoomIn',
@@ -137,13 +153,14 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
accelerator: 'CmdOrCtrl+Shift+=',
}, {
role: 'zoomOut',
label: localizeMessage('main.menus.app.view.zoomOut', 'Zoom Out'),
accelerator: 'CmdOrCtrl+-',
}, {
role: 'zoomOut',
visible: false,
accelerator: 'CmdOrCtrl+Shift+-',
}, separatorItem, {
label: 'Developer Tools for Application Wrapper',
label: localizeMessage('main.menus.app.view.devToolsAppWrapper', 'Developer Tools for Application Wrapper'),
accelerator: (() => {
if (process.platform === 'darwin') {
return 'Alt+Command+I';
@@ -161,7 +178,7 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
}
},
}, {
label: 'Developer Tools for Current Server',
label: localizeMessage('main.menus.app.view.devToolsCurrentServer', 'Developer Tools for Current Server'),
click() {
WindowManager.openBrowserViewDevTools();
},
@@ -170,7 +187,7 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
if (process.platform !== 'darwin' && process.platform !== 'win32') {
viewSubMenu.push(separatorItem);
viewSubMenu.push({
label: 'Toggle Dark Mode',
label: localizeMessage('main.menus.app.view.toggleDarkMode', 'Toggle Dark Mode'),
click() {
config.toggleDarkModeManually();
},
@@ -178,13 +195,13 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
}
template.push({
label: '&View',
label: localizeMessage('main.menus.app.view', '&View'),
submenu: viewSubMenu,
});
template.push({
label: '&History',
label: localizeMessage('main.menus.app.history', '&History'),
submenu: [{
label: 'Back',
label: localizeMessage('main.menus.app.history.back', 'Back'),
accelerator: process.platform === 'darwin' ? 'Cmd+[' : 'Alt+Left',
click: () => {
const view = WindowManager.viewManager?.getCurrentView();
@@ -194,7 +211,7 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
}
},
}, {
label: 'Forward',
label: localizeMessage('main.menus.app.history.forward', 'Forward'),
accelerator: process.platform === 'darwin' ? 'Cmd+]' : 'Alt+Right',
click: () => {
const view = WindowManager.viewManager?.getCurrentView();
@@ -208,21 +225,24 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
const teams = config.data?.teams || [];
const windowMenu = {
label: '&Window',
label: localizeMessage('main.menus.app.window', '&Window'),
role: isMac ? 'windowMenu' : null,
submenu: [{
role: 'minimize',
label: localizeMessage('main.menus.app.window.minimize', 'Minimize'),
// empty string removes shortcut on Windows; null will default by OS
accelerator: process.platform === 'win32' ? '' : null,
}, ...(isMac ? [{
role: 'zoom',
label: localizeMessage('main.menus.app.window.zoom', 'Zoom'),
}, separatorItem,
] : []), {
role: 'close',
label: isMac ? localizeMessage('main.menus.app.window.closeWindow', 'Close Window') : localizeMessage('main.menus.app.window.close', 'Close'),
accelerator: 'CmdOrCtrl+W',
}, separatorItem, {
label: 'Show Servers',
label: localizeMessage('main.menus.app.window.showServers', 'Show Servers'),
accelerator: `${process.platform === 'darwin' ? 'Cmd+Ctrl' : 'Ctrl+Shift'}+S`,
click() {
ipcMain.emit(OPEN_TEAMS_DROPDOWN);
@@ -239,7 +259,7 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
if (WindowManager.getCurrentTeamName() === team.name) {
team.tabs.filter((tab) => tab.isOpen).sort((teamA, teamB) => teamA.order - teamB.order).slice(0, 9).forEach((tab, i) => {
items.push({
label: ` ${getTabDisplayName(tab.name as TabType)}`,
label: ` ${localizeMessage(`common.tabs.${tab.name}`, getTabDisplayName(tab.name as TabType))}`,
accelerator: `CmdOrCtrl+${i + 1}`,
click() {
WindowManager.switchTab(team.name, tab.name);
@@ -249,14 +269,14 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
}
return items;
}).flat(), separatorItem, {
label: 'Select Next Tab',
label: localizeMessage('main.menus.app.window.selectNextTab', 'Select Next Tab'),
accelerator: 'Ctrl+Tab',
click() {
WindowManager.selectNextTab();
},
enabled: (teams.length > 1),
}, {
label: 'Select Previous Tab',
label: localizeMessage('main.menus.app.window.selectPreviousTab', 'Select Previous Tab'),
accelerator: 'Ctrl+Shift+Tab',
click() {
WindowManager.selectPreviousTab();
@@ -264,6 +284,7 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
enabled: (teams.length > 1),
}, ...(isMac ? [separatorItem, {
role: 'front',
label: localizeMessage('main.menus.app.window.bringAllToFront', 'Bring All to Front'),
}] : []),
],
};
@@ -272,21 +293,21 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
if (updateManager && config.canUpgrade) {
if (updateManager.versionDownloaded) {
submenu.push({
label: 'Restart and Update',
label: localizeMessage('main.menus.app.help.restartAndUpdate', 'Restart and Update'),
click() {
updateManager.handleUpdate();
},
});
} else if (updateManager.versionAvailable) {
submenu.push({
label: 'Download Update',
label: localizeMessage('main.menus.app.help.downloadUpdate', 'Download Update'),
click() {
updateManager.handleDownload();
},
});
} else {
submenu.push({
label: 'Check for Updates',
label: localizeMessage('main.menus.app.help.checkForUpdates', 'Check for Updates'),
click() {
updateManager.checkForUpdates(true);
},
@@ -295,7 +316,7 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
}
if (config.data?.helpLink) {
submenu.push({
label: 'Learn More...',
label: localizeMessage('main.menus.app.help.learnMore', 'Learn More...'),
click() {
shell.openExternal(config.data!.helpLink);
},
@@ -303,10 +324,13 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
submenu.push(separatorItem);
}
// eslint-disable-next-line no-undef
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const version = `Version ${app.getVersion()}${__HASH_VERSION__ ? ` commit: ${__HASH_VERSION__}` : ''}`;
const version = localizeMessage('main.menus.app.help.versionString', 'Version {version}{commit}', {
version: app.getVersion(),
// eslint-disable-next-line no-undef
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
commit: __HASH_VERSION__ ? localizeMessage('main.menus.app.help.commitString', ' commit: {hashVersion}', {hashVersion: __HASH_VERSION__}) : '',
});
submenu.push({
label: version,
enabled: true,
@@ -315,7 +339,7 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
},
});
template.push({label: 'Hel&p', submenu});
template.push({label: localizeMessage('main.menus.app.help', 'Hel&p'), submenu});
return template;
}
@@ -323,3 +347,7 @@ export function createMenu(config: Config, updateManager: UpdateManager) {
// TODO: Electron is enforcing certain variables that it doesn't need
return Menu.buildFromTemplate(createTemplate(config, updateManager) as Array<MenuItemConstructorOptions | MenuItem>);
}
t('common.tabs.TAB_MESSAGING');
t('common.tabs.TAB_FOCALBOARD');
t('common.tabs.TAB_PLAYBOOKS');

View File

@@ -5,6 +5,10 @@
import {createTemplate} from './tray';
jest.mock('main/i18nManager', () => ({
localizeMessage: jest.fn(),
}));
jest.mock('main/windows/windowManager', () => ({}));
describe('main/menus/tray', () => {

View File

@@ -7,6 +7,7 @@ import {Menu, MenuItem, MenuItemConstructorOptions} from 'electron';
import {CombinedConfig} from 'types/config';
import WindowManager from 'main/windows/windowManager';
import {localizeMessage} from 'main/i18nManager';
export function createTemplate(config: CombinedConfig) {
const teams = config.teams;
@@ -21,7 +22,7 @@ export function createTemplate(config: CombinedConfig) {
}), {
type: 'separator',
}, {
label: process.platform === 'darwin' ? 'Preferences...' : 'Settings',
label: process.platform === 'darwin' ? localizeMessage('main.menus.tray.preferences', 'Preferences...') : localizeMessage('main.menus.tray.settings', 'Settings'),
click: () => {
WindowManager.showSettingsWindow();
},

View File

@@ -8,11 +8,13 @@ import {app, Notification} from 'electron';
import Utils from 'common/utils/util';
import {localizeMessage} from 'main/i18nManager';
const assetsDir = path.resolve(app.getAppPath(), 'assets');
const appIconURL = path.resolve(assetsDir, 'appicon_48.png');
const defaultOptions = {
title: 'Download Complete',
title: localizeMessage('main.notifications.download.complete.title', 'Download Complete'),
silent: false,
icon: appIconURL,
urgency: 'normal' as Notification['urgency'],
@@ -27,8 +29,8 @@ export class DownloadNotification extends Notification {
Reflect.deleteProperty(options, 'icon');
}
options.title = process.platform === 'win32' ? serverName : 'Download Complete';
options.body = process.platform === 'win32' ? `Download Complete \n ${fileName}` : fileName;
options.title = process.platform === 'win32' ? serverName : localizeMessage('main.notifications.download.complete.title', 'Download Complete');
options.body = process.platform === 'win32' ? localizeMessage('main.notifications.download.complete.body', 'Download Complete \n {fileName}', {fileName}) : fileName;
super(options);
}

View File

@@ -10,11 +10,13 @@ import {MentionOptions} from 'types/notification';
import Utils from 'common/utils/util';
import {localizeMessage} from 'main/i18nManager';
const assetsDir = path.resolve(app.getAppPath(), 'assets');
const appIconURL = path.resolve(assetsDir, 'appicon_48.png');
const defaultOptions = {
title: 'Someone mentioned you',
title: localizeMessage('main.notifications.mention.title', 'Someone mentioned you'),
silent: false,
icon: appIconURL,
urgency: 'normal' as Notification['urgency'],

View File

@@ -5,12 +5,14 @@ import path from 'path';
import {app, Notification} from 'electron';
import {localizeMessage} from 'main/i18nManager';
const assetsDir = path.resolve(app.getAppPath(), 'assets');
const appIconURL = path.resolve(assetsDir, 'appicon_48.png');
const defaultOptions = {
title: 'New desktop version available',
body: 'A new version is available for you to download now.',
title: localizeMessage('main.notifications.upgrade.newVersion.title', 'New desktop version available'),
body: localizeMessage('main.notifications.upgrade.newVersion.body', 'A new version is available for you to download now.'),
silent: false,
icon: appIconURL,
urgency: 'normal' as Notification['urgency'],
@@ -33,8 +35,8 @@ export class NewVersionNotification extends Notification {
export class UpgradeNotification extends Notification {
constructor() {
const options = {...defaultOptions};
options.title = 'Click to restart and install update';
options.body = 'A new desktop version is ready to install now.';
options.title = localizeMessage('main.notifications.upgrade.readyToInstall.title', 'Click to restart and install update');
options.body = localizeMessage('main.notifications.upgrade.readyToInstall.body', 'A new desktop version is ready to install now.');
if (process.platform === 'win32') {
options.icon = appIconURL;
} else if (process.platform === 'darwin') {

View File

@@ -8,6 +8,8 @@ import {Notification, shell} from 'electron';
import {PLAY_SOUND} from 'common/communication';
import {TAB_MESSAGING} from 'common/tabs/TabView';
import {localizeMessage} from 'main/i18nManager';
import WindowManager from '../windows/windowManager';
import {displayMention, displayDownloadCompleted, currentNotifications} from './index';
@@ -58,6 +60,10 @@ jest.mock('../windows/windowManager', () => ({
switchTab: jest.fn(),
}));
jest.mock('main/i18nManager', () => ({
localizeMessage: jest.fn(),
}));
describe('main/notifications', () => {
describe('displayMention', () => {
beforeEach(() => {
@@ -152,6 +158,7 @@ describe('main/notifications', () => {
describe('displayDownloadCompleted', () => {
it('should open file when clicked', () => {
localizeMessage.mockReturnValue('test_filename');
displayDownloadCompleted(
'test_filename',
'/path/to/file',

View File

@@ -16,6 +16,8 @@ import {
SHOW_EDIT_SERVER_MODAL,
SHOW_REMOVE_SERVER_MODAL,
UPDATE_TEAMS,
GET_LANGUAGE_INFORMATION,
RETRIEVED_LANGUAGE_INFORMATION,
} from 'common/communication';
console.log('preloaded for the dropdown!');
@@ -50,6 +52,9 @@ window.addEventListener('message', async (event) => {
case UPDATE_TEAMS:
ipcRenderer.invoke(UPDATE_TEAMS, event.data.data);
break;
case GET_LANGUAGE_INFORMATION:
window.postMessage({type: RETRIEVED_LANGUAGE_INFORMATION, data: await ipcRenderer.invoke(GET_LANGUAGE_INFORMATION)});
break;
default:
console.log(`got a message: ${event}`);
console.log(event);

View File

@@ -6,6 +6,11 @@
import {ipcRenderer, contextBridge} from 'electron';
import {
GET_LANGUAGE_INFORMATION,
RETRIEVED_LANGUAGE_INFORMATION,
} from 'common/communication';
contextBridge.exposeInMainWorld('ipcRenderer', {
send: ipcRenderer.send,
on: (channel, listener) => ipcRenderer.on(channel, (_, ...args) => listener(null, ...args)),
@@ -24,3 +29,11 @@ contextBridge.exposeInMainWorld('timers', {
setImmediate,
});
window.addEventListener('message', async (event) => {
switch (event.data.type) {
case GET_LANGUAGE_INFORMATION:
window.postMessage({type: RETRIEVED_LANGUAGE_INFORMATION, data: await ipcRenderer.invoke(GET_LANGUAGE_INFORMATION)});
break;
}
});

View File

@@ -18,6 +18,8 @@ import {
MODAL_UNCLOSEABLE,
PING_DOMAIN,
PING_DOMAIN_RESPONSE,
GET_LANGUAGE_INFORMATION,
RETRIEVED_LANGUAGE_INFORMATION,
} from 'common/communication';
console.log('preloaded for the modal!');
@@ -70,6 +72,9 @@ window.addEventListener('message', async (event) => {
window.postMessage({type: PING_DOMAIN_RESPONSE, data: error}, window.location.href);
}
break;
case GET_LANGUAGE_INFORMATION:
window.postMessage({type: RETRIEVED_LANGUAGE_INFORMATION, data: await ipcRenderer.invoke(GET_LANGUAGE_INFORMATION)});
break;
default:
console.log(`got a message: ${event}`);
console.log(event);

View File

@@ -7,6 +7,8 @@ import {app, nativeImage, Tray, systemPreferences, nativeTheme} from 'electron';
import {UPDATE_TRAY} from 'common/communication';
import {localizeMessage} from 'main/i18nManager';
import WindowManager from '../windows/windowManager';
import * as AppState from '../appState';
@@ -95,11 +97,11 @@ export function setupTray(icontheme: string) {
AppState.on(UPDATE_TRAY, (anyExpired, anyMentions, anyUnreads) => {
if (anyMentions) {
setTray('mention', 'You have been mentioned');
setTray('mention', localizeMessage('main.tray.tray.mention', 'You have been mentioned'));
} else if (anyUnreads) {
setTray('unread', 'You have unread channels');
setTray('unread', localizeMessage('main.tray.tray.unread', 'You have unread channels'));
} else if (anyExpired) {
setTray('mention', 'Session Expired: Please sign in to continue receiving notifications.');
setTray('mention', localizeMessage('main.tray.tray.expired', 'Session Expired: Please sign in to continue receiving notifications.'));
} else {
setTray('normal', app.name);
}

View File

@@ -48,6 +48,10 @@ jest.mock('common/utils/url', () => ({
getView: jest.fn(),
}));
jest.mock('main/i18nManager', () => ({
localizeMessage: jest.fn(),
}));
jest.mock('main/server/serverInfo', () => ({
ServerInfo: jest.fn(),
}));

View File

@@ -28,6 +28,7 @@ import Utils from 'common/utils/util';
import {MattermostServer} from 'common/servers/MattermostServer';
import {getServerView, getTabViewName, TabTuple, TabType} from 'common/tabs/TabView';
import {localizeMessage} from 'main/i18nManager';
import {ServerInfo} from 'main/server/serverInfo';
import {getLocalURLString, getLocalPreload, getWindowBoundaries} from '../utils';
@@ -531,7 +532,10 @@ export class ViewManager {
}
}
} else {
dialog.showErrorBox('No matching server', `there is no configured server in the app that matches the requested url: ${parsedURL.toString()}`);
dialog.showErrorBox(
localizeMessage('main.views.viewManager.handleDeepLink.error.title', 'No matching server'),
localizeMessage('main.views.viewManager.handleDeepLink.error.body', 'There is no configured server in the app that matches the requested url: {url}', {url: parsedURL.toString()}),
);
}
}
};

View File

@@ -69,6 +69,10 @@ jest.mock('../utils', () => ({
getLocalURLString: jest.fn(),
}));
jest.mock('main/i18nManager', () => ({
localizeMessage: jest.fn(),
}));
'use strict';
describe('main/windows/mainWindow', () => {

View File

@@ -16,6 +16,7 @@ import {DEFAULT_WINDOW_HEIGHT, DEFAULT_WINDOW_WIDTH, MINIMUM_WINDOW_HEIGHT, MINI
import Utils from 'common/utils/util';
import {boundsInfoPath} from 'main/constants';
import {localizeMessage} from 'main/i18nManager';
import * as Validator from '../Validator';
import ContextMenu from '../contextMenu';
@@ -156,11 +157,11 @@ function createMainWindow(options: {linuxAppIcon: string; fullscreen?: boolean})
hideWindow(mainWindow);
} else {
dialog.showMessageBox(mainWindow, {
title: 'Minimize to Tray',
message: 'Mattermost will continue to run in the system tray. This can be disabled in Settings.',
title: localizeMessage('main.windows.mainWindow.minimizeToTray.dialog.title', 'Minimize to Tray'),
message: localizeMessage('main.windows.mainWindow.minimizeToTray.dialog.message', '{appName} will continue to run in the system tray. This can be disabled in Settings.', {appName: app.name}),
type: 'info',
checkboxChecked: true,
checkboxLabel: 'Don\'t show again',
checkboxLabel: localizeMessage('main.windows.mainWindow.minimizeToTray.dialog.checkboxLabel', 'Don\'t show again'),
}).then((result: {response: number; checkboxChecked: boolean}) => {
Config.set('alwaysMinimize', result.checkboxChecked);
hideWindow(mainWindow);
@@ -170,13 +171,16 @@ function createMainWindow(options: {linuxAppIcon: string; fullscreen?: boolean})
app.quit();
} else {
dialog.showMessageBox(mainWindow, {
title: 'Close Application',
message: 'Are you sure you want to quit?',
detail: 'You will no longer receive notifications for messages. If you want to leave Mattermost running in the system tray, you can enable this in Settings.',
title: localizeMessage('main.windows.mainWindow.closeApp.dialog.title', 'Close Application'),
message: localizeMessage('main.windows.mainWindow.closeApp.dialog.message', 'Are you sure you want to quit?'),
detail: localizeMessage('main.windows.mainWindow.closeApp.dialog.detail', 'You will no longer receive notifications for messages. If you want to leave {appName} running in the system tray, you can enable this in Settings.', {appName: app.name}),
type: 'question',
buttons: ['Yes', 'No'],
buttons: [
localizeMessage('label.yes', 'Yes'),
localizeMessage('label.no', 'No'),
],
checkboxChecked: true,
checkboxLabel: 'Don\'t ask again',
checkboxLabel: localizeMessage('main.windows.mainWindow.closeApp.dialog.checkboxLabel', 'Don\'t ask again'),
}).then((result: {response: number; checkboxChecked: boolean}) => {
Config.set('alwaysClose', result.checkboxChecked && result.response === 0);
if (result.response === 0) {