summaryrefslogtreecommitdiffhomepage
path: root/gui/src
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2021-01-26 09:19:06 +0100
committerOskar Nyberg <oskar@mullvad.net>2021-01-26 09:19:06 +0100
commit2c5163b9fa05e8b21ed92f6992e3a216e96817fe (patch)
tree32f4f2e706e152626b31ca472e1e564f71340319 /gui/src
parent2b055cc39c4eee4b8af8bb193143ddd4055b39ac (diff)
parent5e9d51d61ecf3a7f8df10fcfc767213a108274c1 (diff)
downloadmullvadvpn-2c5163b9fa05e8b21ed92f6992e3a216e96817fe.tar.xz
mullvadvpn-2c5163b9fa05e8b21ed92f6992e3a216e96817fe.zip
Merge branch 'sandbox-electron-renderer'
Diffstat (limited to 'gui/src')
-rw-r--r--gui/src/main/index.ts54
-rw-r--r--gui/src/main/ipc-event-channel.ts5
-rw-r--r--gui/src/main/linux-desktop-entry.ts16
-rw-r--r--gui/src/main/load-translations.ts72
-rw-r--r--gui/src/main/logging.ts2
-rw-r--r--gui/src/main/window-controller.ts7
-rw-r--r--gui/src/renderer/app.tsx67
-rw-r--r--gui/src/renderer/components/AdvancedSettings.tsx2
-rw-r--r--gui/src/renderer/components/CustomScrollbars.tsx2
-rw-r--r--gui/src/renderer/components/Focus.tsx3
-rw-r--r--gui/src/renderer/components/HeaderBar.tsx2
-rw-r--r--gui/src/renderer/components/ImageView.tsx3
-rw-r--r--gui/src/renderer/components/NavigationBarStyles.tsx2
-rw-r--r--gui/src/renderer/components/PlatformWindow.tsx2
-rw-r--r--gui/src/renderer/components/Preferences.tsx4
-rw-r--r--gui/src/renderer/containers/SelectLanguagePage.tsx4
-rw-r--r--gui/src/renderer/context.tsx4
-rw-r--r--gui/src/renderer/index.html3
-rw-r--r--gui/src/renderer/lib/ipc-event-channel.ts5
-rw-r--r--gui/src/renderer/lib/load-translations.ts22
-rw-r--r--gui/src/renderer/lib/logging.ts3
-rw-r--r--gui/src/renderer/preload.ts4
-rw-r--r--gui/src/renderer/redux/settings/reducers.ts2
-rw-r--r--gui/src/renderer/redux/store.ts2
-rw-r--r--gui/src/shared/gettext.ts59
-rw-r--r--gui/src/shared/ipc-helpers.ts60
-rw-r--r--gui/src/shared/ipc-schema.ts (renamed from gui/src/shared/ipc-event-channel.ts)34
-rw-r--r--gui/src/shared/ipc-types.ts10
-rw-r--r--gui/src/shared/notifications/error.ts4
-rw-r--r--gui/src/shared/notifications/no-valid-key.ts2
30 files changed, 272 insertions, 189 deletions
diff --git a/gui/src/main/index.ts b/gui/src/main/index.ts
index cbd56f3843..f3ac7e72c0 100644
--- a/gui/src/main/index.ts
+++ b/gui/src/main/index.ts
@@ -34,11 +34,12 @@ import {
RelaySettingsUpdate,
TunnelState,
} from '../shared/daemon-rpc-types';
-import { loadTranslations, messages } from '../shared/gettext';
+import { messages, relayLocations } from '../shared/gettext';
import { SYSTEM_PREFERRED_LOCALE_KEY } from '../shared/gui-settings-state';
-import { IpcMainEventChannel } from '../shared/ipc-event-channel';
import log, { ConsoleOutput, Logger } from '../shared/logging';
import { LogLevel } from '../shared/logging-types';
+import { IpcMainEventChannel } from './ipc-event-channel';
+import { ICurrentAppVersionInfo } from '../shared/ipc-types';
import {
AccountExpiredNotificationProvider,
CloseToAccountExpiryNotificationProvider,
@@ -65,11 +66,13 @@ import {
IpcInput,
OLD_LOG_FILES,
} from './logging';
+import { loadTranslations } from './load-translations';
import NotificationController from './notification-controller';
import { resolveBin } from './proc';
import ReconnectionBackoff from './reconnection-backoff';
import TrayIconController, { TrayIconType } from './tray-icon-controller';
import WindowController from './window-controller';
+import { ITranslations } from '../shared/ipc-schema';
// Only import when running app on Linux.
const linuxSplitTunneling = process.platform === 'linux' && require('./linux-split-tunneling');
@@ -88,13 +91,6 @@ enum AppQuitStage {
ready,
}
-export interface ICurrentAppVersionInfo {
- gui: string;
- daemon: string;
- isConsistent: boolean;
- isBeta: boolean;
-}
-
type AccountVerification = { status: 'verified' } | { status: 'deferred'; error: Error };
class ApplicationMain {
@@ -204,6 +200,7 @@ class ApplicationMain {
private autoConnectFallbackScheduler = new Scheduler();
private rendererLog?: Logger;
+ private translations: ITranslations = { locale: this.locale };
public run() {
// Remove window animations to combat window flickering when opening window. Can be removed when
@@ -212,6 +209,10 @@ class ApplicationMain {
app.commandLine.appendSwitch('wm-window-animations-disabled');
}
+ if (process.platform !== 'linux') {
+ app.enableSandbox();
+ }
+
this.overrideAppPaths();
if (this.ensureSingleInstance()) {
@@ -373,7 +374,7 @@ class ApplicationMain {
this.blockRequests();
- this.updateCurrentLocale();
+ this.translations = this.updateCurrentLocale();
this.daemonRpc.addConnectionObserver(
new ConnectionObserver(this.onDaemonConnected, this.onDaemonDisconnected),
@@ -1007,6 +1008,9 @@ class ApplicationMain {
upgradeVersion: this.upgradeVersion,
guiSettings: this.guiSettings.state,
wireguardPublicKey: this.wireguardPublicKey,
+ translations: this.translations,
+ platform: process.platform,
+ runningInDevelopment: process.env.NODE_ENV === 'development',
}));
IpcMainEventChannel.settings.handleSetAllowLan((allowLan: boolean) =>
@@ -1074,7 +1078,7 @@ class ApplicationMain {
IpcMainEventChannel.guiSettings.handleSetPreferredLocale((locale: string) => {
this.guiSettings.preferredLocale = locale;
- this.didChangeLocale();
+ return Promise.resolve(this.updateCurrentLocale());
});
IpcMainEventChannel.account.handleCreate(() => this.createNewAccount());
@@ -1368,15 +1372,13 @@ class ApplicationMain {
log.info(`Detected locale: ${this.locale}`);
- loadTranslations(this.locale, messages);
- }
-
- private didChangeLocale() {
- this.updateCurrentLocale();
-
- if (this.windowController) {
- IpcMainEventChannel.locale.notify(this.windowController.webContents, this.locale);
- }
+ const messagesTranslations = loadTranslations(this.locale, messages);
+ const relayLocationsTranslations = loadTranslations(this.locale, relayLocations);
+ return {
+ locale: this.locale,
+ messages: messagesTranslations,
+ relayLocations: relayLocationsTranslations,
+ };
}
// Since the app frontend never performs any network requests, all requests originating from the
@@ -1446,11 +1448,15 @@ class ApplicationMain {
transparent: !this.guiSettings.unpinnedWindow,
useContentSize: true,
webPreferences: {
- nodeIntegration: true,
- devTools: process.env.NODE_ENV === 'development',
- // TODO: Remove use of remote
- enableRemoteModule: true,
+ preload: path.join(__dirname, '../renderer/preloadBundle.js'),
+ nodeIntegration: false,
+ nodeIntegrationInWorker: false,
+ nodeIntegrationInSubFrames: false,
+ enableRemoteModule: false,
+ sandbox: process.platform !== 'linux',
+ contextIsolation: true,
spellcheck: false,
+ devTools: process.env.NODE_ENV === 'development',
},
};
diff --git a/gui/src/main/ipc-event-channel.ts b/gui/src/main/ipc-event-channel.ts
new file mode 100644
index 0000000000..9bd8af1490
--- /dev/null
+++ b/gui/src/main/ipc-event-channel.ts
@@ -0,0 +1,5 @@
+import { ipcMain } from 'electron';
+import { createIpcMain } from '../shared/ipc-helpers';
+import { ipcSchema } from '../shared/ipc-schema';
+
+export const IpcMainEventChannel = createIpcMain(ipcSchema, ipcMain);
diff --git a/gui/src/main/linux-desktop-entry.ts b/gui/src/main/linux-desktop-entry.ts
index dc1ae0e57b..a2712bb7e3 100644
--- a/gui/src/main/linux-desktop-entry.ts
+++ b/gui/src/main/linux-desktop-entry.ts
@@ -1,4 +1,5 @@
import child_process from 'child_process';
+import { nativeImage } from 'electron';
import fs from 'fs';
import path from 'path';
import { ILinuxApplication } from '../shared/application-types';
@@ -45,7 +46,7 @@ export async function getAppIcon(name?: string) {
// Chromium doesn't support .xpm files
const extensions = ['svg', 'png'];
- return findIcon(name, extensions, [
+ const iconPath = await findIcon(name, extensions, [
getIconDirectories(),
await getGtkThemeDirectories(),
// Begin with preferred sized but if nothing matches other sizes should be considered as well.
@@ -53,6 +54,19 @@ export async function getAppIcon(name?: string) {
// Search in all categories of icons.
[/.*/],
]);
+
+ if (iconPath && path.extname(iconPath) === '.svg') {
+ try {
+ const contents = await fs.promises.readFile(iconPath);
+ return `data:image/svg+xml;base64,${contents.toString('base64')}`;
+ } catch (error) {
+ log.error(`Failed to read icon of application: ${name},`, error);
+ }
+ } else if (iconPath) {
+ return nativeImage.createFromPath(iconPath).toDataURL();
+ }
+
+ return undefined;
}
// Implemented according to freedesktop specification.
diff --git a/gui/src/main/load-translations.ts b/gui/src/main/load-translations.ts
new file mode 100644
index 0000000000..63d76f0ac6
--- /dev/null
+++ b/gui/src/main/load-translations.ts
@@ -0,0 +1,72 @@
+import fs from 'fs';
+import { GetTextTranslations, po } from 'gettext-parser';
+import Gettext from 'node-gettext';
+import path from 'path';
+import log from '../shared/logging';
+
+const SOURCE_LANGUAGE = 'en';
+const LOCALES_DIR = path.resolve(__dirname, '../../locales');
+
+export function loadTranslations(
+ currentLocale: string,
+ catalogue: Gettext,
+): GetTextTranslations | undefined {
+ // First look for exact match of the current locale
+ const preferredLocales = [];
+
+ if (currentLocale !== SOURCE_LANGUAGE) {
+ preferredLocales.push(currentLocale);
+ }
+
+ // In case of region bound locale like en-US, fallback to en.
+ const language = Gettext.getLanguageCode(currentLocale);
+ if (currentLocale !== language) {
+ preferredLocales.push(language);
+ }
+
+ const domain = catalogue.domain;
+ for (const locale of preferredLocales) {
+ const parsedTranslations = parseTranslation(locale, domain, catalogue);
+ if (parsedTranslations) {
+ log.info(`Loaded translations ${locale}/${domain}`);
+ catalogue.setLocale(locale);
+ return parsedTranslations;
+ }
+ }
+
+ // Reset the locale to source language if we couldn't load the catalogue for the requested locale
+ // Add empty translations to suppress some of the warnings produces by node-gettext
+ catalogue.addTranslations(SOURCE_LANGUAGE, domain, {});
+ catalogue.setLocale(SOURCE_LANGUAGE);
+ return;
+}
+
+function parseTranslation(
+ locale: string,
+ domain: string,
+ catalogue: Gettext,
+): GetTextTranslations | undefined {
+ const filename = path.join(LOCALES_DIR, locale, `${domain}.po`);
+ let contents: string;
+
+ try {
+ contents = fs.readFileSync(filename, { encoding: 'utf8' });
+ } catch (error) {
+ if (error.code !== 'ENOENT') {
+ log.error(`Cannot read the gettext file "${filename}": ${error.message}`);
+ }
+ return undefined;
+ }
+
+ let translations: GetTextTranslations;
+ try {
+ translations = po.parse(contents);
+ } catch (error) {
+ log.error(`Cannot parse the gettext file "${filename}": ${error.message}`);
+ return undefined;
+ }
+
+ catalogue.addTranslations(locale, domain, translations);
+
+ return translations;
+}
diff --git a/gui/src/main/logging.ts b/gui/src/main/logging.ts
index fb21c54b48..7029625d3f 100644
--- a/gui/src/main/logging.ts
+++ b/gui/src/main/logging.ts
@@ -1,7 +1,7 @@
import { app } from 'electron';
import fs from 'fs';
import path from 'path';
-import { IpcMainEventChannel } from '../shared/ipc-event-channel';
+import { IpcMainEventChannel } from './ipc-event-channel';
import { LogLevel, ILogInput, ILogOutput } from '../shared/logging-types';
export const OLD_LOG_FILES = ['frontend-renderer.log'];
diff --git a/gui/src/main/window-controller.ts b/gui/src/main/window-controller.ts
index a44724d23f..69ae6acb02 100644
--- a/gui/src/main/window-controller.ts
+++ b/gui/src/main/window-controller.ts
@@ -1,15 +1,12 @@
import { BrowserWindow, Display, screen, Tray, WebContents } from 'electron';
-import { IpcMainEventChannel } from '../shared/ipc-event-channel';
+import { IpcMainEventChannel } from './ipc-event-channel';
+import { IWindowShapeParameters } from '../shared/ipc-types';
interface IPosition {
x: number;
y: number;
}
-export interface IWindowShapeParameters {
- arrowPosition?: number;
-}
-
interface IWindowPositioning {
getPosition(window: BrowserWindow): IPosition;
getWindowShapeParameters(window: BrowserWindow): IWindowShapeParameters;
diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx
index f63e03633a..9ff1459322 100644
--- a/gui/src/renderer/app.tsx
+++ b/gui/src/renderer/app.tsx
@@ -15,14 +15,15 @@ import configureStore from './redux/store';
import userInterfaceActions from './redux/userinterface/actions';
import versionActions from './redux/version/actions';
-import { ICurrentAppVersionInfo } from '../main';
-import { loadTranslations, messages, relayLocations } from '../shared/gettext';
-import { IGuiSettingsState, SYSTEM_PREFERRED_LOCALE_KEY } from '../shared/gui-settings-state';
-import { IpcRendererEventChannel, IRelayListPair } from '../shared/ipc-event-channel';
+import { ICurrentAppVersionInfo } from '../shared/ipc-types';
import { ILinuxSplitTunnelingApplication } from '../shared/application-types';
+import { messages, relayLocations } from '../shared/gettext';
+import { IGuiSettingsState, SYSTEM_PREFERRED_LOCALE_KEY } from '../shared/gui-settings-state';
import log, { ConsoleOutput } from '../shared/logging';
+import { IRelayListPair } from '../shared/ipc-schema';
import consumePromise from '../shared/promise';
import History from './lib/history';
+import { loadTranslations } from './lib/load-translations';
import {
AccountToken,
@@ -45,6 +46,8 @@ import {
import { LogLevel } from '../shared/logging-types';
import IpcOutput from './lib/logging';
+const IpcRendererEventChannel = window.ipc;
+
interface IPreferredLocaleDescriptor {
name: string;
code: string;
@@ -98,20 +101,6 @@ export default class AppRenderer {
log.addOutput(new ConsoleOutput(LogLevel.debug));
log.addOutput(new IpcOutput(LogLevel.debug));
- IpcRendererEventChannel.locale.listen((locale) => {
- // load translations for the new locale
- this.loadTranslations(locale);
-
- // set current locale
- this.setLocale(locale);
-
- // refresh the relay list pair with the new translations
- this.propagateRelayListPairToRedux();
-
- // refresh the location with the new translations
- this.propagateLocationToRedux();
- });
-
IpcRendererEventChannel.windowShape.listen((windowShapeParams) => {
if (typeof windowShapeParams.arrowPosition === 'number') {
this.reduxActions.userInterface.updateWindowArrowPosition(windowShapeParams.arrowPosition);
@@ -186,9 +175,20 @@ export default class AppRenderer {
// Request the initial state from the main process
const initialState = IpcRendererEventChannel.state.get();
- // Load translations
- this.loadTranslations(initialState.locale);
+ window.platform = initialState.platform;
+ window.runningInDevelopment = initialState.runningInDevelopment;
+
this.setLocale(initialState.locale);
+ loadTranslations(
+ messages,
+ initialState.translations.locale,
+ initialState.translations.messages,
+ );
+ loadTranslations(
+ relayLocations,
+ initialState.translations.locale,
+ initialState.translations.relayLocations,
+ );
this.setAccountExpiry(initialState.accountData && initialState.accountData.expiry);
this.handleAccountChange(undefined, initialState.settings.accountToken);
@@ -464,8 +464,23 @@ export default class AppRenderer {
];
}
- public setPreferredLocale(preferredLocale: string) {
- IpcRendererEventChannel.guiSettings.setPreferredLocale(preferredLocale);
+ public async setPreferredLocale(preferredLocale: string): Promise<void> {
+ const translations = await IpcRendererEventChannel.guiSettings.setPreferredLocale(
+ preferredLocale,
+ );
+
+ // set current locale
+ this.setLocale(translations.locale);
+
+ // load translations for new locale
+ loadTranslations(messages, translations.locale, translations.messages);
+ loadTranslations(relayLocations, translations.locale, translations.relayLocations);
+
+ // refresh the relay list pair with the new translations
+ this.propagateRelayListPairToRedux();
+
+ // refresh the location with the new translations
+ this.propagateLocationToRedux();
}
public getPreferredLocaleDisplayName(localeCode: string): string {
@@ -479,12 +494,6 @@ export default class AppRenderer {
this.loginTimer = global.setTimeout(() => this.history.resetWith('/connect'), 1000);
}
- private loadTranslations(locale: string) {
- for (const catalogue of [messages, relayLocations]) {
- loadTranslations(locale, catalogue);
- }
- }
-
private setLocale(locale: string) {
this.locale = locale;
this.reduxActions.userInterface.updateLocale(locale);
@@ -576,7 +585,7 @@ export default class AppRenderer {
}
private async autoConnect() {
- if (process.env.NODE_ENV === 'development') {
+ if (window.runningInDevelopment) {
log.info('Skip autoconnect in development');
} else if (this.autoConnected) {
log.info('Skip autoconnect because it was done before');
diff --git a/gui/src/renderer/components/AdvancedSettings.tsx b/gui/src/renderer/components/AdvancedSettings.tsx
index 6633e35746..7c97f0d844 100644
--- a/gui/src/renderer/components/AdvancedSettings.tsx
+++ b/gui/src/renderer/components/AdvancedSettings.tsx
@@ -429,7 +429,7 @@ export default class AdvancedSettings extends React.Component<IProps, IState> {
<Cell.Icon height={12} width={7} source="icon-chevron" />
</Cell.CellButton>
- {process.platform === 'linux' && (
+ {window.platform === 'linux' && (
<Cell.CellButton onClick={this.props.onViewLinuxSplitTunneling}>
<Cell.Label>
{messages.pgettext('advanced-settings-view', 'Split tunneling')}
diff --git a/gui/src/renderer/components/CustomScrollbars.tsx b/gui/src/renderer/components/CustomScrollbars.tsx
index bc56ffcf3c..87c5b82dce 100644
--- a/gui/src/renderer/components/CustomScrollbars.tsx
+++ b/gui/src/renderer/components/CustomScrollbars.tsx
@@ -47,7 +47,7 @@ interface IScrollbarUpdateContext {
export default class CustomScrollbars extends React.Component<IProps, IState> {
public static defaultProps: IProps = {
// auto-hide on macOS by default
- autoHide: process.platform === 'darwin',
+ autoHide: window.platform === 'darwin',
trackPadding: { x: 2, y: 2 },
};
diff --git a/gui/src/renderer/components/Focus.tsx b/gui/src/renderer/components/Focus.tsx
index 6a2e9c7126..85c2c307e4 100644
--- a/gui/src/renderer/components/Focus.tsx
+++ b/gui/src/renderer/components/Focus.tsx
@@ -1,4 +1,3 @@
-import path from 'path';
import React, { useImperativeHandle, useState } from 'react';
import { useLocation } from 'react-router';
import { sprintf } from 'sprintf-js';
@@ -29,7 +28,7 @@ function Focus(props: IFocusProps, ref: React.Ref<IFocusHandle>) {
ref,
() => ({
resetFocus: () => {
- const pageName = path.basename(location.pathname);
+ const pageName = location.pathname.slice(location.pathname.lastIndexOf('/') + 1);
const titleElement = document.getElementsByTagName('h1')[0];
const titleContent = titleElement?.textContent ?? pageName;
setTitle(titleContent);
diff --git a/gui/src/renderer/components/HeaderBar.tsx b/gui/src/renderer/components/HeaderBar.tsx
index 67b9622b21..bdf7d51924 100644
--- a/gui/src/renderer/components/HeaderBar.tsx
+++ b/gui/src/renderer/components/HeaderBar.tsx
@@ -28,7 +28,7 @@ interface IHeaderBarContainerProps {
const HeaderBarContainer = styled.header({}, (props: IHeaderBarContainerProps) => ({
padding: '12px 16px',
- paddingTop: process.platform === 'darwin' && !props.unpinnedWindow ? '24px' : '12px',
+ paddingTop: window.platform === 'darwin' && !props.unpinnedWindow ? '24px' : '12px',
backgroundColor: headerBarStyleColorMap[props.barStyle ?? HeaderBarStyle.default],
}));
diff --git a/gui/src/renderer/components/ImageView.tsx b/gui/src/renderer/components/ImageView.tsx
index fd6588b1fb..3be04159a2 100644
--- a/gui/src/renderer/components/ImageView.tsx
+++ b/gui/src/renderer/components/ImageView.tsx
@@ -1,4 +1,3 @@
-import path from 'path';
import React, { useMemo } from 'react';
import styled from 'styled-components';
@@ -40,7 +39,7 @@ const ImageMask = styled.div((props: IImageMaskProps) => {
const HiddenImage = styled.img({ visibility: 'hidden' });
export default function ImageView(props: IImageViewProps) {
- const url = path.isAbsolute(props.source)
+ const url = props.source.startsWith('data:')
? props.source
: `../../assets/images/${props.source}.svg`;
diff --git a/gui/src/renderer/components/NavigationBarStyles.tsx b/gui/src/renderer/components/NavigationBarStyles.tsx
index 98c5184ea3..a82256b077 100644
--- a/gui/src/renderer/components/NavigationBarStyles.tsx
+++ b/gui/src/renderer/components/NavigationBarStyles.tsx
@@ -20,7 +20,7 @@ export const StyledNavigationItems = styled.div({
export const StyledNavigationBar = styled.nav((props: { unpinnedWindow: boolean }) => ({
flex: 0,
padding: '12px',
- paddingTop: process.platform === 'darwin' && !props.unpinnedWindow ? '24px' : '12px',
+ paddingTop: window.platform === 'darwin' && !props.unpinnedWindow ? '24px' : '12px',
}));
export const StyledNavigationBarWrapper = styled.div({
diff --git a/gui/src/renderer/components/PlatformWindow.tsx b/gui/src/renderer/components/PlatformWindow.tsx
index 633b7aee95..83aa7b3f7f 100644
--- a/gui/src/renderer/components/PlatformWindow.tsx
+++ b/gui/src/renderer/components/PlatformWindow.tsx
@@ -10,7 +10,7 @@ interface IPlatformWindowProps {
export default styled.div({}, (props: IPlatformWindowProps) => {
let mask: string | undefined;
- if (process.platform === 'darwin' && !props.unpinnedWindow) {
+ if (window.platform === 'darwin' && !props.unpinnedWindow) {
const arrowPositionCss =
props.arrowPosition !== undefined ? `${props.arrowPosition - ARROW_WIDTH * 0.5}px` : '50%';
diff --git a/gui/src/renderer/components/Preferences.tsx b/gui/src/renderer/components/Preferences.tsx
index ab2b423686..d47a7315c6 100644
--- a/gui/src/renderer/components/Preferences.tsx
+++ b/gui/src/renderer/components/Preferences.tsx
@@ -179,8 +179,8 @@ export default class Preferences extends React.Component<IProps> {
</Cell.Footer>
</AriaInputGroup>
- {(process.platform === 'win32' ||
- (process.platform === 'darwin' && process.env.NODE_ENV === 'development')) && (
+ {(window.platform === 'win32' ||
+ (window.platform === 'darwin' && window.runningInDevelopment)) && (
<AriaInputGroup>
<Cell.Container>
<AriaLabel>
diff --git a/gui/src/renderer/containers/SelectLanguagePage.tsx b/gui/src/renderer/containers/SelectLanguagePage.tsx
index eb052fb8a6..5c073c22e1 100644
--- a/gui/src/renderer/containers/SelectLanguagePage.tsx
+++ b/gui/src/renderer/containers/SelectLanguagePage.tsx
@@ -11,8 +11,8 @@ const mapStateToProps = (state: IReduxState) => ({
const mapDispatchToProps = (_dispatch: ReduxDispatch, props: RouteComponentProps & IAppContext) => {
return {
preferredLocalesList: props.app.getPreferredLocaleList(),
- setPreferredLocale(locale: string) {
- props.app.setPreferredLocale(locale);
+ async setPreferredLocale(locale: string) {
+ await props.app.setPreferredLocale(locale);
props.history.goBack();
},
onClose() {
diff --git a/gui/src/renderer/context.tsx b/gui/src/renderer/context.tsx
index 113a29fe71..d4b20daeb1 100644
--- a/gui/src/renderer/context.tsx
+++ b/gui/src/renderer/context.tsx
@@ -6,7 +6,7 @@ export interface IAppContext {
}
export const AppContext = React.createContext<IAppContext | undefined>(undefined);
-if (process.env.NODE_ENV === 'development') {
+if (window.runningInDevelopment) {
AppContext.displayName = 'AppContext';
}
@@ -34,7 +34,7 @@ export default function withAppContext<Props>(BaseComponent: React.ComponentType
);
};
- if (process.env.NODE_ENV === 'development') {
+ if (window.runningInDevelopment) {
wrappedComponent.displayName =
'withAppContext(' + (BaseComponent.displayName || BaseComponent.name) + ')';
}
diff --git a/gui/src/renderer/index.html b/gui/src/renderer/index.html
index 3f2da3d633..043955b3c5 100644
--- a/gui/src/renderer/index.html
+++ b/gui/src/renderer/index.html
@@ -4,10 +4,11 @@
<title>Mullvad VPN</title>
<link rel="stylesheet" href="../../assets/css/style.css" />
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline'">
+ <meta charset="UTF-8">
</head>
<body>
<div id="app"></div>
<script>var exports = {};</script>
- <script src="./index.js"></script>
+ <script src="./bundle.js"></script>
</body>
</html>
diff --git a/gui/src/renderer/lib/ipc-event-channel.ts b/gui/src/renderer/lib/ipc-event-channel.ts
new file mode 100644
index 0000000000..bcb816e918
--- /dev/null
+++ b/gui/src/renderer/lib/ipc-event-channel.ts
@@ -0,0 +1,5 @@
+import { ipcRenderer } from 'electron';
+import { createIpcRenderer } from '../../shared/ipc-helpers';
+import { ipcSchema } from '../../shared/ipc-schema';
+
+export const IpcRendererEventChannel = createIpcRenderer(ipcSchema, ipcRenderer);
diff --git a/gui/src/renderer/lib/load-translations.ts b/gui/src/renderer/lib/load-translations.ts
new file mode 100644
index 0000000000..43a483de25
--- /dev/null
+++ b/gui/src/renderer/lib/load-translations.ts
@@ -0,0 +1,22 @@
+import { GetTextTranslations } from 'gettext-parser';
+import Gettext from 'node-gettext';
+import log from '../../shared/logging';
+
+const SOURCE_LANGUAGE = 'en';
+
+export function loadTranslations(
+ catalogue: Gettext,
+ locale: string,
+ translations?: GetTextTranslations,
+) {
+ if (translations) {
+ catalogue.addTranslations(locale, catalogue.domain, translations);
+ catalogue.setLocale(locale);
+ log.info(`Loaded translations ${locale}/${catalogue.domain}`);
+ } else {
+ // Reset the locale to source language if we couldn't load the catalogue for the requested locale
+ // Add empty translations to suppress some of the warnings produces by node-gettext
+ catalogue.addTranslations(SOURCE_LANGUAGE, catalogue.domain, {});
+ catalogue.setLocale(SOURCE_LANGUAGE);
+ }
+}
diff --git a/gui/src/renderer/lib/logging.ts b/gui/src/renderer/lib/logging.ts
index 433ba20f8f..7c8dc9e542 100644
--- a/gui/src/renderer/lib/logging.ts
+++ b/gui/src/renderer/lib/logging.ts
@@ -1,10 +1,9 @@
-import { IpcRendererEventChannel } from '../../shared/ipc-event-channel';
import { ILogOutput, LogLevel } from '../../shared/logging-types';
export default class IpcOutput implements ILogOutput {
constructor(public level: LogLevel) {}
public write(level: LogLevel, message: string) {
- IpcRendererEventChannel.logging.log({ level: level, message });
+ window.ipc.logging.log({ level: level, message });
}
}
diff --git a/gui/src/renderer/preload.ts b/gui/src/renderer/preload.ts
new file mode 100644
index 0000000000..5c44b71eeb
--- /dev/null
+++ b/gui/src/renderer/preload.ts
@@ -0,0 +1,4 @@
+import { contextBridge } from 'electron';
+import { IpcRendererEventChannel } from './lib/ipc-event-channel';
+
+contextBridge.exposeInMainWorld('ipc', IpcRendererEventChannel);
diff --git a/gui/src/renderer/redux/settings/reducers.ts b/gui/src/renderer/redux/settings/reducers.ts
index 59b4c32b46..592caab687 100644
--- a/gui/src/renderer/redux/settings/reducers.ts
+++ b/gui/src/renderer/redux/settings/reducers.ts
@@ -148,7 +148,7 @@ const initialState: ISettingsReduxState = {
autoConnect: true,
monochromaticIcon: false,
startMinimized: false,
- unpinnedWindow: process.platform !== 'win32' && process.platform !== 'darwin',
+ unpinnedWindow: window.platform !== 'win32' && window.platform !== 'darwin',
},
relaySettings: {
normal: {
diff --git a/gui/src/renderer/redux/store.ts b/gui/src/renderer/redux/store.ts
index 49f864d18b..92d85e4dbe 100644
--- a/gui/src/renderer/redux/store.ts
+++ b/gui/src/renderer/redux/store.ts
@@ -54,7 +54,7 @@ export default function configureStore(initialState?: IReduxState) {
const composeEnhancers: typeof compose = (() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const reduxCompose = window && (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__;
- if (process.env.NODE_ENV === 'development' && reduxCompose) {
+ if (window.runningInDevelopment && reduxCompose) {
return reduxCompose({ actionCreators });
}
return compose;
diff --git a/gui/src/shared/gettext.ts b/gui/src/shared/gettext.ts
index 3283269c70..0416e8b1ce 100644
--- a/gui/src/shared/gettext.ts
+++ b/gui/src/shared/gettext.ts
@@ -1,67 +1,8 @@
-import fs from 'fs';
-import { po } from 'gettext-parser';
import Gettext from 'node-gettext';
-import path from 'path';
import { LocalizationContexts } from './localization-contexts';
import log from './logging';
const SOURCE_LANGUAGE = 'en';
-const LOCALES_DIR = path.resolve(__dirname, '../../locales');
-
-export function loadTranslations(currentLocale: string, catalogue: Gettext) {
- // First look for exact match of the current locale
- const preferredLocales = [];
-
- if (currentLocale !== SOURCE_LANGUAGE) {
- preferredLocales.push(currentLocale);
- }
-
- // In case of region bound locale like en-US, fallback to en.
- const language = Gettext.getLanguageCode(currentLocale);
- if (currentLocale !== language) {
- preferredLocales.push(language);
- }
-
- const domain = catalogue.domain;
- for (const locale of preferredLocales) {
- if (parseTranslation(locale, domain, catalogue)) {
- log.info(`Loaded translations ${locale}/${domain}`);
- catalogue.setLocale(locale);
- return;
- }
- }
-
- // Reset the locale to source language if we couldn't load the catalogue for the requested locale
- // Add empty translations to suppress some of the warnings produces by node-gettext
- catalogue.addTranslations(SOURCE_LANGUAGE, domain, {});
- catalogue.setLocale(SOURCE_LANGUAGE);
-}
-
-function parseTranslation(locale: string, domain: string, catalogue: Gettext): boolean {
- const filename = path.join(LOCALES_DIR, locale, `${domain}.po`);
- let contents: string;
-
- try {
- contents = fs.readFileSync(filename, { encoding: 'utf8' });
- } catch (error) {
- if (error.code !== 'ENOENT') {
- log.error(`Cannot read the gettext file "${filename}": ${error.message}`);
- }
- return false;
- }
-
- let translations: ReturnType<typeof po.parse>;
- try {
- translations = po.parse(contents);
- } catch (error) {
- log.error(`Cannot parse the gettext file "${filename}": ${error.message}`);
- return false;
- }
-
- catalogue.addTranslations(locale, domain, translations);
-
- return true;
-}
function setErrorHandler(catalogue: Gettext) {
catalogue.on('error', (error) => {
diff --git a/gui/src/shared/ipc-helpers.ts b/gui/src/shared/ipc-helpers.ts
index d1a72a2df6..59eee46459 100644
--- a/gui/src/shared/ipc-helpers.ts
+++ b/gui/src/shared/ipc-helpers.ts
@@ -1,4 +1,4 @@
-import { ipcMain, ipcRenderer, WebContents } from 'electron';
+import { IpcMain as EIpcMain, IpcRenderer as EIpcRenderer, WebContents } from 'electron';
import { capitalize } from './string-helpers';
import log from './logging';
@@ -7,26 +7,21 @@ type Sender<T, R> = (arg: T) => R;
type Notifier<T> = (webContents: WebContents, arg: T) => void;
type Listener<T> = (callback: (arg: T) => void) => void;
-interface IpcCall<T, R> {
- direction: 'renderer-to-main' | 'main-to-renderer';
- send: (event: string) => Notifier<T> | Sender<T, R>;
- receive: (event: string) => Listener<T> | Handler<T, R>;
-}
-
-interface MainToRenderer<T> extends IpcCall<T, never> {
+interface MainToRenderer<T> {
direction: 'main-to-renderer';
- send: (event: string) => Notifier<T>;
- receive: (event: string) => Listener<T>;
+ send: (event: string, ipcMain: EIpcMain) => Notifier<T>;
+ receive: (event: string, ipcRenderer: EIpcRenderer) => Listener<T>;
}
-interface RendererToMain<T, R> extends IpcCall<T, R> {
+interface RendererToMain<T, R> {
direction: 'renderer-to-main';
- send: (event: string) => Sender<T, R>;
- receive: (event: string) => Handler<T, R>;
+ send: (event: string, ipcRenderer: EIpcRenderer) => Sender<T, R>;
+ receive: (event: string, ipcMain: EIpcMain) => Handler<T, R>;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
-type AnyIpcCall = IpcCall<any, any>;
+type AnyIpcCall = MainToRenderer<any> | RendererToMain<any, any>;
+
type Schema = Record<string, Record<string, AnyIpcCall>>;
// Renames all IPC calls, e.g. `callName` to either `notifyCallName` or `handleCallName` depending
@@ -66,22 +61,31 @@ type IpcRenderer<S extends Schema> = {
};
// Preforms the transformation of the main event channel in accordance with the above types.
-export function createIpcMain<S extends Schema>(ipc: S): IpcMain<S> {
- return createIpc(ipc, (event, key, spec) => {
+export function createIpcMain<S extends Schema>(schema: S, ipcMain: EIpcMain): IpcMain<S> {
+ return createIpc(schema, (event, key, spec) => {
const capitalizedKey = capitalize(key);
const newKey =
spec.direction === 'main-to-renderer' ? `notify${capitalizedKey}` : `handle${capitalizedKey}`;
- const newValue = spec.direction === 'main-to-renderer' ? spec.send(event) : spec.receive(event);
+ const newValue =
+ spec.direction === 'main-to-renderer'
+ ? spec.send(event, ipcMain)
+ : spec.receive(event, ipcMain);
return [newKey, newValue];
});
}
// Preforms the transformation of the renderer event channel in accordance with the above types.
-export function createIpcRenderer<S extends Schema>(ipc: S): IpcRenderer<S> {
- return createIpc(ipc, (event, key, spec) => {
+export function createIpcRenderer<S extends Schema>(
+ schema: S,
+ ipcRenderer: EIpcRenderer,
+): IpcRenderer<S> {
+ return createIpc(schema, (event, key, spec) => {
const newKey = spec.direction === 'main-to-renderer' ? `listen${capitalize(key)}` : key;
- const newValue = spec.direction === 'main-to-renderer' ? spec.receive(event) : spec.send(event);
+ const newValue =
+ spec.direction === 'main-to-renderer'
+ ? spec.receive(event, ipcRenderer)
+ : spec.send(event, ipcRenderer);
return [newKey, newValue];
});
@@ -105,8 +109,8 @@ function createIpc<S extends Schema, T, R extends IpcMain<S> | IpcRenderer<S>>(
export function send<T>(): RendererToMain<T, void> {
return {
direction: 'renderer-to-main',
- send: (event: string) => (newValue: T) => ipcRenderer.send(event, newValue),
- receive: (event: string) => (handlerFn: (value: T) => void) => {
+ send: (event, ipcRenderer) => (newValue: T) => ipcRenderer.send(event, newValue),
+ receive: (event, ipcMain) => (handlerFn: (value: T) => void) => {
ipcMain.on(event, (_event, newValue: T) => {
handlerFn(newValue);
});
@@ -118,8 +122,8 @@ export function send<T>(): RendererToMain<T, void> {
export function invokeSync<T, R>(): RendererToMain<T, R> {
return {
direction: 'renderer-to-main',
- send: (event: string) => (newValue: T) => ipcRenderer.sendSync(event, newValue),
- receive: (event: string) => (handlerFn: (value: T) => R) => {
+ send: (event, ipcRenderer) => (newValue: T) => ipcRenderer.sendSync(event, newValue),
+ receive: (event, ipcMain) => (handlerFn: (value: T) => R) => {
ipcMain.on(event, (ipcEvent, newValue: T) => {
ipcEvent.returnValue = handlerFn(newValue);
});
@@ -141,13 +145,13 @@ export function notifyRenderer<T>(): MainToRenderer<T> {
return {
direction: 'main-to-renderer',
send: notifyRendererImpl,
- receive: (event: string) => (fn: (value: T) => void) => {
+ receive: (event, ipcRenderer) => (fn: (value: T) => void) => {
ipcRenderer.on(event, (_event, newState: T) => fn(newState));
},
};
}
-function notifyRendererImpl<T>(event: string): Notifier<T> {
+function notifyRendererImpl<T>(event: string, _ipcMain: EIpcMain): Notifier<T> {
return (webContents: WebContents, value: T) => {
if (webContents.isDestroyed()) {
log.error(`sender(${event}): webContents is already destroyed!`);
@@ -159,7 +163,7 @@ function notifyRendererImpl<T>(event: string): Notifier<T> {
type RequestResult<T> = { type: 'success'; value: T } | { type: 'error'; message: string };
-function invokeImpl<T, R>(event: string): Sender<T, Promise<R>> {
+function invokeImpl<T, R>(event: string, ipcRenderer: EIpcRenderer): Sender<T, Promise<R>> {
return async (arg: T): Promise<R> => {
const result: RequestResult<R> = await ipcRenderer.invoke(event, arg);
switch (result.type) {
@@ -171,7 +175,7 @@ function invokeImpl<T, R>(event: string): Sender<T, Promise<R>> {
};
}
-function handle<T, R>(event: string): Handler<T, Promise<R>> {
+function handle<T, R>(event: string, ipcMain: EIpcMain): Handler<T, Promise<R>> {
return (fn: (arg: T) => Promise<R>) => {
ipcMain.handle(event, async (_ipcEvent, arg: T) => {
try {
diff --git a/gui/src/shared/ipc-event-channel.ts b/gui/src/shared/ipc-schema.ts
index 19c11f85ab..a31745c01f 100644
--- a/gui/src/shared/ipc-event-channel.ts
+++ b/gui/src/shared/ipc-schema.ts
@@ -1,6 +1,5 @@
-import { ICurrentAppVersionInfo } from '../main/index';
-import { IWindowShapeParameters } from '../main/window-controller';
-import { ILinuxSplitTunnelingApplication } from '../shared/application-types';
+import { GetTextTranslations } from 'gettext-parser';
+import { ILinuxSplitTunnelingApplication } from './application-types';
import {
AccountToken,
BridgeSettings,
@@ -18,20 +17,20 @@ import {
VoucherResponse,
} from './daemon-rpc-types';
import { IGuiSettingsState } from './gui-settings-state';
-import {
- createIpcMain,
- createIpcRenderer,
- invoke,
- invokeSync,
- notifyRenderer,
- send,
-} from './ipc-helpers';
import { LogLevel } from './logging-types';
interface ILogEntry {
level: LogLevel;
message: string;
}
+import { invoke, invokeSync, notifyRenderer, send } from './ipc-helpers';
+import { ICurrentAppVersionInfo, IWindowShapeParameters } from './ipc-types';
+
+export interface ITranslations {
+ locale: string;
+ messages?: GetTextTranslations;
+ relayLocations?: GetTextTranslations;
+}
export interface IRelayListPair {
relays: IRelayList;
@@ -52,6 +51,9 @@ export interface IAppStateSnapshot {
upgradeVersion: IAppVersionInfo;
guiSettings: IGuiSettingsState;
wireguardPublicKey?: IWireguardPublicKey;
+ translations: ITranslations;
+ platform: NodeJS.Platform;
+ runningInDevelopment: boolean;
}
// The different types of requests are:
@@ -93,13 +95,10 @@ export interface IAppStateSnapshot {
// listenFourth: (fn: (arg: boolean) => void) => void,
// },
// }
-const ipc = {
+export const ipcSchema = {
state: {
get: invokeSync<void, IAppStateSnapshot>(),
},
- locale: {
- '': notifyRenderer<string>(),
- },
windowShape: {
'': notifyRenderer<IWindowShapeParameters>(),
},
@@ -155,7 +154,7 @@ const ipc = {
setAutoConnect: send<boolean>(),
setStartMinimized: send<boolean>(),
setMonochromaticIcon: send<boolean>(),
- setPreferredLocale: send<string>(),
+ setPreferredLocale: invoke<string, ITranslations>(),
setUnpinnedWindow: send<boolean>(),
},
account: {
@@ -192,6 +191,3 @@ const ipc = {
log: send<ILogEntry>(),
},
};
-
-export const IpcMainEventChannel = createIpcMain(ipc);
-export const IpcRendererEventChannel = createIpcRenderer(ipc);
diff --git a/gui/src/shared/ipc-types.ts b/gui/src/shared/ipc-types.ts
new file mode 100644
index 0000000000..7781551ed5
--- /dev/null
+++ b/gui/src/shared/ipc-types.ts
@@ -0,0 +1,10 @@
+export interface ICurrentAppVersionInfo {
+ gui: string;
+ daemon: string;
+ isConsistent: boolean;
+ isBeta: boolean;
+}
+
+export interface IWindowShapeParameters {
+ arrowPosition?: number;
+}
diff --git a/gui/src/shared/notifications/error.ts b/gui/src/shared/notifications/error.ts
index d580bf1f1e..c5f15482dc 100644
--- a/gui/src/shared/notifications/error.ts
+++ b/gui/src/shared/notifications/error.ts
@@ -45,7 +45,7 @@ export class ErrorNotificationProvider
function getMessage(errorDetails: IErrorState, accountExpiry?: string): string {
if (errorDetails.blockFailure) {
if (errorDetails.cause.reason === 'set_firewall_policy_error') {
- switch (process.platform) {
+ switch (process.platform ?? window.platform) {
case 'win32':
return messages.pgettext(
'notifications',
@@ -86,7 +86,7 @@ function getMessage(errorDetails: IErrorState, accountExpiry?: string): string {
'Could not configure IPv6. Disable it in the app or enable it on your device.',
);
case 'set_firewall_policy_error':
- switch (process.platform) {
+ switch (process.platform ?? window.platform) {
case 'win32':
return messages.pgettext(
'notifications',
diff --git a/gui/src/shared/notifications/no-valid-key.ts b/gui/src/shared/notifications/no-valid-key.ts
index d1b90095e2..8da145a1f0 100644
--- a/gui/src/shared/notifications/no-valid-key.ts
+++ b/gui/src/shared/notifications/no-valid-key.ts
@@ -14,7 +14,7 @@ export class NoValidKeyNotificationProvider implements InAppNotificationProvider
public mayDisplay() {
const usingWireGuard =
this.context.tunnelProtocol === 'wireguard' ||
- (this.context.tunnelProtocol === 'any' && process.platform !== 'win32');
+ (this.context.tunnelProtocol === 'any' && (process.platform ?? window.platform) !== 'win32');
const keyInvalid =
this.context.wireGuardKey.type === 'key-not-set' ||
this.context.wireGuardKey.type === 'too-many-keys' ||