[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:
@@ -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();
|
||||
|
@@ -22,7 +22,7 @@ export class AutoLauncher {
|
||||
return;
|
||||
}
|
||||
const appLauncher = new AutoLaunch({
|
||||
name: 'Mattermost',
|
||||
name: app.name,
|
||||
});
|
||||
const enabled = await appLauncher.isEnabled();
|
||||
if (enabled) {
|
||||
|
@@ -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(() => {
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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').
|
||||
|
@@ -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'};
|
||||
|
@@ -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', () => {
|
||||
|
@@ -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,
|
||||
|
@@ -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(),
|
||||
|
@@ -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",
|
||||
|
@@ -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',
|
||||
|
@@ -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');
|
||||
|
@@ -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', () => ({
|
||||
|
@@ -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,
|
||||
|
@@ -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(() => {
|
||||
|
@@ -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}),
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -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', () => {
|
||||
|
@@ -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);
|
||||
}
|
||||
|
72
src/main/i18nManager.test.js
Normal file
72
src/main/i18nManager.test.js
Normal 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
60
src/main/i18nManager.ts
Normal 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;
|
@@ -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 = {
|
||||
|
@@ -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');
|
||||
|
@@ -5,6 +5,10 @@
|
||||
|
||||
import {createTemplate} from './tray';
|
||||
|
||||
jest.mock('main/i18nManager', () => ({
|
||||
localizeMessage: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('main/windows/windowManager', () => ({}));
|
||||
|
||||
describe('main/menus/tray', () => {
|
||||
|
@@ -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();
|
||||
},
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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'],
|
||||
|
@@ -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') {
|
||||
|
@@ -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',
|
||||
|
@@ -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);
|
||||
|
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -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);
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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(),
|
||||
}));
|
||||
|
@@ -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()}),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@@ -69,6 +69,10 @@ jest.mock('../utils', () => ({
|
||||
getLocalURLString: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('main/i18nManager', () => ({
|
||||
localizeMessage: jest.fn(),
|
||||
}));
|
||||
|
||||
'use strict';
|
||||
|
||||
describe('main/windows/mainWindow', () => {
|
||||
|
@@ -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) {
|
||||
|
Reference in New Issue
Block a user