[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
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
92 changed files with 3554 additions and 2375 deletions

View file

@ -12,19 +12,19 @@ executors:
check-image:
working_directory: ~/mattermost-desktop
docker:
- image: electronuserland/builder:wine-chrome
- image: electronuserland/builder:16-wine-chrome
environment:
TAR_OPTIONS: --no-same-owner
wine-chrome:
working_directory: ~/mattermost-desktop
docker:
- image: electronuserland/builder:wine-chrome
- image: electronuserland/builder:16-wine-chrome
environment:
TAR_OPTIONS: --no-same-owner
wine-mono:
working_directory: ~/mattermost-desktop
docker:
- image: electronuserland/builder:wine-mono
- image: electronuserland/builder:16-wine-mono
mac:
working_directory: ~/mattermost-desktop
macos:
@ -140,6 +140,13 @@ jobs:
- update_image:
apt_opts: "--no-install-recommends"
- run: npm run check-types
- run:
name: i18n check
command: |
cp i18n/en.json /tmp/en.json
npm run mmjstool -- i18n extract-desktop --desktop-dir .
diff /tmp/en.json i18n/en.json
rm -rf tmp
- run: ELECTRON_DISABLE_SANDBOX=1 npm run test:unit-ci
- run: mkdir -p /tmp/test-results
- run: cp test-results.xml /tmp/test-results/

View file

@ -95,6 +95,7 @@ const demoConfig = {
darkMode: false,
lastActiveTeam: 0,
spellCheckerLocales: [],
appLanguage: 'en',
};
const demoMattermostConfig = {

1
i18n/bg.json Normal file
View file

@ -0,0 +1 @@
{}

1
i18n/de.json Normal file
View file

@ -0,0 +1 @@
{}

234
i18n/en.json Normal file
View file

@ -0,0 +1,234 @@
{
"common.permissions.canBasicAuth": "Web Authentication",
"common.tabs.TAB_FOCALBOARD": "Boards",
"common.tabs.TAB_MESSAGING": "Channels",
"common.tabs.TAB_PLAYBOOKS": "Playbooks",
"label.accept": "Accept",
"label.add": "Add",
"label.cancel": "Cancel",
"label.change": "Change",
"label.close": "Close",
"label.login": "Login",
"label.no": "No",
"label.ok": "OK",
"label.remove": "Remove",
"label.save": "Save",
"label.yes": "Yes",
"main.allowProtocolDialog.button.saveProtocolAsAllowed": "Yes (Save {protocol} as allowed)",
"main.allowProtocolDialog.detail": "The requested link is {URL}. Do you want to continue?",
"main.allowProtocolDialog.message": "{protocol} link requires an external application.",
"main.allowProtocolDialog.title": "Non http(s) protocol",
"main.app.app.handleAppCertificateError.certError.button.cancelConnection": "Cancel Connection",
"main.app.app.handleAppCertificateError.certError.button.moreDetails": "More Details",
"main.app.app.handleAppCertificateError.certError.dialog.detail": "{extraDetail}origin: {origin}\nError: {error}",
"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.",
"main.app.app.handleAppCertificateError.certError.dialog.title": "Certificate Error",
"main.app.app.handleAppCertificateError.certNotTrusted.button.cancelConnection": "Cancel Connection",
"main.app.app.handleAppCertificateError.certNotTrusted.button.trustInsecureCertificate": "Trust Insecure Certificate",
"main.app.app.handleAppCertificateError.certNotTrusted.dialog.message": "Certificate from \"{issuerName}\" is not trusted.",
"main.app.app.handleAppCertificateError.certNotTrusted.dialog.title": "Certificate Not Trusted",
"main.app.app.handleAppCertificateError.dialog.extraDetail": "Certificate is different from previous one.\n\n",
"main.app.initialize.downloadBox.allFiles": "All files",
"main.app.utils.migrateMacAppStore.button.dontImport": "Don't Import",
"main.app.utils.migrateMacAppStore.button.selectAndImport": "Select Directory and Import",
"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.",
"main.app.utils.migrateMacAppStore.dialog.message": "Import Existing Configuration",
"main.autoUpdater.download.dialog.button.download": "Download",
"main.autoUpdater.download.dialog.button.remindMeLater": "Remind me Later",
"main.autoUpdater.download.dialog.detail": "A new version of the {appName} Desktop App is available for you to download and install now.",
"main.autoUpdater.download.dialog.message": "New desktop version available",
"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.",
"main.autoUpdater.noUpdate.message": "You're up to date",
"main.autoUpdater.update.dialog.button.remindMeLater": "Remind me Later",
"main.autoUpdater.update.dialog.button.restartAndUpdate": "Restart and Update",
"main.autoUpdater.update.dialog.detail": "A new version of the {appName} Desktop App is ready to install.",
"main.autoUpdater.update.dialog.message": "A new version is ready to install",
"main.badge.noUnreads": "You have no unread messages",
"main.badge.sessionExpired": "Session Expired: Please sign in to continue receiving notifications.",
"main.badge.unreadChannels": "You have unread channels",
"main.badge.unreadMentions": "You have unread mentions ({mentionCount})",
"main.CriticalErrorHandler.uncaughtException.button.reopen": "Reopen",
"main.CriticalErrorHandler.uncaughtException.button.showDetails": "Show Details",
"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}",
"main.CriticalErrorHandler.unresponsive.dialog.message": "The window is no longer responsive.\nDo you wait until the window becomes responsive again?",
"main.menus.app.edit": "&Edit",
"main.menus.app.edit.copy": "Copy",
"main.menus.app.edit.cut": "Cut",
"main.menus.app.edit.paste": "Paste",
"main.menus.app.edit.pasteAndMatchStyle": "Paste and Match Style",
"main.menus.app.edit.redo": "Redo",
"main.menus.app.edit.selectAll": "Select All",
"main.menus.app.edit.undo": "Undo",
"main.menus.app.file": "&File",
"main.menus.app.file.about": "About {appName}",
"main.menus.app.file.exit": "Exit",
"main.menus.app.file.hide": "Hide {appName}",
"main.menus.app.file.hideOthers": "Hide Others",
"main.menus.app.file.preferences": "Preferences...",
"main.menus.app.file.quit": "Quit {appName}",
"main.menus.app.file.settings": "Settings...",
"main.menus.app.file.signInToAnotherServer": "Sign in to Another Server",
"main.menus.app.file.unhide": "Show All",
"main.menus.app.help": "Hel&p",
"main.menus.app.help.checkForUpdates": "Check for Updates",
"main.menus.app.help.commitString": " commit: {hashVersion}",
"main.menus.app.help.downloadUpdate": "Download Update",
"main.menus.app.help.learnMore": "Learn More...",
"main.menus.app.help.restartAndUpdate": "Restart and Update",
"main.menus.app.help.versionString": "Version {version}{commit}",
"main.menus.app.history": "&History",
"main.menus.app.history.back": "Back",
"main.menus.app.history.forward": "Forward",
"main.menus.app.view": "&View",
"main.menus.app.view.actualSize": "Actual Size",
"main.menus.app.view.clearCacheAndReload": "Clear Cache and Reload",
"main.menus.app.view.devToolsAppWrapper": "Developer Tools for Application Wrapper",
"main.menus.app.view.devToolsCurrentServer": "Developer Tools for Current Server",
"main.menus.app.view.find": "Find..",
"main.menus.app.view.fullscreen": "Toggle Full Screen",
"main.menus.app.view.reload": "Reload",
"main.menus.app.view.toggleDarkMode": "Toggle Dark Mode",
"main.menus.app.view.zoomIn": "Zoom In",
"main.menus.app.view.zoomOut": "Zoom Out",
"main.menus.app.window": "&Window",
"main.menus.app.window.bringAllToFront": "Bring All to Front",
"main.menus.app.window.close": "Close",
"main.menus.app.window.closeWindow": "Close Window",
"main.menus.app.window.minimize": "Minimize",
"main.menus.app.window.selectNextTab": "Select Next Tab",
"main.menus.app.window.selectPreviousTab": "Select Previous Tab",
"main.menus.app.window.showServers": "Show Servers",
"main.menus.app.window.zoom": "Zoom",
"main.menus.tray.preferences": "Preferences...",
"main.menus.tray.settings": "Settings...",
"main.notifications.download.complete.body": "Download Complete \n {fileName}",
"main.notifications.download.complete.title": "Download Complete",
"main.notifications.mention.title": "Someone mentioned you",
"main.notifications.upgrade.newVersion.body": "A new version is available for you to download now.",
"main.notifications.upgrade.newVersion.title": "New desktop version available",
"main.notifications.upgrade.readyToInstall.body": "A new desktop version is ready to install now.",
"main.notifications.upgrade.readyToInstall.title": "Click to restart and install update",
"main.tray.tray.expired": "Session Expired: Please sign in to continue receiving notifications.",
"main.tray.tray.mention": "You have been mentioned",
"main.tray.tray.unread": "You have unread channels",
"main.views.viewManager.handleDeepLink.error.body": "There is no configured server in the app that matches the requested url: {url}",
"main.views.viewManager.handleDeepLink.error.title": "No matching server",
"main.windows.mainWindow.closeApp.dialog.checkboxLabel": "Don't ask again",
"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.",
"main.windows.mainWindow.closeApp.dialog.message": "Are you sure you want to quit?",
"main.windows.mainWindow.closeApp.dialog.title": "Close Application",
"main.windows.mainWindow.minimizeToTray.dialog.checkboxLabel": "Don't show again",
"main.windows.mainWindow.minimizeToTray.dialog.message": "{appName} will continue to run in the system tray. This can be disabled in Settings.",
"main.windows.mainWindow.minimizeToTray.dialog.title": "Minimize to Tray",
"renderer.components.autoSaveIndicator.saved": "Saved",
"renderer.components.autoSaveIndicator.saving": "Saving...",
"renderer.components.errorView.cannotConnectToAppName": "Cannot connect to {appName}",
"renderer.components.errorView.havingTroubleConnecting": "We're having trouble connecting to {appName}. We'll continue to try and establish a connection.",
"renderer.components.errorView.refreshThenVerify": "If refreshing this page (Ctrl+R or Command+R) does not work please verify that:",
"renderer.components.errorView.troubleshooting.browserView.canReachFromBrowserWindow": "You can reach <link>{url}</link> from a browser window.",
"renderer.components.errorView.troubleshooting.computerIsConnected": "Your computer is connected to the internet.",
"renderer.components.errorView.troubleshooting.urlIsCorrect.appNameIsCorrect": "The {appName} URL <link>{url}</link> is correct",
"renderer.components.extraBar.back": "Back",
"renderer.components.mainPage.contextMenu.ariaLabel": "Context menu",
"renderer.components.mainPage.downloadingUpdate": "Downloading update. {percentDone}% of {total} @ {speed}/s",
"renderer.components.mainPage.updateAvailable": "Update available",
"renderer.components.mainPage.updateReady": "Update ready to install",
"renderer.components.newTeamModal.error.nameRequired": "Name is required.",
"renderer.components.newTeamModal.error.serverNameExists": "A server with the same name already exists.",
"renderer.components.newTeamModal.error.serverUrlExists": "A server with the same URL already exists.",
"renderer.components.newTeamModal.error.urlIncorrectFormatting": "URL is not formatted correctly.",
"renderer.components.newTeamModal.error.urlNeedsHttp": "URL should start with http:// or https://.",
"renderer.components.newTeamModal.error.urlRequired": "URL is required.",
"renderer.components.newTeamModal.serverDisplayName": "Server Display Name",
"renderer.components.newTeamModal.serverDisplayName.description": "The name of the server displayed on your desktop app tab bar.",
"renderer.components.newTeamModal.serverURL": "Server URL",
"renderer.components.newTeamModal.serverURL.description": "The URL of your Mattermost server. Must start with http:// or https://.",
"renderer.components.newTeamModal.title.add": "Add Server",
"renderer.components.newTeamModal.title.edit": "Edit Server",
"renderer.components.removeServerModal.body": "This will remove the server from your Desktop App but will not delete any of its data - you can add the server back to the app at any time.",
"renderer.components.removeServerModal.confirm": "Confirm you wish to remove the {serverName} server?",
"renderer.components.removeServerModal.title": "Remove Server",
"renderer.components.settingsPage.afterRestart": "Setting takes effect after restarting the app.",
"renderer.components.settingsPage.appLanguage": "Set app language (beta)",
"renderer.components.settingsPage.appLanguage.description": "Chooses the language that the Desktop App will use for menu items and popups. Still in beta, some languages will be missing translation strings.",
"renderer.components.settingsPage.appLanguage.useSystemDefault": "Use system default",
"renderer.components.settingsPage.appOptions": "App Options",
"renderer.components.settingsPage.bounceIcon": "Bounce the Dock icon",
"renderer.components.settingsPage.bounceIcon.description": "If enabled, the Dock icon bounces once or until the user opens the app when a new notification is received.",
"renderer.components.settingsPage.bounceIcon.once": "once",
"renderer.components.settingsPage.bounceIcon.untilOpenApp": "until I open the app",
"renderer.components.settingsPage.checkSpelling": "Check spelling",
"renderer.components.settingsPage.checkSpelling.description": "Highlight misspelled words in your messages based on your system language or language preference.",
"renderer.components.settingsPage.checkSpelling.editSpellcheckUrl": "Use an alternative dictionary URL",
"renderer.components.settingsPage.checkSpelling.preferredLanguages": "Select preferred language(s)",
"renderer.components.settingsPage.checkSpelling.revertToDefault": "Revert to default",
"renderer.components.settingsPage.checkSpelling.specifyURL": "Specify the url where dictionary definitions can be retrieved",
"renderer.components.settingsPage.downloadLocation": "Download Location",
"renderer.components.settingsPage.downloadLocation.description": "Specify the folder where files will download.",
"renderer.components.settingsPage.enableHardwareAcceleration": "Use GPU hardware acceleration",
"renderer.components.settingsPage.enableHardwareAcceleration.description": "If enabled, Mattermost UI is rendered more efficiently but can lead to decreased stability for some systems.",
"renderer.components.settingsPage.flashWindow": "Flash taskbar icon when a new message is received",
"renderer.components.settingsPage.flashWindow.description": "If enabled, the taskbar icon will flash for a few seconds when a new message is received.",
"renderer.components.settingsPage.flashWindow.description.linuxFunctionality": "This functionality may not work with all Linux window managers.",
"renderer.components.settingsPage.flashWindow.description.note": "NOTE: ",
"renderer.components.settingsPage.fullscreen": "Open app in fullscreen",
"renderer.components.settingsPage.fullscreen.description": "If enabled, the Mattermost application will always open in full screen",
"renderer.components.settingsPage.header": "Settings",
"renderer.components.settingsPage.launchAppMinimized": "Launch app minimized",
"renderer.components.settingsPage.launchAppMinimized.description": "If enabled, the app will start in system tray, and will not show the window on launch.",
"renderer.components.settingsPage.loadingConfig": "Loading configuration...",
"renderer.components.settingsPage.loggingLevel": "Logging level",
"renderer.components.settingsPage.loggingLevel.description": "Logging is helpful for developers and support to isolate issues you may be encountering with the desktop app.",
"renderer.components.settingsPage.loggingLevel.description.subtitle": "Increasing the log level increases disk space usage and can impact performance. We recommend only increasing the log level if you are having issues.",
"renderer.components.settingsPage.loggingLevel.level.debug": "Debug (debug)",
"renderer.components.settingsPage.loggingLevel.level.error": "Errors (error)",
"renderer.components.settingsPage.loggingLevel.level.info": "Info (info)",
"renderer.components.settingsPage.loggingLevel.level.silly": "Finest (silly)",
"renderer.components.settingsPage.loggingLevel.level.verbose": "Verbose (verbose)",
"renderer.components.settingsPage.loggingLevel.level.warn": "Errors and Warnings (warn)",
"renderer.components.settingsPage.minimizeToTray": "Leave app running in notification area when application window is closed",
"renderer.components.settingsPage.minimizeToTray.description": "If enabled, the app stays running in the notification area after app window is closed.",
"renderer.components.settingsPage.saving.error": "Can't save your changes. Please try again.",
"renderer.components.settingsPage.showUnreadBadge": "Show red badge on {taskbar} icon to indicate unread messages",
"renderer.components.settingsPage.showUnreadBadge.description": "Regardless of this setting, mentions are always indicated with a red badge and item count on the {taskbar} icon.",
"renderer.components.settingsPage.startAppOnLogin": "Start app on login",
"renderer.components.settingsPage.startAppOnLogin.description": "If enabled, the app starts automatically when you log in to your machine.",
"renderer.components.settingsPage.trayIcon.show": "Show icon in the notification area",
"renderer.components.settingsPage.trayIcon.show.darwin": "Show {appName} icon in the menu bar",
"renderer.components.settingsPage.trayIcon.theme": "Icon theme: ",
"renderer.components.settingsPage.trayIcon.theme.dark": "Dark",
"renderer.components.settingsPage.trayIcon.theme.light": "Light",
"renderer.components.settingsPage.trayIcon.theme.systemDefault": "Use system default",
"renderer.components.settingsPage.updates": "Updates",
"renderer.components.settingsPage.updates.automatic": "Automatically check for updates",
"renderer.components.settingsPage.updates.automatic.description": "If enabled, updates to the Desktop App will download automatically and you will be notified when ready to install.",
"renderer.components.settingsPage.updates.checkNow": "Check for Updates Now",
"renderer.components.showCertificateModal.algorithm": "Algorithm",
"renderer.components.showCertificateModal.commonName": "Common Name",
"renderer.components.showCertificateModal.issuerName": "Issuer Name",
"renderer.components.showCertificateModal.noCertSelected": "No certificate Selected",
"renderer.components.showCertificateModal.notValidAfter": "Not Valid After",
"renderer.components.showCertificateModal.notValidBefore": "Not Valid Before",
"renderer.components.showCertificateModal.publicKeyInfo": "Public Key Info",
"renderer.components.showCertificateModal.serialNumber": "Serial Number",
"renderer.components.showCertificateModal.subjectName": "Subject Name",
"renderer.components.teamDropdownButton.noServersConfigured": "No servers configured",
"renderer.dropdown.addAServer": "Add a server",
"renderer.dropdown.servers": "Servers",
"renderer.modals.certificate.certificateModal.certInfoButton": "Certificate Information",
"renderer.modals.certificate.certificateModal.issuer": "Issuer",
"renderer.modals.certificate.certificateModal.noCertsAvailable": "No certificates available",
"renderer.modals.certificate.certificateModal.serial": "Serial",
"renderer.modals.certificate.certificateModal.subject": "Subject",
"renderer.modals.certificate.certificateModal.subtitle": "Select a certificate to authenticate yourself to {url}",
"renderer.modals.certificate.certificateModal.title": "Select a certificate",
"renderer.modals.login.loginModal.message.proxy": "The proxy {host}:{port} requires a username and password.",
"renderer.modals.login.loginModal.message.server": "The server {url} requires a username and password.",
"renderer.modals.login.loginModal.password": "Password",
"renderer.modals.login.loginModal.title": "Authentication Required",
"renderer.modals.login.loginModal.username": "User Name",
"renderer.modals.permission.permissionModal.body": "A site that's not included in your Mattermost server configuration requires access for {permission}.",
"renderer.modals.permission.permissionModal.requestOriginatedFrom": "This request originated from ",
"renderer.modals.permission.permissionModal.title": "{permission} Required",
"renderer.modals.permission.permissionModal.unknownOrigin": "unknown origin"
}

1
i18n/en_AU.json Normal file
View file

@ -0,0 +1 @@
{}

1
i18n/es.json Normal file
View file

@ -0,0 +1 @@
{}

1
i18n/fa.json Normal file
View file

@ -0,0 +1 @@
{}

1
i18n/fr.json Normal file
View file

@ -0,0 +1 @@
{}

1
i18n/hu.json Normal file
View file

@ -0,0 +1 @@
{}

161
i18n/i18n.ts Normal file
View file

@ -0,0 +1,161 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable import/order */
import bg from './bg.json';
import de from './de.json';
import en from './en.json';
import enAU from './en_AU.json';
import es from './es.json';
import fa from './fa.json';
import fr from './fr.json';
import hu from './hu.json';
import it from './it.json';
import ja from './ja.json';
import ko from './ko.json';
import nl from './nl.json';
import pl from './pl.json';
import ptBR from './pt-BR.json';
import ro from './ro.json';
import ru from './ru.json';
import sv from './sv.json';
import tr from './tr.json';
import uk from './uk.json';
import zhTW from './zh-TW.json';
import zhCN from './zh-CN.json';
export type Language = {
value: string;
name: string;
order: number;
url: Record<string, string>;
};
export const languages: Record<string, Language> = {
de: {
value: 'de',
name: 'Deutsch',
order: 0,
url: de,
},
en: {
value: 'en',
name: 'English (US)',
order: 1,
url: en,
},
'en-AU': {
value: 'en-AU',
name: 'English (Australia)',
order: 2,
url: enAU,
},
es: {
value: 'es',
name: 'Español',
order: 3,
url: es,
},
fr: {
value: 'fr',
name: 'Français',
order: 4,
url: fr,
},
it: {
value: 'it',
name: 'Italiano (Alpha)',
order: 5,
url: it,
},
hu: {
value: 'hu',
name: 'Magyar',
order: 6,
url: hu,
},
nl: {
value: 'nl',
name: 'Nederlands',
order: 7,
url: nl,
},
pl: {
value: 'pl',
name: 'Polski',
order: 8,
url: pl,
},
'pt-BR': {
value: 'pt-BR',
name: 'Português (Brasil) (Beta)',
order: 9,
url: ptBR,
},
ro: {
value: 'ro',
name: 'Română (Beta)',
order: 10,
url: ro,
},
sv: {
value: 'sv',
name: 'Svenska',
order: 11,
url: sv,
},
tr: {
value: 'tr',
name: 'Türkçe',
order: 12,
url: tr,
},
bg: {
value: 'bg',
name: 'Български',
order: 13,
url: bg,
},
ru: {
value: 'ru',
name: 'Pусский',
order: 14,
url: ru,
},
uk: {
value: 'uk',
name: 'Yкраїнська (Alpha)',
order: 15,
url: uk,
},
fa: {
value: 'fa',
name: 'فارسی (Beta)',
order: 16,
url: fa,
},
ko: {
value: 'ko',
name: '한국어 (Alpha)',
order: 17,
url: ko,
},
'zh-CN': {
value: 'zh-CN',
name: '中文 (简体)',
order: 18,
url: zhCN,
},
'zh-TW': {
value: 'zh-TW',
name: '中文 (繁體)',
order: 19,
url: zhTW,
},
ja: {
value: 'ja',
name: '日本語',
order: 20,
url: ja,
},
};

1
i18n/it.json Normal file
View file

@ -0,0 +1 @@
{}

1
i18n/ja.json Normal file
View file

@ -0,0 +1 @@
{}

1
i18n/ko.json Normal file
View file

@ -0,0 +1 @@
{}

1
i18n/nl.json Normal file
View file

@ -0,0 +1 @@
{}

1
i18n/pl.json Normal file
View file

@ -0,0 +1 @@
{}

1
i18n/pt-BR.json Normal file
View file

@ -0,0 +1 @@
{}

1
i18n/ro.json Normal file
View file

@ -0,0 +1 @@
{}

1
i18n/ru.json Normal file
View file

@ -0,0 +1 @@
{}

1
i18n/sv.json Normal file
View file

@ -0,0 +1 @@
{}

1
i18n/tr.json Normal file
View file

@ -0,0 +1 @@
{}

1
i18n/uk.json Normal file
View file

@ -0,0 +1 @@
{}

1
i18n/zh-CN.json Normal file
View file

@ -0,0 +1 @@
{}

1
i18n/zh-TW.json Normal file
View file

@ -0,0 +1 @@
{}

2897
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -65,7 +65,9 @@
"check-build-config:build": "babel ./src/common/config/buildConfig.ts -o ./dist/buildConfig.js",
"check-build-config:run": "node -r @babel/register scripts/check_build_config.js",
"check-types": "tsc",
"prune": "ts-prune"
"prune": "ts-prune",
"mmjstool": "mmjstool",
"i18n-extract": "npm run mmjstool -- i18n extract-desktop"
},
"jest": {
"clearMocks": true,
@ -160,6 +162,7 @@
"jest": "27.5.1",
"jest-junit": "13.1.0",
"mini-css-extract-plugin": "2.6.0",
"mmjstool": "github:mattermost/mattermost-utilities#d849d3819112bd828f08caf0155bd7ed62f18950",
"mocha-circleci-reporter": "0.0.3",
"node-gyp": "9.0.0",
"npm-run-all": "4.1.5",
@ -171,7 +174,7 @@
"ts-prune": "0.10.3",
"typescript": "4.6.3",
"webpack": "5.71.0",
"webpack-cli": "4.9.2",
"webpack-cli": "4.10.0",
"webpack-dev-server": "4.8.0",
"webpack-merge": "5.8.0"
},
@ -193,6 +196,7 @@
"react-beautiful-dnd": "13.1.0",
"react-bootstrap": "1.6.4",
"react-dom": "16.14.0",
"react-intl": "5.20.10",
"react-select": "5.2.2",
"sass": "1.49.11",
"semver": "7.3.5",

View file

@ -122,3 +122,7 @@ export const RELOAD_CURRENT_VIEW = 'reload-current-view';
export const PING_DOMAIN = 'ping-domain';
export const PING_DOMAIN_RESPONSE = 'ping-domain-response';
export const GET_LANGUAGE_INFORMATION = 'get-language-information';
export const RETRIEVED_LANGUAGE_INFORMATION = 'retrieved-language-information';
export const GET_AVAILABLE_LANGUAGES = 'get-available-languages';

View file

@ -328,6 +328,10 @@ export class Config extends EventEmitter {
return this.combinedData?.autoCheckForUpdates;
}
get appLanguage() {
return this.combinedData?.appLanguage;
}
// initialization/processing methods
/**

View file

@ -6,5 +6,5 @@ export const BASIC_AUTH_PERMISSION = 'canBasicAuth';
// Permission descriptions
export const PERMISSION_DESCRIPTION = {
[BASIC_AUTH_PERMISSION]: 'Web Authentication',
[BASIC_AUTH_PERMISSION]: 'common.permissions.canBasicAuth',
};

View file

@ -59,10 +59,6 @@ export function getServerView(srv: MattermostServer, tab: Tab) {
}
}
export function getTabViewName(serverName: string, tabType: string) {
return `${serverName}___${tabType}`;
}
export function getTabDisplayName(tabType: TabType) {
switch (tabType) {
case TAB_MESSAGING:
@ -76,6 +72,10 @@ export function getTabDisplayName(tabType: TabType) {
}
}
export function getTabViewName(serverName: string, tabType: string) {
return `${serverName}___${tabType}`;
}
export function canCloseTab(tabType: TabType) {
return tabType !== TAB_MESSAGING;
}

View file

@ -60,6 +60,10 @@ function isVersionGreaterThanOrEqualTo(currentVersion: string, compareVersion: s
return true;
}
export function t(s: string) {
return s;
}
export default {
getDisplayBoundaries,
runMode,

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);
}
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
const version = `Version ${app.getVersion()}${__HASH_VERSION__ ? ` commit: ${__HASH_VERSION__}` : ''}`;
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) {

View file

@ -4,6 +4,7 @@
import React from 'react';
import {Alert} from 'react-bootstrap';
import {IntlShape, useIntl} from 'react-intl';
const baseClassName = 'AutoSaveIndicator';
const leaveClassName = `${baseClassName}-Leave`;
@ -15,16 +16,16 @@ export enum SavingState {
SAVING_STATE_DONE = 'done',
}
function getClassNameAndMessage(savingState: SavingState, errorMessage?: string) {
function getClassNameAndMessage(intl: IntlShape, savingState: SavingState, errorMessage?: React.ReactNode) {
switch (savingState) {
case SavingState.SAVING_STATE_SAVING:
return {className: baseClassName, message: 'Saving...'};
return {className: baseClassName, message: intl.formatMessage({id: 'renderer.components.autoSaveIndicator.saving', defaultMessage: 'Saving...'})};
case SavingState.SAVING_STATE_SAVED:
return {className: baseClassName, message: 'Saved'};
return {className: baseClassName, message: intl.formatMessage({id: 'renderer.components.autoSaveIndicator.saved', defaultMessage: 'Saved'})};
case SavingState.SAVING_STATE_ERROR:
return {className: `${baseClassName}`, message: errorMessage};
case SavingState.SAVING_STATE_DONE:
return {className: `${baseClassName} ${leaveClassName}`, message: 'Saved'};
return {className: `${baseClassName} ${leaveClassName}`, message: intl.formatMessage({id: 'renderer.components.autoSaveIndicator.saved', defaultMessage: 'Saved'})};
default:
return {className: `${baseClassName} ${leaveClassName}`, message: ''};
}
@ -33,12 +34,13 @@ function getClassNameAndMessage(savingState: SavingState, errorMessage?: string)
type Props = {
id?: string;
savingState: SavingState;
errorMessage?: string;
errorMessage?: React.ReactNode;
};
export default function AutoSaveIndicator(props: Props) {
const AutoSaveIndicator: React.FC<Props> = (props: Props) => {
const intl = useIntl();
const {savingState, errorMessage, ...rest} = props;
const {className, message} = getClassNameAndMessage(savingState, errorMessage);
const {className, message} = getClassNameAndMessage(intl, savingState, errorMessage);
return (
<Alert
className={className}
@ -48,4 +50,6 @@ export default function AutoSaveIndicator(props: Props) {
{message}
</Alert>
);
}
};
export default AutoSaveIndicator;

View file

@ -6,6 +6,7 @@
import React from 'react';
import {Container, Row, Col} from 'react-bootstrap';
import {FormattedMessage} from 'react-intl';
type Props = {
errorInfo?: string;
@ -41,31 +42,74 @@ export default function ErrorView(props: Props) {
md={10}
lg={8}
>
<h2>{`Cannot connect to ${props.appName}`}</h2>
<h2>
<FormattedMessage
id='renderer.components.errorView.cannotConnectToAppName'
defaultMessage='Cannot connect to {appName}'
values={{
appName: props.appName,
}}
/>
</h2>
<hr/>
<p>
{`We're having trouble connecting to ${props.appName}. We'll continue to try and establish a connection.`}
<FormattedMessage
id='renderer.components.errorView.havingTroubleConnecting'
defaultMessage={'We\'re having trouble connecting to {appName}. We\'ll continue to try and establish a connection.'}
values={{
appName: props.appName,
}}
/>
<br/>
{'If refreshing this page (Ctrl+R or Command+R) does not work please verify that:'}
<FormattedMessage
id='renderer.components.errorView.refreshThenVerify'
defaultMessage='If refreshing this page (Ctrl+R or Command+R) does not work please verify that:'
/>
</p>
<ul className='ErrorView-bullets' >
<li>{'Your computer is connected to the internet.'}</li>
<li>{`The ${props.appName} URL `}
<li>
<FormattedMessage
id='renderer.components.errorView.troubleshooting.computerIsConnected'
defaultMessage='Your computer is connected to the internet.'
/>
</li>
<li>
<FormattedMessage
id='renderer.components.errorView.troubleshooting.urlIsCorrect.appNameIsCorrect'
defaultMessage='The {appName} URL <link>{url}</link> is correct'
values={{
appName: props.appName,
url: props.url,
link: (msg: React.ReactNode) => (
<a
onClick={props.handleLink}
href='#'
>
{props.url}
</a>{' is correct.'}</li>
<li>{'You can reach '}
{msg}
</a>
),
}}
/>
</li>
<li>
<FormattedMessage
id='renderer.components.errorView.troubleshooting.browserView.canReachFromBrowserWindow'
defaultMessage='You can reach <link>{url}</link> from a browser window.'
values={{
url: props.url,
link: (msg: React.ReactNode) => (
<a
onClick={props.handleLink}
href='#'
>
{props.url}
</a>{' from a browser window.'}</li>
{msg}
</a>
),
}}
/>
</li>
</ul>
<br/>
<div className='ErrorView-techInfo'>

View file

@ -3,6 +3,7 @@
import React from 'react';
import {Row, Button} from 'react-bootstrap';
import {FormattedMessage} from 'react-intl';
type Props = {
darkMode?: boolean;
@ -39,7 +40,10 @@ export default class ExtraBar extends React.PureComponent<Props> {
>
<span className={'backIcon icon-arrow-left'}/>
<span className={'backLabel'}>
{'Back'}
<FormattedMessage
id='renderer.components.extraBar.back'
defaultMessage='Back'
/>
</span>
</Button>
</div>

View file

@ -6,6 +6,7 @@ import classNames from 'classnames';
import React, {Fragment} from 'react';
import {Container, Row} from 'react-bootstrap';
import {DropResult} from 'react-beautiful-dnd';
import {injectIntl, IntlShape} from 'react-intl';
import {IpcRendererEvent} from 'electron/renderer';
import prettyBytes from 'pretty-bytes';
@ -82,6 +83,7 @@ type Props = {
darkMode: boolean;
appName: string;
useNativeWindow: boolean;
intl: IntlShape;
};
type State = {
@ -115,7 +117,7 @@ type TabViewStatus = {
};
}
export default class MainPage extends React.PureComponent<Props, State> {
class MainPage extends React.PureComponent<Props, State> {
topBar: React.RefObject<HTMLDivElement>;
threeDotMenu: React.RefObject<HTMLButtonElement>;
@ -363,6 +365,7 @@ export default class MainPage extends React.PureComponent<Props, State> {
}
render() {
const {intl} = this.props;
const currentTabs = this.props.teams.find((team) => team.name === this.state.activeServerName)?.tabs || [];
const tabsRow = (
@ -419,13 +422,20 @@ export default class MainPage extends React.PureComponent<Props, State> {
let upgradeTooltip;
switch (this.state.upgradeStatus) {
case UpgradeStatus.AVAILABLE:
upgradeTooltip = 'Update available';
upgradeTooltip = intl.formatMessage({id: 'renderer.components.mainPage.updateAvailable', defaultMessage: 'Update available'});
break;
case UpgradeStatus.DOWNLOADED:
upgradeTooltip = 'Update ready to install';
upgradeTooltip = intl.formatMessage({id: 'renderer.components.mainPage.updateReady', defaultMessage: 'Update ready to install'});
break;
case UpgradeStatus.DOWNLOADING:
upgradeTooltip = `Downloading update. ${String(this.state.upgradeProgress?.percent).split('.')[0]}% of ${prettyBytes(this.state.upgradeProgress?.total || 0)} @ ${prettyBytes(this.state.upgradeProgress?.bytesPerSecond || 0)}/s`;
upgradeTooltip = intl.formatMessage({
id: 'renderer.components.mainPage.downloadingUpdate',
defaultMessage: 'Downloading update. {percentDone}% of {total} @ {speed}/s',
}, {
percentDone: String(this.state.upgradeProgress?.percent).split('.')[0],
total: prettyBytes(this.state.upgradeProgress?.total || 0),
speed: prettyBytes(this.state.upgradeProgress?.bytesPerSecond || 0),
});
break;
}
@ -516,7 +526,7 @@ export default class MainPage extends React.PureComponent<Props, State> {
onClick={this.openMenu}
tabIndex={0}
ref={this.threeDotMenu}
aria-label='Context menu'
aria-label={intl.formatMessage({id: 'renderer.components.mainPage.contextMenu.ariaLabel', defaultMessage: 'Context menu'})}
>
<i className='icon-dots-vertical'/>
</button>
@ -593,3 +603,5 @@ export default class MainPage extends React.PureComponent<Props, State> {
);
}
}
export default injectIntl(MainPage);

View file

@ -4,6 +4,7 @@
import React from 'react';
import {Modal, Button, FormGroup, FormControl, FormLabel, FormText} from 'react-bootstrap';
import {FormattedMessage, injectIntl, IntlShape} from 'react-intl';
import {TeamWithIndex} from 'types/config';
@ -20,6 +21,7 @@ type Props = {
restoreFocus?: boolean;
currentOrder?: number;
setInputRef?: (inputRef: HTMLInputElement) => void;
intl: IntlShape;
};
type State = {
@ -30,7 +32,7 @@ type State = {
saveStarted: boolean;
}
export default class NewTeamModal extends React.PureComponent<Props, State> {
class NewTeamModal extends React.PureComponent<Props, State> {
wasShown?: boolean;
teamNameInputRef?: HTMLInputElement;
@ -70,10 +72,20 @@ export default class NewTeamModal extends React.PureComponent<Props, State> {
currentTeams.splice(this.props.team.index, 1);
}
if (currentTeams.find((team) => team.name === this.state.teamName)) {
return 'A server with the same name already exists.';
return (
<FormattedMessage
id='renderer.components.newTeamModal.error.serverNameExists'
defaultMessage='A server with the same name already exists.'
/>
);
}
}
return this.state.teamName.length > 0 ? null : 'Name is required.';
return this.state.teamName.length > 0 ? null : (
<FormattedMessage
id='renderer.components.newTeamModal.error.nameRequired'
defaultMessage='Name is required.'
/>
);
}
getTeamNameValidationState() {
@ -96,17 +108,37 @@ export default class NewTeamModal extends React.PureComponent<Props, State> {
currentTeams.splice(this.props.team.index, 1);
}
if (currentTeams.find((team) => team.url === this.state.teamUrl)) {
return 'A server with the same URL already exists.';
return (
<FormattedMessage
id='renderer.components.newTeamModal.error.serverUrlExists'
defaultMessage='A server with the same URL already exists.'
/>
);
}
}
if (this.state.teamUrl.length === 0) {
return 'URL is required.';
return (
<FormattedMessage
id='renderer.components.newTeamModal.error.urlRequired'
defaultMessage='URL is required.'
/>
);
}
if (!(/^https?:\/\/.*/).test(this.state.teamUrl.trim())) {
return 'URL should start with http:// or https://.';
return (
<FormattedMessage
id='renderer.components.newTeamModal.error.urlNeedsHttp'
defaultMessage='URL should start with http:// or https://.'
/>
);
}
if (!urlUtils.isValidURL(this.state.teamUrl.trim())) {
return 'URL is not formatted correctly.';
return (
<FormattedMessage
id='renderer.components.newTeamModal.error.urlIncorrectFormatting'
defaultMessage='URL is not formatted correctly.'
/>
);
}
return null;
}
@ -185,16 +217,36 @@ export default class NewTeamModal extends React.PureComponent<Props, State> {
getSaveButtonLabel() {
if (this.props.editMode) {
return 'Save';
return (
<FormattedMessage
id='label.save'
defaultMessage='Save'
/>
);
}
return 'Add';
return (
<FormattedMessage
id='label.add'
defaultMessage='Add'
/>
);
}
getModalTitle() {
if (this.props.editMode) {
return 'Edit Server';
return (
<FormattedMessage
id='renderer.components.newTeamModal.title.edit'
defaultMessage='Edit Server'
/>
);
}
return 'Add Server';
return (
<FormattedMessage
id='renderer.components.newTeamModal.title.add'
defaultMessage='Add Server'
/>
);
}
render() {
@ -235,12 +287,17 @@ export default class NewTeamModal extends React.PureComponent<Props, State> {
<Modal.Body>
<form>
<FormGroup>
<FormLabel>{'Server Display Name'}</FormLabel>
<FormLabel>
<FormattedMessage
id='renderer.components.newTeamModal.serverDisplayName'
defaultMessage='Server Display Name'
/>
</FormLabel>
<FormControl
id='teamNameInput'
type='text'
value={this.state.teamName}
placeholder='Server Name'
placeholder={this.props.intl.formatMessage({id: 'renderer.components.newTeamModal.serverDisplayName', defaultMessage: 'Server Display Name'})}
onChange={this.handleTeamNameChange}
ref={(ref: HTMLInputElement) => {
this.teamNameInputRef = ref;
@ -255,12 +312,22 @@ export default class NewTeamModal extends React.PureComponent<Props, State> {
isInvalid={Boolean(this.getTeamNameValidationState())}
/>
<FormControl.Feedback/>
<FormText>{'The name of the server displayed on your desktop app tab bar.'}</FormText>
<FormText>
<FormattedMessage
id='renderer.components.newTeamModal.serverDisplayName.description'
defaultMessage='The name of the server displayed on your desktop app tab bar.'
/>
</FormText>
</FormGroup>
<FormGroup
className='NewTeamModal-noBottomSpace'
>
<FormLabel>{'Server URL'}</FormLabel>
<FormLabel>
<FormattedMessage
id='renderer.components.newTeamModal.serverURL'
defaultMessage='Server URL'
/>
</FormLabel>
<FormControl
id='teamUrlInput'
type='text'
@ -273,7 +340,12 @@ export default class NewTeamModal extends React.PureComponent<Props, State> {
isInvalid={Boolean(this.getTeamUrlValidationState())}
/>
<FormControl.Feedback/>
<FormText className='NewTeamModal-noBottomSpace'>{'The URL of your Mattermost server. Must start with http:// or https://.'}</FormText>
<FormText className='NewTeamModal-noBottomSpace'>
<FormattedMessage
id='renderer.components.newTeamModal.serverURL.description'
defaultMessage='The URL of your Mattermost server. Must start with http:// or https://.'
/>
</FormText>
</FormGroup>
</form>
</Modal.Body>
@ -291,7 +363,10 @@ export default class NewTeamModal extends React.PureComponent<Props, State> {
onClick={this.props.onClose}
variant='link'
>
{'Cancel'}
<FormattedMessage
id='label.cancel'
defaultMessage='Cancel'
/>
</Button>
}
{this.props.onSave &&
@ -310,3 +385,5 @@ export default class NewTeamModal extends React.PureComponent<Props, State> {
);
}
}
export default injectIntl(NewTeamModal);

View file

@ -4,6 +4,7 @@
import React from 'react';
import {Modal} from 'react-bootstrap';
import {FormattedMessage, useIntl} from 'react-intl';
import DestructiveConfirmationModal from './DestructiveConfirmModal';
@ -13,27 +14,36 @@ type Props = {
onHide: () => void;
onAccept: React.MouseEventHandler<HTMLButtonElement>;
onCancel: React.MouseEventHandler<HTMLButtonElement>;
}
};
export default function RemoveServerModal(props: Props) {
function RemoveServerModal(props: Props) {
const intl = useIntl();
const {serverName, ...rest} = props;
return (
<DestructiveConfirmationModal
{...rest}
title='Remove Server'
acceptLabel='Remove'
cancelLabel='Cancel'
title={intl.formatMessage({id: 'renderer.components.removeServerModal.title', defaultMessage: 'Remove Server'})}
acceptLabel={intl.formatMessage({id: 'label.remove', defaultMessage: 'Remove'})}
cancelLabel={intl.formatMessage({id: 'label.cancel', defaultMessage: 'Cancel'})}
body={(
<Modal.Body>
<p>
{'This will remove the server from your Desktop App but will not delete any of its data' +
' - you can add the server back to the app at any time.'}
<FormattedMessage
id='renderer.components.removeServerModal.body'
defaultMessage='This will remove the server from your Desktop App but will not delete any of its data - you can add the server back to the app at any time.'
/>
</p>
<p>
{'Confirm you wish to remove the '}<strong>{serverName}</strong>{' server?'}
<FormattedMessage
id='renderer.components.removeServerModal.confirm'
defaultMessage='Confirm you wish to remove the {serverName} server?'
values={{serverName}}
/>
</p>
</Modal.Body>
)}
/>
);
}
export default RemoveServerModal;

View file

@ -8,6 +8,7 @@ import 'renderer/css/settings.css';
import React from 'react';
import {FormCheck, Col, FormGroup, FormText, Container, Row, Button, FormControl} from 'react-bootstrap';
import {FormattedMessage, injectIntl, IntlShape} from 'react-intl';
import ReactSelect, {ActionMeta, MultiValue} from 'react-select';
import {CombinedConfig, LocalConfiguration} from 'types/config';
@ -23,6 +24,7 @@ import {
RELOAD_CONFIGURATION,
GET_AVAILABLE_SPELL_CHECKER_LANGUAGES,
CHECK_FOR_UPDATES,
GET_AVAILABLE_LANGUAGES,
} from 'common/communication';
import AutoSaveIndicator, {SavingState} from './AutoSaveIndicator';
@ -32,6 +34,10 @@ const CONFIG_TYPE_APP_OPTIONS = 'appOptions';
type ConfigType = typeof CONFIG_TYPE_UPDATES | typeof CONFIG_TYPE_APP_OPTIONS;
type Props = {
intl: IntlShape;
}
type State = DeepPartial<CombinedConfig> & {
ready: boolean;
maximized?: boolean;
@ -39,13 +45,14 @@ type State = DeepPartial<CombinedConfig> & {
userOpenedDownloadDialog: boolean;
allowSaveSpellCheckerURL: boolean;
availableLanguages: Array<{label: string; value: string}>;
availableSpellcheckerLanguages: Array<{label: string; value: string}>;
canUpgrade?: boolean;
}
type SavingStateItems = {
appOptions: SavingState;
updates: SavingState;
};
}
type SaveQueueItem = {
configType: ConfigType;
@ -53,7 +60,7 @@ type SaveQueueItem = {
data: CombinedConfig[keyof CombinedConfig];
}
export default class SettingsPage extends React.PureComponent<Record<string, never>, State> {
class SettingsPage extends React.PureComponent<Props, State> {
trayIconThemeRef: React.RefObject<HTMLDivElement>;
downloadLocationRef: React.RefObject<HTMLInputElement>;
showTrayIconRef: React.RefObject<HTMLInputElement>;
@ -69,6 +76,7 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
startInFullscreenRef: React.RefObject<HTMLInputElement>;
autoCheckForUpdatesRef: React.RefObject<HTMLInputElement>;
logLevelRef: React.RefObject<HTMLSelectElement>;
appLanguageRef: React.RefObject<HTMLSelectElement>;
saveQueue: SaveQueueItem[];
@ -77,7 +85,7 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
savingIsDebounced: boolean;
resetSaveStateIsDebounced: boolean;
constructor(props: Record<string, never>) {
constructor(props: Props) {
super(props);
this.state = {
ready: false,
@ -88,6 +96,7 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
userOpenedDownloadDialog: false,
allowSaveSpellCheckerURL: false,
availableLanguages: [],
availableSpellcheckerLanguages: [],
};
this.getConfig();
@ -106,6 +115,7 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
this.spellCheckerURLRef = React.createRef();
this.autoCheckForUpdatesRef = React.createRef();
this.logLevelRef = React.createRef();
this.appLanguageRef = React.createRef();
this.saveQueue = [];
this.selectedSpellCheckerLocales = [];
@ -121,6 +131,12 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
});
window.ipcRenderer.invoke(GET_AVAILABLE_SPELL_CHECKER_LANGUAGES).then((languages: string[]) => {
const availableSpellcheckerLanguages = languages.filter((language) => localeTranslations[language]).map((language) => ({label: localeTranslations[language], value: language}));
availableSpellcheckerLanguages.sort((a, b) => a.label.localeCompare(b.label));
this.setState({availableSpellcheckerLanguages});
});
window.ipcRenderer.invoke(GET_AVAILABLE_LANGUAGES).then((languages: string[]) => {
const availableLanguages = languages.filter((language) => localeTranslations[language]).map((language) => ({label: localeTranslations[language], value: language}));
availableLanguages.sort((a, b) => a.label.localeCompare(b.label));
this.setState({availableLanguages});
@ -318,6 +334,13 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
});
}
handleChangeAppLanguage = () => {
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'appLanguage', data: this.appLanguageRef.current?.value});
this.setState({
appLanguage: this.appLanguageRef.current?.value,
});
}
handleChangeAutoCheckForUpdates = () => {
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_UPDATES, {key: 'autoCheckForUpdates', data: this.autoCheckForUpdatesRef.current?.checked});
this.setState({
@ -415,6 +438,8 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
}
render() {
const {intl} = this.props;
const settingsPage = {
close: {
textDecoration: 'none',
@ -471,6 +496,17 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
fontWeight: 500,
},
appLanguageInput: {
marginRight: '3px',
marginTop: '8px',
width: '320px',
height: '34px',
padding: '0 12px',
borderRadius: '4px',
border: '1px solid #ccc',
fontWeight: 500,
},
container: {
paddingBottom: '40px',
},
@ -496,9 +532,15 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
checked={this.state.autostart}
onChange={this.handleChangeAutoStart}
/>
{'Start app on login'}
<FormattedMessage
id='renderer.components.settingsPage.startAppOnLogin'
defaultMessage='Start app on login'
/>
<FormText>
{'If enabled, the app starts automatically when you log in to your machine.'}
<FormattedMessage
id='renderer.components.settingsPage.startAppOnLogin.description'
defaultMessage='If enabled, the app starts automatically when you log in to your machine.'
/>
</FormText>
</FormCheck>);
@ -512,9 +554,15 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
checked={this.state.hideOnStart}
onChange={this.handleChangeHideOnStart}
/>
{'Launch app minimized'}
<FormattedMessage
id='renderer.components.settingsPage.launchAppMinimized'
defaultMessage='Launch app minimized'
/>
<FormText>
{'If enabled, the app will start in system tray, and will not show the window on launch.'}
<FormattedMessage
id='renderer.components.settingsPage.launchAppMinimized.description'
defaultMessage='If enabled, the app will start in system tray, and will not show the window on launch.'
/>
</FormText>
</FormCheck>);
}
@ -530,10 +578,20 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
checked={this.state.useSpellChecker}
onChange={this.handleChangeUseSpellChecker}
/>
{'Check spelling'}
<FormattedMessage
id='renderer.components.settingsPage.checkSpelling'
defaultMessage='Check spelling'
/>
<FormText>
{'Highlight misspelled words in your messages based on your system language or language preference. '}
{'Setting takes effect after restarting the app.'}
<FormattedMessage
id='renderer.components.settingsPage.checkSpelling.description'
defaultMessage='Highlight misspelled words in your messages based on your system language or language preference.'
/>
{' '}
<FormattedMessage
id='renderer.components.settingsPage.afterRestart'
defaultMessage='Setting takes effect after restarting the app.'
/>
</FormText>
</FormCheck>
{this.state.useSpellChecker &&
@ -541,12 +599,17 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
inputId='inputSpellCheckerLocalesDropdown'
className='SettingsPage__spellCheckerLocalesDropdown'
classNamePrefix='SettingsPage__spellCheckerLocalesDropdown'
options={this.state.availableLanguages}
options={this.state.availableSpellcheckerLanguages}
isMulti={true}
isClearable={false}
onChange={this.handleChangeSpellCheckerLocales}
value={this.selectedSpellCheckerLocales}
placeholder={'Select preferred language(s)'}
placeholder={
<FormattedMessage
id='renderer.components.settingsPage.checkSpelling.preferredLanguages'
defaultMessage='Select preferred language(s)'
/>
}
/>
}
</>,
@ -559,7 +622,12 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
key='editSpellcheckerURL'
onClick={() => this.setState({spellCheckerURL: '', allowSaveSpellCheckerURL: false})}
variant='link'
>{'Use an alternative dictionary URL'}</Button>,
>
<FormattedMessage
id='renderer.components.settingsPage.checkSpelling.editSpellcheckUrl'
defaultMessage='Use an alternative dictionary URL'
/>
</Button>,
);
} else {
options.push(
@ -583,22 +651,33 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
id='saveSpellCheckerURL'
onClick={this.saveSpellCheckerURL}
>
<span>{'Save'}</span>
<FormattedMessage
id='label.save'
defaultMessage='Save'
/>
</Button>
<FormText>
{'Specify the url where dictionary definitions can be retrieved'}
<FormattedMessage
id='renderer.components.settingsPage.checkSpelling.specifyURL'
defaultMessage='Specify the url where dictionary definitions can be retrieved'
/>
</FormText>
<Button
id='revertSpellcheckerURL'
key='revertSpellcheckerURL'
onClick={this.resetSpellCheckerURL}
variant='link'
>{'Revert to default'}</Button>
>
<FormattedMessage
id='renderer.components.settingsPage.checkSpelling.revertToDefault'
defaultMessage='Revert to default'
/>
</Button>
</div>);
}
}
if (window.process.platform === 'darwin' || window.process.platform === 'win32') {
const TASKBAR = window.process.platform === 'win32' ? 'taskbar' : 'Dock';
const taskbar = window.process.platform === 'win32' ? 'taskbar' : 'Dock';
options.push(
<FormCheck
key='showunreadbadge'
@ -611,9 +690,17 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
checked={this.state.showUnreadBadge}
onChange={this.handleShowUnreadBadge}
/>
{`Show red badge on ${TASKBAR} icon to indicate unread messages`}
<FormattedMessage
id='renderer.components.settingsPage.showUnreadBadge'
defaultMessage='Show red badge on {taskbar} icon to indicate unread messages'
values={{taskbar}}
/>
<FormText>
{`Regardless of this setting, mentions are always indicated with a red badge and item count on the ${TASKBAR} icon.`}
<FormattedMessage
id='renderer.components.settingsPage.showUnreadBadge.description'
defaultMessage='Regardless of this setting, mentions are always indicated with a red badge and item count on the {taskbar} icon.'
values={{taskbar}}
/>
</FormText>
</FormCheck>);
}
@ -629,13 +716,30 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
checked={!this.state.notifications || this.state.notifications.flashWindow === 2}
onChange={this.handleFlashWindow}
/>
{'Flash taskbar icon when a new message is received'}
<FormattedMessage
id='renderer.components.settingsPage.flashWindow'
defaultMessage='Flash taskbar icon when a new message is received'
/>
<FormText>
{'If enabled, the taskbar icon will flash for a few seconds when a new message is received.'}
<FormattedMessage
id='renderer.components.settingsPage.flashWindow.description'
defaultMessage='If enabled, the taskbar icon will flash for a few seconds when a new message is received.'
/>
{window.process.platform === 'linux' && (
<>
<br/>
<em><strong>{'NOTE: '}</strong>{'This functionality may not work with all Linux window managers.'}</em>
<em>
<strong>
<FormattedMessage
id='renderer.components.settingsPage.flashWindow.description.note'
defaultMessage='NOTE: '
/>
</strong>
<FormattedMessage
id='renderer.components.settingsPage.flashWindow.description.linuxFunctionality'
defaultMessage='This functionality may not work with all Linux window managers.'
/>
</em>
</>
)}
</FormText>
@ -656,7 +760,12 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
checked={this.state.notifications ? this.state.notifications.bounceIcon : false}
onChange={this.handleBounceIcon}
style={{marginRight: '10px'}}
label='Bounce the Dock icon'
label={
<FormattedMessage
id='renderer.components.settingsPage.bounceIcon'
defaultMessage='Bounce the Dock icon'
/>
}
/>
<FormCheck
type='radio'
@ -670,7 +779,12 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
this.state.notifications.bounceIconType === 'informational'
}
onChange={this.handleBounceIconType}
label='once'
label={
<FormattedMessage
id='renderer.components.settingsPage.bounceIcon.once'
defaultMessage='once'
/>
}
/>
{' '}
<FormCheck
@ -681,12 +795,20 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
disabled={!this.state.notifications || !this.state.notifications.bounceIcon}
defaultChecked={this.state.notifications && this.state.notifications.bounceIconType === 'critical'}
onChange={this.handleBounceIconType}
label={'until I open the app'}
label={
<FormattedMessage
id='renderer.components.settingsPage.bounceIcon.untilOpenApp'
defaultMessage='until I open the app'
/>
}
/>
<FormText
style={{marginLeft: '20px'}}
>
{'If enabled, the Dock icon bounces once or until the user opens the app when a new notification is received.'}
<FormattedMessage
id='renderer.components.settingsPage.bounceIcon.description'
defaultMessage='If enabled, the Dock icon bounces once or until the user opens the app when a new notification is received.'
/>
</FormText>
</FormGroup>,
);
@ -704,9 +826,22 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
checked={this.state.showTrayIcon}
onChange={this.handleChangeShowTrayIcon}
/>
{window.process.platform === 'darwin' ? `Show ${this.state.appName} icon in the menu bar` : 'Show icon in the notification area'}
{window.process.platform === 'darwin' ?
<FormattedMessage
id='renderer.components.settingsPage.trayIcon.show.darwin'
defaultMessage='Show {appName} icon in the menu bar'
values={{appName: this.state.appName}}
/> :
<FormattedMessage
id='renderer.components.settingsPage.trayIcon.show'
defaultMessage='Show icon in the notification area'
/>
}
<FormText>
{'Setting takes effect after restarting the app.'}
<FormattedMessage
id='renderer.components.settingsPage.afterRestart'
defaultMessage='Setting takes effect after restarting the app.'
/>
</FormText>
</FormCheck>);
}
@ -719,7 +854,10 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
ref={this.trayIconThemeRef}
style={{marginLeft: '20px'}}
>
{'Icon theme: '}
<FormattedMessage
id='renderer.components.settingsPage.trayIcon.theme'
defaultMessage='Icon theme: '
/>
{window.process.platform === 'win32' &&
<>
<FormCheck
@ -729,7 +867,12 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
value='use_system'
defaultChecked={this.state.trayIconTheme === 'use_system' || !this.state.trayIconTheme}
onChange={() => this.handleChangeTrayIconTheme('use_system')}
label={'Use system default'}
label={
<FormattedMessage
id='renderer.components.settingsPage.trayIcon.theme.systemDefault'
defaultMessage='Use system default'
/>
}
/>
{' '}
</>
@ -741,7 +884,12 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
value='light'
defaultChecked={this.state.trayIconTheme === 'light' || !this.state.trayIconTheme}
onChange={() => this.handleChangeTrayIconTheme('light')}
label={'Light'}
label={
<FormattedMessage
id='renderer.components.settingsPage.trayIcon.theme.light'
defaultMessage='Light'
/>
}
/>
{' '}
<FormCheck
@ -751,7 +899,12 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
value='dark'
defaultChecked={this.state.trayIconTheme === 'dark'}
onChange={() => this.handleChangeTrayIconTheme('dark')}
label={'Dark'}
label={
<FormattedMessage
id='renderer.components.settingsPage.trayIcon.theme.dark'
defaultMessage='Dark'
/>
}
/>
</FormGroup>,
);
@ -771,10 +924,24 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
checked={this.state.minimizeToTray}
onChange={this.handleChangeMinimizeToTray}
/>
{'Leave app running in notification area when application window is closed'}
<FormattedMessage
id='renderer.components.settingsPage.minimizeToTray'
defaultMessage='Leave app running in notification area when application window is closed'
/>
<FormText>
{'If enabled, the app stays running in the notification area after app window is closed.'}
{this.state.showTrayIcon ? ' Setting takes effect after restarting the app.' : ''}
<FormattedMessage
id='renderer.components.settingsPage.minimizeToTray.description'
defaultMessage='If enabled, the app stays running in the notification area after app window is closed.'
/>
{this.state.showTrayIcon &&
<>
{' '}
<FormattedMessage
id='renderer.components.settingsPage.afterRestart'
defaultMessage='Setting takes effect after restarting the app.'
/>
</>
}
</FormText>
</FormCheck>);
}
@ -790,10 +957,20 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
checked={this.state.enableHardwareAcceleration}
onChange={this.handleChangeEnableHardwareAcceleration}
/>
{'Use GPU hardware acceleration'}
<FormattedMessage
id='renderer.components.settingsPage.enableHardwareAcceleration'
defaultMessage='Use GPU hardware acceleration'
/>
<FormText>
{'If enabled, Mattermost UI is rendered more efficiently but can lead to decreased stability for some systems.'}
{' Setting takes effect after restarting the app.'}
<FormattedMessage
id='renderer.components.settingsPage.enableHardwareAcceleration.description'
defaultMessage='If enabled, Mattermost UI is rendered more efficiently but can lead to decreased stability for some systems.'
/>
{' '}
<FormattedMessage
id='renderer.components.settingsPage.afterRestart'
defaultMessage='Setting takes effect after restarting the app.'
/>
</FormText>
</FormCheck>,
);
@ -809,9 +986,15 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
checked={this.state.startInFullscreen}
onChange={this.handleChangeStartInFullscreen}
/>
{'Open app in fullscreen'}
<FormattedMessage
id='renderer.components.settingsPage.fullscreen'
defaultMessage='Open app in fullscreen'
/>
<FormText>
{'If enabled, the Mattermost application will always open in full screen'}
<FormattedMessage
id='renderer.components.settingsPage.fullscreen.description'
defaultMessage='If enabled, the Mattermost application will always open in full screen'
/>
</FormText>
</FormCheck>,
);
@ -822,7 +1005,50 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
key='containerDownloadLocation'
>
<hr/>
<div>{'Download Location'}</div>
<FormattedMessage
id='renderer.components.settingsPage.appLanguage'
defaultMessage='Set app language (beta)'
/>
<FormControl
style={settingsPage.appLanguageInput}
as='select'
id='inputAppLanguage'
ref={this.appLanguageRef}
value={this.state.appLanguage}
onChange={this.handleChangeAppLanguage}
>
<option value=''>
{intl.formatMessage({id: 'renderer.components.settingsPage.appLanguage.useSystemDefault', defaultMessage: 'Use system default'})}
</option>
{this.state.availableLanguages.map((language) => {
return (
<option
key={language.value}
value={language.value}
>
{language.label}
</option>
);
})}
</FormControl>
<FormText>
<FormattedMessage
id='renderer.components.settingsPage.appLanguage.description'
defaultMessage='Chooses the language that the Desktop App will use for menu items and popups. Still in beta, some languages will be missing translation strings.'
/>
<br/>
<FormattedMessage
id='renderer.components.settingsPage.afterRestart'
defaultMessage='Setting takes effect after restarting the app.'
/>
</FormText>
<br/>
<div>
<FormattedMessage
id='renderer.components.settingsPage.downloadLocation'
defaultMessage='Download Location'
/>
</div>
<input
disabled={true}
style={settingsPage.downloadLocationInput}
@ -837,13 +1063,22 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
id='saveDownloadLocation'
onClick={this.selectDownloadLocation}
>
<span>{'Change'}</span>
<FormattedMessage
id='label.change'
defaultMessage='Change'
/>
</Button>
<FormText>
{'Specify the folder where files will download.'}
<FormattedMessage
id='renderer.components.settingsPage.downloadLocation.description'
defaultMessage='Specify the folder where files will download.'
/>
</FormText>
<br/>
{'Logging level'}
<FormattedMessage
id='renderer.components.settingsPage.loggingLevel'
defaultMessage='Logging level'
/>
<FormControl
style={settingsPage.logLevelInput}
as='select'
@ -852,16 +1087,35 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
value={this.state.logLevel}
onChange={this.handleChangeLogLevel}
>
<option value='error'>{'Errors (error)'}</option>
<option value='warn'>{'Errors and Warnings (warn)'}</option>
<option value='info'>{'Info (info)'}</option>
<option value='verbose'>{'Verbose (verbose)'}</option>
<option value='debug'>{'Debug (debug)'}</option>
<option value='silly'>{'Finest (silly)'}</option>
<option value='error'>
{intl.formatMessage({id: 'renderer.components.settingsPage.loggingLevel.level.error', defaultMessage: 'Errors (error)'})}
</option>
<option value='warn'>
{intl.formatMessage({id: 'renderer.components.settingsPage.loggingLevel.level.warn', defaultMessage: 'Errors and Warnings (warn)'})}
</option>
<option value='info'>
{intl.formatMessage({id: 'renderer.components.settingsPage.loggingLevel.level.info', defaultMessage: 'Info (info)'})}
</option>
<option value='verbose'>
{intl.formatMessage({id: 'renderer.components.settingsPage.loggingLevel.level.verbose', defaultMessage: 'Verbose (verbose)'})}
</option>
<option value='debug'>
{intl.formatMessage({id: 'renderer.components.settingsPage.loggingLevel.level.debug', defaultMessage: 'Debug (debug)'})}
</option>
<option value='silly'>
{intl.formatMessage({id: 'renderer.components.settingsPage.loggingLevel.level.silly', defaultMessage: 'Finest (silly)'})}
</option>
</FormControl>
<FormText>
{'Logging is helpful for developers and support to isolate issues you may be encountering with the desktop app.'}
<br/>{'Increasing the log level increases disk space usage and can impact performance. We recommend only increasing the log level if you are having issues.'}
<FormattedMessage
id='renderer.components.settingsPage.loggingLevel.description'
defaultMessage='Logging is helpful for developers and support to isolate issues you may be encountering with the desktop app.'
/>
<br/>
<FormattedMessage
id='renderer.components.settingsPage.loggingLevel.description.subtitle'
defaultMessage='Increasing the log level increases disk space usage and can impact performance. We recommend only increasing the log level if you are having issues.'
/>
</FormText>
</div>,
);
@ -871,12 +1125,22 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
optionsRow = (
<Row>
<Col md={12}>
<h2 style={settingsPage.sectionHeading}>{'App Options'}</h2>
<h2 style={settingsPage.sectionHeading}>
<FormattedMessage
id='renderer.components.settingsPage.appOptions'
defaultMessage='App Options'
/>
</h2>
<div className='IndicatorContainer appOptionsSaveIndicator'>
<AutoSaveIndicator
id='appOptionsSaveIndicator'
savingState={this.state.savingState.appOptions}
errorMessage={'Can\'t save your changes. Please try again.'}
errorMessage={
<FormattedMessage
id='renderer.components.settingsPage.saving.error'
defaultMessage={'Can\'t save your changes. Please try again.'}
/>
}
/>
</div>
{ options.map((opt) => (
@ -895,12 +1159,22 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
<>
<Row>
<Col md={12}>
<h2 style={settingsPage.sectionHeading}>{'Updates'}</h2>
<h2 style={settingsPage.sectionHeading}>
<FormattedMessage
id='renderer.components.settingsPage.updates'
defaultMessage='Updates'
/>
</h2>
<div className='IndicatorContainer updatesSaveIndicator'>
<AutoSaveIndicator
id='updatesSaveIndicator'
savingState={this.state.savingState.updates}
errorMessage={'Can\'t save your changes. Please try again.'}
errorMessage={
<FormattedMessage
id='renderer.components.settingsPage.saving.error'
defaultMessage={'Can\'t save your changes. Please try again.'}
/>
}
/>
</div>
<FormGroup
@ -915,9 +1189,15 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
checked={this.state.autoCheckForUpdates}
onChange={this.handleChangeAutoCheckForUpdates}
/>
{'Automatically check for updates'}
<FormattedMessage
id='renderer.components.settingsPage.updates.automatic'
defaultMessage='Automatically check for updates'
/>
<FormText>
{'If enabled, updates to the Desktop App will download automatically and you will be notified when ready to install.'}
<FormattedMessage
id='renderer.components.settingsPage.updates.automatic.description'
defaultMessage='If enabled, updates to the Desktop App will download automatically and you will be notified when ready to install.'
/>
</FormText>
</FormCheck>
<Button
@ -925,7 +1205,10 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
id='checkForUpdatesNow'
onClick={this.checkForUpdates}
>
<span>{'Check for Updates Now'}</span>
<FormattedMessage
id='renderer.components.settingsPage.updates.checkNow'
defaultMessage='Check for Updates Now'
/>
</Button>
</FormGroup>
</Col>
@ -944,7 +1227,14 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
</>
);
} else {
waitForIpc = (<p>{'Loading configuration...'}</p>);
waitForIpc = (
<p>
<FormattedMessage
id='renderer.components.settingsPage.loadingConfig'
defaultMessage='Loading configuration...'
/>
</p>
);
}
return (
@ -962,7 +1252,12 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
}}
>
<div style={{position: 'relative'}}>
<h1 style={settingsPage.heading}>{'Settings'}</h1>
<h1 style={settingsPage.heading}>
<FormattedMessage
id='renderer.components.settingsPage.header'
defaultMessage='Settings'
/>
</h1>
<hr/>
</div>
<Container
@ -975,3 +1270,5 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
);
}
}
export default injectIntl(SettingsPage);

View file

@ -5,11 +5,12 @@
import React from 'react';
import {Nav, NavItem, NavLink} from 'react-bootstrap';
import {DragDropContext, Draggable, DraggingStyle, Droppable, DropResult, NotDraggingStyle} from 'react-beautiful-dnd';
import {FormattedMessage, injectIntl, IntlShape} from 'react-intl';
import classNames from 'classnames';
import {Tab} from 'types/config';
import {getTabDisplayName, getTabViewName, TabType, canCloseTab} from 'common/tabs/TabView';
import {getTabViewName, TabType, canCloseTab, getTabDisplayName} from 'common/tabs/TabView';
type Props = {
activeTabName?: string;
@ -25,6 +26,7 @@ type Props = {
onDrop: (result: DropResult) => void;
tabsDisabled?: boolean;
isMenuOpen?: boolean;
intl: IntlShape;
};
function getStyle(style?: DraggingStyle | NotDraggingStyle) {
@ -38,7 +40,7 @@ function getStyle(style?: DraggingStyle | NotDraggingStyle) {
return style;
}
export default class TabBar extends React.PureComponent<Props> {
class TabBar extends React.PureComponent<Props> {
onCloseTab = (name: string) => {
return (event: React.MouseEvent<HTMLButtonElement>) => {
event.stopPropagation();
@ -102,7 +104,7 @@ export default class TabBar extends React.PureComponent<Props> {
as='li'
id={`teamTabItem${index}`}
draggable={false}
title={getTabDisplayName(tab.name as TabType)}
title={this.props.intl.formatMessage({id: `common.tabs.${tab.name}`, defaultMessage: getTabDisplayName(tab.name as TabType)})}
className={classNames('teamTabItem', {
active: this.props.activeTabName === tab.name,
dragging: snapshot.isDragging,
@ -121,9 +123,10 @@ export default class TabBar extends React.PureComponent<Props> {
}}
>
<div className='TabBar-tabSeperator'>
<span>
{getTabDisplayName(tab.name as TabType)}
</span>
<FormattedMessage
id={`common.tabs.${tab.name}`}
defaultMessage={getTabDisplayName(tab.name as TabType)}
/>
{ badgeDiv }
{canCloseTab(tab.name as TabType) &&
<button
@ -169,3 +172,5 @@ export default class TabBar extends React.PureComponent<Props> {
);
}
}
export default injectIntl(TabBar);

View file

@ -3,6 +3,7 @@
import classNames from 'classnames';
import React, {useEffect} from 'react';
import {FormattedMessage} from 'react-intl';
import {CLOSE_TEAMS_DROPDOWN, OPEN_TEAMS_DROPDOWN} from 'common/communication';
@ -64,7 +65,13 @@ const TeamDropdownButton: React.FC<Props> = (props: Props) => {
<i className='icon-server-variant'/>
{badgeDiv}
</div>
<span>{activeServerName || 'No servers configured'}</span>
{activeServerName && <span>{activeServerName}</span>}
{!activeServerName &&
<FormattedMessage
id='renderer.components.teamDropdownButton.noServersConfigured'
defaultMessage='No servers configured'
/>
}
<i className='icon-chevron-down'/>
</button>
);

View file

@ -1,110 +0,0 @@
// Copyright (c) 2015-2016 Yuya Ochiai
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Button, Navbar, ProgressBar} from 'react-bootstrap';
type InstallButtonProps = {
notifyOnly?: boolean;
onClickInstall?: React.MouseEventHandler<HTMLButtonElement>;
onClickDownload?: React.MouseEventHandler<HTMLButtonElement>;
};
function InstallButton(props: InstallButtonProps) {
if (props.notifyOnly) {
return (
<Button
variant='primary'
onClick={props.onClickDownload}
>{'Download Update'}</Button>
);
}
return (
<Button
variant='primary'
onClick={props.onClickInstall}
>{'Install Update'}</Button>
);
}
type UpdaterPageProps = {
appName: string;
notifyOnly?: boolean;
isDownloading?: boolean;
progress?: number;
onClickInstall?: React.MouseEventHandler<HTMLButtonElement>;
onClickDownload?: React.MouseEventHandler<HTMLButtonElement>;
onClickReleaseNotes?: React.MouseEventHandler<HTMLAnchorElement>;
onClickRemind?: React.MouseEventHandler<HTMLButtonElement>;
onClickSkip?: React.MouseEventHandler<HTMLButtonElement>;
onClickCancel?: React.MouseEventHandler<HTMLButtonElement>;
};
function UpdaterPage(props: UpdaterPageProps) {
let footer;
if (props.isDownloading) {
footer = (
<Navbar
className='UpdaterPage-footer'
fixed='bottom'
>
<ProgressBar
animated={true}
now={props.progress}
label={`${props.progress}%`}
/>
<div className='pull-right'>
<Button
onClick={props.onClickCancel}
>{'Cancel'}</Button>
</div>
</Navbar>
);
} else {
footer = (
<Navbar
className='UpdaterPage-footer'
fixed='bottom'
>
<Button
className='UpdaterPage-skipButton'
variant='link'
onClick={props.onClickSkip}
>{'Skip this version'}</Button>
<div className='pull-right'>
<Button
variant='link'
onClick={props.onClickRemind}
>{'Remind me in 2 days'}</Button>
<InstallButton
notifyOnly={props.notifyOnly}
onClickInstall={props.onClickInstall}
onClickDownload={props.onClickDownload}
/>
</div>
</Navbar>
);
}
return (
<div className='UpdaterPage'>
<Navbar>
<h1 className='UpdaterPage-heading'>{'New update is available'}</h1>
</Navbar>
<div className='container-fluid'>
<p>{`A new version of the ${props.appName} is available!`}</p>
<p>{'Read the '}
<a
href='#'
onClick={props.onClickReleaseNotes}
>{'release notes'}</a>
{' to learn more.'}
</p>
</div>
{footer}
</div>
);
}
export default UpdaterPage;

View file

@ -1,53 +0,0 @@
// Copyright (c) 2015-2016 Yuya Ochiai
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {storiesOf} from '@storybook/react';
import {action} from '@storybook/addon-actions';
import UpdaterPage from '../UpdaterPage';
import '../../css/components/UpdaterPage.css';
/*
appName: propTypes.string.isRequired,
notifyOnly: propTypes.bool.isRequired,
isDownloading: propTypes.bool.isRequired,
progress: propTypes.number,
onClickInstall: propTypes.func.isRequired,
onClickDownload: propTypes.func.isRequired,
onClickReleaseNotes: propTypes.func.isRequired,
onClickRemind: propTypes.func.isRequired,
onClickSkip: propTypes.func.isRequired,
*/
const appName = 'Storybook App';
storiesOf('UpdaterPage', module).
add('Normal', () => (
<UpdaterPage
appName={appName}
notifyOnly={false}
isDownloading={false}
progress={0}
onClickInstall={action('clicked install')}
onClickReleaseNotes={action('clicked release notes')}
onClickRemind={action('clicked remind')}
onClickSkip={action('clicked skip')}
/>
)).
add('NotifyOnly', () => (
<UpdaterPage
appName={appName}
notifyOnly={true}
onClickDownload={action('clicked download')}
/>
)).
add('Downloading', () => (
<UpdaterPage
appName={appName}
isDownloading={true}
progress={0}
onClickCancel={action('clicked cancel')}
/>
));

View file

@ -3,6 +3,7 @@
import React, {Fragment} from 'react';
import {Modal, Button, Row, Col} from 'react-bootstrap';
import {FormattedMessage} from 'react-intl';
import {Certificate} from 'electron/renderer';
type Props = {
@ -54,7 +55,10 @@ export default class ShowCertificateModal extends React.PureComponent<Props, Sta
onHide={() => {}}
>
<Modal.Body>
{'No certificate Selected'}
<FormattedMessage
id='renderer.components.showCertificateModal.noCertSelected'
defaultMessage='No certificate Selected'
/>
</Modal.Body>
</Modal>
);
@ -69,7 +73,7 @@ export default class ShowCertificateModal extends React.PureComponent<Props, Sta
const expiration = utcSeconds(this.state.certificate?.validExpiry || 0);
const creation = utcSeconds(this.state.certificate?.validStart || 0);
const dateDisplayOptions = {dateStyle: 'full' as const, timeStyle: 'full' as const};
const dateLocale = 'en-US';
const dateLocale = 'en-US'; // TODO: Translate?
return (
<Modal
bsClass='modal'
@ -83,21 +87,72 @@ export default class ShowCertificateModal extends React.PureComponent<Props, Sta
<Modal.Body>
<p className='details'>{'Details'}</p>
<dl>
{certificateSection('Subject Name')}
{certificateItem('Common Name', this.state.certificate?.subject.commonName)}
{certificateSection(
<FormattedMessage
id='renderer.components.showCertificateModal.subjectName'
defaultMessage='Subject Name'
/>,
)}
{certificateItem(
<FormattedMessage
id='renderer.components.showCertificateModal.commonName'
defaultMessage='Common Name'
/>,
this.state.certificate?.subject.commonName,
)}
</dl>
<dl>
{certificateSection('Issuer Name')}
{certificateItem('Common Name', this.state.certificate?.issuer.commonName)}
{certificateSection(
<FormattedMessage
id='renderer.components.showCertificateModal.issuerName'
defaultMessage='Issuer Name'
/>,
)}
{certificateItem(
<FormattedMessage
id='renderer.components.showCertificateModal.commonName'
defaultMessage='Common Name'
/>,
this.state.certificate?.issuer.commonName,
)}
</dl>
<dl>
{certificateItem('Serial Number', this.state.certificate?.serialNumber)}
{certificateItem('Not Valid Before', creation.toLocaleString(dateLocale, dateDisplayOptions))}
{certificateItem('Not Valid After', expiration.toLocaleString(dateLocale, dateDisplayOptions))}
{certificateItem(
<FormattedMessage
id='renderer.components.showCertificateModal.serialNumber'
defaultMessage='Serial Number'
/>,
this.state.certificate?.serialNumber,
)}
{certificateItem(
<FormattedMessage
id='renderer.components.showCertificateModal.notValidBefore'
defaultMessage='Not Valid Before'
/>,
creation.toLocaleString(dateLocale, dateDisplayOptions),
)}
{certificateItem(
<FormattedMessage
id='renderer.components.showCertificateModal.notValidAfter'
defaultMessage='Not Valid After'
/>,
expiration.toLocaleString(dateLocale, dateDisplayOptions),
)}
</dl>
<dl>
{certificateSection('Public Key Info')}
{certificateItem('Algorithm', this.state.certificate?.fingerprint.split('/')[0])}
{certificateSection(
<FormattedMessage
id='renderer.components.showCertificateModal.publicKeyInfo'
defaultMessage='Public Key Info'
/>,
)}
{certificateItem(
<FormattedMessage
id='renderer.components.showCertificateModal.algorithm'
defaultMessage='Algorithm'
/>,
this.state.certificate?.fingerprint.split('/')[0],
)}
</dl>
</Modal.Body>
<Modal.Footer className={'no-border'}>
@ -108,7 +163,12 @@ export default class ShowCertificateModal extends React.PureComponent<Props, Sta
variant='primary'
onClick={this.handleOk}
className={'primary'}
>{'Close'}</Button>
>
<FormattedMessage
id='label.close'
defaultMessage='Close'
/>
</Button>
</Col>
</Row>
</div>

View file

@ -3,6 +3,7 @@
import React from 'react';
import ReactDOM from 'react-dom';
import {FormattedMessage} from 'react-intl';
import classNames from 'classnames';
import {DragDropContext, Draggable, DraggingStyle, Droppable, DropResult, NotDraggingStyle} from 'react-beautiful-dnd';
@ -23,6 +24,8 @@ import {TAB_BAR_HEIGHT, THREE_DOT_MENU_WIDTH_MAC} from 'common/utils/constants';
import './css/dropdown.scss';
import IntlProvider from './intl_provider';
type State = {
teams?: TeamWithTabs[];
orderedTeams?: TeamWithTabs[];
@ -227,6 +230,7 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
render() {
return (
<IntlProvider>
<div
onClick={this.preventPropogation}
className={classNames('TeamDropdown', {
@ -238,7 +242,12 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
}}
>
<div className='TeamDropdown__header'>
<span className='TeamDropdown__servers'>{'Servers'}</span>
<span className='TeamDropdown__servers'>
<FormattedMessage
id='renderer.dropdown.servers'
defaultMessage='Servers'
/>
</span>
<span className='TeamDropdown__keyboardShortcut'>
{window.process.platform === 'darwin' ? '⌃⌘S' : 'Ctrl + Shift + S'}
</span>
@ -354,10 +363,14 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
onClick={this.addServer}
>
<i className='icon-plus'/>
<span>{'Add a server'}</span>
<FormattedMessage
id='renderer.dropdown.addAServer'
defaultMessage='Add a server'
/>
</button>
}
</div>
</IntlProvider>
);
}
}

View file

@ -13,6 +13,7 @@ import {CombinedConfig, Team} from 'types/config';
import {GET_CONFIGURATION, UPDATE_TEAMS, QUIT, RELOAD_CONFIGURATION} from 'common/communication';
import MainPage from './components/MainPage';
import IntlProvider from './intl_provider';
type State = {
config?: CombinedConfig;
@ -120,6 +121,7 @@ class Root extends React.PureComponent<Record<string, never>, State> {
}
return (
<IntlProvider>
<MainPage
teams={config.teams}
lastActiveTeam={config.lastActiveTeam}
@ -129,6 +131,7 @@ class Root extends React.PureComponent<Record<string, never>, State> {
appName={config.appName}
useNativeWindow={config.useNativeWindow}
/>
</IntlProvider>
);
}
}

View file

@ -0,0 +1,55 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {IntlProvider as BaseIntlProvider} from 'react-intl';
import {GET_LANGUAGE_INFORMATION, RETRIEVED_LANGUAGE_INFORMATION} from 'common/communication';
import {Language} from '../../i18n/i18n';
type State = {
language?: Language;
}
export default class IntlProvider extends React.PureComponent<any, State> {
constructor(props: any) {
super(props);
this.state = {};
}
componentDidMount() {
window.addEventListener('message', this.handleMessageEvent);
window.postMessage({type: GET_LANGUAGE_INFORMATION});
}
componentWillUnmount() {
window.removeEventListener('message', this.handleMessageEvent);
}
handleMessageEvent = (event: MessageEvent<{type: string; data: Language}>) => {
if (event.data.type === RETRIEVED_LANGUAGE_INFORMATION) {
this.setState({
language: event.data.data,
});
}
}
render() {
if (!this.state.language) {
return null;
}
return (
<BaseIntlProvider
key={this.state.language.value}
locale={this.state.language.value}
messages={this.state.language.url}
textComponent='span'
wrapRichTextChunksInFragment={false}
>
{this.props.children}
</BaseIntlProvider>
);
}
}

View file

@ -4,12 +4,15 @@
import {Certificate} from 'electron/renderer';
import React, {Fragment} from 'react';
import {Modal, Button, Table, Row, Col} from 'react-bootstrap';
import {FormattedMessage} from 'react-intl';
import {CertificateModalData} from 'types/certificate';
import {ModalMessage} from 'types/modals';
import {MODAL_INFO} from 'common/communication';
import IntlProvider from 'renderer/intl_provider';
import ShowCertificateModal from '../../components/showCertificateModal';
type Props = {
@ -91,7 +94,12 @@ export default class SelectCertificateModal extends React.PureComponent<Props, S
</Fragment>
);
}
return (<Fragment><tr/><tr><td/><td>{'No certificates available'}</td><td/></tr></Fragment>);
return (<Fragment><tr/><tr><td/><td>
<FormattedMessage
id='renderer.modals.certificate.certificateModal.noCertsAvailable'
defaultMessage='No certificates available'
/>
</td><td/></tr></Fragment>);
}
getSelectedCert = () => {
@ -128,6 +136,7 @@ export default class SelectCertificateModal extends React.PureComponent<Props, S
}
return (
<IntlProvider>
<Modal
bsClass='modal'
className='certificate-modal'
@ -135,10 +144,21 @@ export default class SelectCertificateModal extends React.PureComponent<Props, S
onHide={() => {}}
>
<Modal.Header>
<Modal.Title >{'Select a certificate'}</Modal.Title>
<Modal.Title>
<FormattedMessage
id='renderer.modals.certificate.certificateModal.title'
defaultMessage='Select a certificate'
/>
</Modal.Title>
</Modal.Header>
<Modal.Body>
<p className={'subtitle'}>{`Select a certificate to authenticate yourself to ${this.state.url}`}</p>
<p className={'subtitle'}>
<FormattedMessage
id='renderer.modals.certificate.certificateModal.subtitle'
defaultMessage='Select a certificate to authenticate yourself to {url}'
values={{url: this.state.url}}
/>
</p>
<Table
striped={true}
hover={true}
@ -148,9 +168,24 @@ export default class SelectCertificateModal extends React.PureComponent<Props, S
>
<thead>
<tr>
<th><span className={'divider'}>{'Subject'}</span></th>
<th><span className={'divider'}>{'Issuer'}</span></th>
<th>{'Serial'}</th>
<th><span className={'divider'}>
<FormattedMessage
id='renderer.modals.certificate.certificateModal.subject'
defaultMessage='Subject'
/>
</span></th>
<th><span className={'divider'}>
<FormattedMessage
id='renderer.modals.certificate.certificateModal.issuer'
defaultMessage='Issuer'
/>
</span></th>
<th>
<FormattedMessage
id='renderer.modals.certificate.certificateModal.serial'
defaultMessage='Serial'
/>
</th>
</tr>
</thead>
<tbody>
@ -168,25 +203,41 @@ export default class SelectCertificateModal extends React.PureComponent<Props, S
disabled={this.state.selectedIndex === null}
onClick={this.handleCertificateInfo}
className={'info'}
>{'Certificate Information'}</Button>
>
<FormattedMessage
id='renderer.modals.certificate.certificateModal.certInfoButton'
defaultMessage='Certificate Information'
/>
</Button>
</Col>
<Col sm={8}>
<Button
variant='link'
onClick={this.props.onCancel}
className={'secondary'}
>{'Cancel'}</Button>
>
<FormattedMessage
id='label.cancel'
defaultMessage='Cancel'
/>
</Button>
<Button
variant='primary'
onClick={this.handleOk}
disabled={this.state.selectedIndex === null}
className={'primary'}
>{'OK'}</Button>
>
<FormattedMessage
id='label.ok'
defaultMessage='OK'
/>
</Button>
</Col>
</Row>
</div>
</Modal.Footer>
</Modal>
</IntlProvider>
);
}
}

View file

@ -12,6 +12,8 @@ import {ModalMessage} from 'types/modals';
import {MODAL_CANCEL, MODAL_INFO, MODAL_RESULT, RETRIEVE_MODAL_INFO} from 'common/communication';
import IntlProvider from 'renderer/intl_provider';
import NewTeamModal from '../../components/NewTeamModal'; //'./addServer.jsx';
import setupDarkMode from '../darkMode';
@ -48,6 +50,7 @@ const EditServerModalWrapper: React.FC = () => {
}, []);
return (
<IntlProvider>
<NewTeamModal
onClose={onClose}
onSave={onSave}
@ -56,6 +59,7 @@ const EditServerModalWrapper: React.FC = () => {
team={server}
currentTeams={currentTeams}
/>
</IntlProvider>
);
};

View file

@ -4,6 +4,7 @@
import React from 'react';
import {Button, Col, FormLabel, Form, FormGroup, FormControl, Modal} from 'react-bootstrap';
import {FormattedMessage, injectIntl, IntlShape} from 'react-intl';
import {LoginModalData} from 'types/auth';
import {ModalMessage} from 'types/modals';
@ -12,10 +13,13 @@ import {AuthenticationResponseDetails, AuthInfo} from 'electron/renderer';
import urlUtils from 'common/utils/url';
import {MODAL_INFO} from 'common/communication';
import IntlProvider from 'renderer/intl_provider';
type Props = {
onCancel: (request: AuthenticationResponseDetails) => void;
onLogin: (request: AuthenticationResponseDetails, username: string, password: string) => void;
getAuthInfo: () => void;
intl: IntlShape;
};
type State = {
@ -25,7 +29,7 @@ type State = {
authInfo?: AuthInfo;
};
export default class LoginModal extends React.PureComponent<Props, State> {
class LoginModal extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
@ -86,27 +90,47 @@ export default class LoginModal extends React.PureComponent<Props, State> {
this.setState({password: e.target.value});
}
render() {
let theServer = '';
renderLoginModalMessage = () => {
if (!(this.state.request && this.state.authInfo)) {
theServer = '';
return null;
} else if (this.state.authInfo.isProxy) {
theServer = `The proxy ${this.state.authInfo.host}:${this.state.authInfo.port}`;
} else {
const tmpURL = urlUtils.parseURL(this.state.request.url);
theServer = `The server ${tmpURL?.protocol}//${tmpURL?.host}`;
}
const message = `${theServer} requires a username and password.`;
return (
<FormattedMessage
id='renderer.modals.login.loginModal.message.proxy'
defaultMessage='The proxy {host}:{port} requires a username and password.'
values={{host: this.state.authInfo.host, port: this.state.authInfo.port}}
/>
);
}
const tmpURL = urlUtils.parseURL(this.state.request.url);
return (
<FormattedMessage
id='renderer.modals.login.loginModal.message.server'
defaultMessage='The server {url} requires a username and password.'
values={{url: `${tmpURL?.protocol}//${tmpURL?.host}`}}
/>
);
}
render() {
const {intl} = this.props;
return (
<IntlProvider>
<Modal
show={Boolean(this.state.request && this.state.authInfo)}
>
<Modal.Header>
<Modal.Title>{'Authentication Required'}</Modal.Title>
<Modal.Title>
<FormattedMessage
id='renderer.modals.login.loginModal.title'
defaultMessage='Authentication Required'
/>
</Modal.Title>
</Modal.Header>
<Modal.Body>
<p>
{ message }
{this.renderLoginModalMessage()}
</p>
<Form
onSubmit={this.handleSubmit}
@ -115,11 +139,16 @@ export default class LoginModal extends React.PureComponent<Props, State> {
<Col
as={FormLabel}
sm={2}
>{'User Name'}</Col>
>
<FormattedMessage
id='renderer.modals.login.loginModal.username'
defaultMessage='User Name'
/>
</Col>
<Col sm={10}>
<FormControl
type='text'
placeholder='User Name'
placeholder={intl.formatMessage({id: 'renderer.modals.login.loginModal.username', defaultMessage: 'User Name'})}
onChange={this.setUsername}
value={this.state.username}
onClick={(e: React.MouseEvent<HTMLInputElement>) => {
@ -132,11 +161,16 @@ export default class LoginModal extends React.PureComponent<Props, State> {
<Col
as={FormLabel}
sm={2}
>{'Password'}</Col>
>
<FormattedMessage
id='renderer.modals.login.loginModal.password'
defaultMessage='Password'
/>
</Col>
<Col sm={10}>
<FormControl
type='password'
placeholder='Password'
placeholder={intl.formatMessage({id: 'renderer.modals.login.loginModal.password', defaultMessage: 'Password'})}
onChange={this.setPassword}
value={this.state.password}
onClick={(e: React.MouseEvent<HTMLInputElement>) => {
@ -151,18 +185,31 @@ export default class LoginModal extends React.PureComponent<Props, State> {
<Button
type='submit'
variant='primary'
>{'Login'}</Button>
>
<FormattedMessage
id='label.login'
defaultMessage='Login'
/>
</Button>
{ ' ' }
<Button
variant='link'
onClick={this.handleCancel}
>{'Cancel'}</Button>
>
<FormattedMessage
id='label.cancel'
defaultMessage='Cancel'
/>
</Button>
</div>
</Col>
</FormGroup>
</Form>
</Modal.Body>
</Modal>
</IntlProvider>
);
}
}
export default injectIntl(LoginModal);

View file

@ -12,6 +12,8 @@ import {ModalMessage} from 'types/modals';
import {GET_MODAL_UNCLOSEABLE, MODAL_CANCEL, MODAL_INFO, MODAL_RESULT, MODAL_UNCLOSEABLE, RETRIEVE_MODAL_INFO} from 'common/communication';
import IntlProvider from 'renderer/intl_provider';
import NewTeamModal from '../../components/NewTeamModal'; //'./addServer.jsx';
import setupDarkMode from '../darkMode';
@ -55,6 +57,7 @@ const NewServerModalWrapper: React.FC = () => {
}, []);
return (
<IntlProvider>
<NewTeamModal
onClose={unremoveable ? undefined : onClose}
onSave={onSave}
@ -62,6 +65,7 @@ const NewServerModalWrapper: React.FC = () => {
show={true}
currentTeams={currentTeams}
/>
</IntlProvider>
);
};

View file

@ -3,20 +3,24 @@
import React from 'react';
import {Modal, Button} from 'react-bootstrap';
import {FormattedMessage, injectIntl, IntlShape} from 'react-intl';
import {PermissionType} from 'types/trustedOrigin';
import {ModalMessage} from 'types/modals';
import urlUtil from 'common/utils/url';
import {t} from 'common/utils/util';
import {MODAL_INFO} from 'common/communication';
import {PERMISSION_DESCRIPTION} from 'common/permissions';
import IntlProvider from 'renderer/intl_provider';
type Props = {
handleDeny: React.MouseEventHandler<HTMLButtonElement>;
handleGrant: React.MouseEventHandler<HTMLButtonElement>;
getPermissionInfo: () => void;
openExternalLink: (protocol: string, url: string) => void;
intl: IntlShape;
};
type State = {
@ -24,7 +28,7 @@ type State = {
permission?: PermissionType;
}
export default class PermissionModal extends React.PureComponent<Props, State> {
class PermissionModal extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = {};
@ -53,12 +57,13 @@ export default class PermissionModal extends React.PureComponent<Props, State> {
}
getModalTitle() {
return `${PERMISSION_DESCRIPTION[this.state.permission!]} Required`;
const permission = this.props.intl.formatMessage({id: `common.permissions.${PERMISSION_DESCRIPTION[this.state.permission!]}`});
return this.props.intl.formatMessage({id: 'renderer.modals.permission.permissionModal.title', defaultMessage: '{permission} Required'}, {permission});
}
getModalBody() {
const {url, permission} = this.state;
const originDisplay = url ? urlUtil.getHost(url) : 'unknown origin';
const originDisplay = url ? urlUtil.getHost(url) : this.props.intl.formatMessage({id: 'renderer.modals.permission.permissionModal.unknownOrigin', defaultMessage: 'unknown origin'});
const originLink = url ? originDisplay : '';
const click = (e: React.MouseEvent<HTMLAnchorElement>) => {
@ -75,10 +80,20 @@ export default class PermissionModal extends React.PureComponent<Props, State> {
return (
<div>
<p>
{`A site that's not included in your Mattermost server configuration requires access for ${PERMISSION_DESCRIPTION[permission!]}.`}
<FormattedMessage
id='renderer.modals.permission.permissionModal.body'
defaultMessage={'A site that\'s not included in your Mattermost server configuration requires access for {permission}.'}
values={{
permission: this.props.intl.formatMessage({id: `common.permissions.${PERMISSION_DESCRIPTION[permission!]}`}),
}}
/>
{}
</p>
<p>
<span>{'This request originated from '}</span>
<FormattedMessage
id='renderer.modals.permission.permissionModal.requestOriginatedFrom'
defaultMessage='This request originated from '
/>
<a onClick={click}>{originDisplay}</a>
</p>
</div>
@ -87,6 +102,7 @@ export default class PermissionModal extends React.PureComponent<Props, State> {
render() {
return (
<IntlProvider>
<Modal
bsClass='modal'
className='permission-modal'
@ -103,16 +119,29 @@ export default class PermissionModal extends React.PureComponent<Props, State> {
</Modal.Body>
<Modal.Footer className={'remove-border'}>
<div>
<Button
onClick={this.props.handleDeny}
>{'Cancel'}</Button>
<Button onClick={this.props.handleDeny}>
<FormattedMessage
id='label.cancel'
defaultMessage='Cancel'
/>
</Button>
<Button
variant='primary'
onClick={this.props.handleGrant}
>{'Accept'}</Button>
>
<FormattedMessage
id='label.accept'
defaultMessage='Accept'
/>
</Button>
</div>
</Modal.Footer>
</Modal>
</IntlProvider>
);
}
}
t('common.permissions.canBasicAuth');
export default injectIntl(PermissionModal);

View file

@ -11,6 +11,8 @@ import {ModalMessage} from 'types/modals';
import {MODAL_CANCEL, MODAL_INFO, MODAL_RESULT, RETRIEVE_MODAL_INFO} from 'common/communication';
import IntlProvider from 'renderer/intl_provider';
import RemoveServerModal from '../../components/RemoveServerModal';
import setupDarkMode from '../darkMode';
@ -45,6 +47,7 @@ const RemoveServerModalWrapper: React.FC = () => {
}, []);
return (
<IntlProvider>
<RemoveServerModal
show={true}
serverName={serverName}
@ -58,6 +61,7 @@ const RemoveServerModalWrapper: React.FC = () => {
onSave(true);
}}
/>
</IntlProvider>
);
};

View file

@ -14,6 +14,7 @@ import {DARK_MODE_CHANGE, GET_DARK_MODE} from 'common/communication';
import darkStyles from 'renderer/css/lazy/settings-dark.lazy.css';
import SettingsPage from './components/SettingsPage';
import IntlProvider from './intl_provider';
const setDarkMode = (darkMode: boolean) => {
if (darkMode) {
@ -28,7 +29,12 @@ window.ipcRenderer.invoke(GET_DARK_MODE).then(setDarkMode);
const start = async () => {
ReactDOM.render(
<SettingsPage/>,
(
<IntlProvider>
<SettingsPage/>
</IntlProvider>
)
,
document.getElementById('app'),
);
};

View file

@ -1,13 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Updater</title>
<link rel="stylesheet" href="../node_modules/bootstrap/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="css/components/index.css">
</head>
<body>
<div id="content"></div>
<script src="updater_bundle.js"></script>
</body>
</html>

View file

@ -1,162 +0,0 @@
// Copyright (c) 2015-2016 Yuya Ochiai
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// TODO: Commented out for now since remote is removed, will be changed with the autoupdater changes anyway.
// import url from 'url';
// import React from 'react';
// import ReactDOM from 'react-dom';
// //import {remote} from 'electron';
// import UpdaterPage from './components/UpdaterPage';
// const thisURL = url.parse(location.href, true);
// const notifyOnly = thisURL.query.notifyOnly === 'true';
// type Props = {
// notifyOnly: boolean;
// initialState: State;
// };
// type State = {
// }
// class UpdaterPageContainer extends React.PureComponent<Props, State> {
// constructor(props: Props) {
// super(props);
// this.state = props.initialState;
// }
// getTabWebContents() {
// return null;//remote.webContents.getFocusedWebContents();
// }
// componentDidMount() {
// window.ipcRenderer.on('start-download', () => {
// this.setState({
// isDownloading: true,
// });
// });
// window.ipcRenderer.on('progress', (event, progress) => {
// this.setState({
// progress,
// });
// });
// window.ipcRenderer.on('zoom-in', () => {
// const activeTabWebContents = this.getTabWebContents();
// if (!activeTabWebContents) {
// return;
// }
// if (activeTabWebContents.zoomLevel >= 9) {
// return;
// }
// activeTabWebContents.zoomLevel += 1;
// });
// window.ipcRenderer.on('zoom-out', () => {
// const activeTabWebContents = this.getTabWebContents();
// if (!activeTabWebContents) {
// return;
// }
// if (activeTabWebContents.zoomLevel <= -8) {
// return;
// }
// activeTabWebContents.zoomLevel -= 1;
// });
// window.ipcRenderer.on('zoom-reset', () => {
// const activeTabWebContents = this.getTabWebContents();
// if (!activeTabWebContents) {
// return;
// }
// activeTabWebContents.zoomLevel = 0;
// });
// window.ipcRenderer.on('undo', () => {
// const activeTabWebContents = this.getTabWebContents();
// if (!activeTabWebContents) {
// return;
// }
// activeTabWebContents.undo();
// });
// window.ipcRenderer.on('redo', () => {
// const activeTabWebContents = this.getTabWebContents();
// if (!activeTabWebContents) {
// return;
// }
// activeTabWebContents.redo();
// });
// window.ipcRenderer.on('cut', () => {
// const activeTabWebContents = this.getTabWebContents();
// if (!activeTabWebContents) {
// return;
// }
// activeTabWebContents.cut();
// });
// window.ipcRenderer.on('copy', () => {
// const activeTabWebContents = this.getTabWebContents();
// if (!activeTabWebContents) {
// return;
// }
// activeTabWebContents.copy();
// });
// window.ipcRenderer.on('paste', () => {
// const activeTabWebContents = this.getTabWebContents();
// if (!activeTabWebContents) {
// return;
// }
// activeTabWebContents.paste();
// });
// window.ipcRenderer.on('paste-and-match', () => {
// const activeTabWebContents = this.getTabWebContents();
// if (!activeTabWebContents) {
// return;
// }
// activeTabWebContents.pasteAndMatchStyle();
// });
// }
// render() {
// return (
// <UpdaterPage
// appName={`${remote.app.name} Desktop App`}
// notifyOnly={this.props.notifyOnly}
// {...this.state}
// onClickReleaseNotes={() => {
// window.ipcRenderer.send('click-release-notes');
// }}
// onClickSkip={() => {
// window.ipcRenderer.send('click-skip');
// }}
// onClickRemind={() => {
// window.ipcRenderer.send('click-remind');
// }}
// onClickInstall={() => {
// window.ipcRenderer.send('click-install');
// }}
// onClickDownload={() => {
// window.ipcRenderer.send('click-download');
// }}
// onClickCancel={() => {
// window.ipcRenderer.send('click-cancel');
// }}
// />
// );
// }
// }
// ReactDOM.render(
// <UpdaterPageContainer
// notifyOnly={notifyOnly}
// initialState={{isDownloading: false, progress: 0}}
// />,
// document.getElementById('content'),
// );

View file

@ -45,6 +45,7 @@ export type ConfigV3 = {
alwaysMinimize?: boolean;
alwaysClose?: boolean;
logLevel?: string;
appLanguage?: string;
}
export type ConfigV2 = {