diff options
Diffstat (limited to 'gui/src')
20 files changed, 226 insertions, 183 deletions
diff --git a/gui/src/main/index.ts b/gui/src/main/index.ts index 2d048ab2c3..67bb326465 100644 --- a/gui/src/main/index.ts +++ b/gui/src/main/index.ts @@ -17,7 +17,7 @@ import { RelaySettingsUpdate, TunnelStateTransition, } from '../shared/daemon-rpc-types'; -import { loadTranslations } from '../shared/gettext'; +import { loadTranslations, messages } from '../shared/gettext'; import { IpcMainEventChannel } from '../shared/ipc-event-channel'; import { getOpenAtLogin, setOpenAtLogin } from './autostart'; import { ConnectionObserver, DaemonRpc, SubscriptionListener } from './daemon-rpc'; @@ -282,7 +282,7 @@ class ApplicationMain { log.info(`Detected locale: ${this.locale}`); - loadTranslations(this.locale); + loadTranslations(this.locale, messages); this.daemonRpc.addConnectionObserver( new ConnectionObserver(this.onDaemonConnected, this.onDaemonDisconnected), diff --git a/gui/src/main/notification-controller.ts b/gui/src/main/notification-controller.ts index 35314aa73a..a2e1e00a0b 100644 --- a/gui/src/main/notification-controller.ts +++ b/gui/src/main/notification-controller.ts @@ -3,7 +3,7 @@ import path from 'path'; import { sprintf } from 'sprintf-js'; import config from '../config.json'; import { TunnelStateTransition } from '../shared/daemon-rpc-types'; -import { pgettext } from '../shared/gettext'; +import { messages } from '../shared/gettext'; export default class NotificationController { private lastTunnelStateAnnouncement?: { body: string; notification: Notification }; @@ -26,24 +26,26 @@ export default class NotificationController { switch (tunnelState.state) { case 'connecting': if (!this.reconnecting) { - this.showTunnelStateNotification(pgettext('notifications', 'Connecting')); + this.showTunnelStateNotification(messages.pgettext('notifications', 'Connecting')); } break; case 'connected': - this.showTunnelStateNotification(pgettext('notifications', 'Secured')); + this.showTunnelStateNotification(messages.pgettext('notifications', 'Secured')); break; case 'disconnected': - this.showTunnelStateNotification(pgettext('notifications', 'Unsecured')); + this.showTunnelStateNotification(messages.pgettext('notifications', 'Unsecured')); break; case 'blocked': switch (tunnelState.details.reason) { case 'set_firewall_policy_error': this.showTunnelStateNotification( - pgettext('notifications', 'Critical failure - Unsecured'), + messages.pgettext('notifications', 'Critical failure - Unsecured'), ); break; default: - this.showTunnelStateNotification(pgettext('notifications', 'Blocked all connections')); + this.showTunnelStateNotification( + messages.pgettext('notifications', 'Blocked all connections'), + ); break; } break; @@ -54,7 +56,7 @@ export default class NotificationController { // no-op break; case 'reconnect': - this.showTunnelStateNotification(pgettext('notifications', 'Reconnecting')); + this.showTunnelStateNotification(messages.pgettext('notifications', 'Reconnecting')); this.reconnecting = true; return; } @@ -68,7 +70,7 @@ export default class NotificationController { this.presentNotificationOnce('inconsistent-version', () => { const notification = new Notification({ title: this.notificationTitle, - body: pgettext( + body: messages.pgettext( 'notifications', 'Inconsistent internal version information, please restart the app', ), @@ -87,7 +89,7 @@ export default class NotificationController { // TRANSLATORS: The system notification displayed to the user when the running app becomes unsupported. // TRANSLATORS: Available placeholder: // TRANSLATORS: %(version) - the newest available version of the app - pgettext( + messages.pgettext( 'notifications', 'You are running an unsupported app version. Please upgrade to %(version)s now to ensure your security', ), diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx index 54b2743467..96d0b3e43d 100644 --- a/gui/src/renderer/app.tsx +++ b/gui/src/renderer/app.tsx @@ -22,7 +22,7 @@ import versionActions from './redux/version/actions'; import { IAppUpgradeInfo, ICurrentAppVersionInfo } from '../main'; import { IWindowShapeParameters } from '../main/window-controller'; -import { loadTranslations } from '../shared/gettext'; +import { cities, countries, loadTranslations, messages, relayLocations } from '../shared/gettext'; import { IGuiSettingsState } from '../shared/gui-settings-state'; import { IpcRendererEventChannel } from '../shared/ipc-event-channel'; import AccountDataCache, { AccountFetchRetryAction } from './lib/account-data-cache'; @@ -176,7 +176,9 @@ export default class AppRenderer { webFrame.setVisualZoomLevelLimits(1, 1); // Load translations - loadTranslations(this.locale); + for (const catalogue of [messages, countries, cities, relayLocations]) { + loadTranslations(this.locale, catalogue); + } } public renderView() { diff --git a/gui/src/renderer/components/Account.tsx b/gui/src/renderer/components/Account.tsx index 90de402269..abde15b818 100644 --- a/gui/src/renderer/components/Account.tsx +++ b/gui/src/renderer/components/Account.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { Component, Text, View } from 'reactxp'; -import { pgettext } from '../../shared/gettext'; +import { messages } from '../../shared/gettext'; import AccountExpiry from '../lib/account-expiry'; import styles from './AccountStyles'; import * as AppButton from './AppButton'; @@ -30,31 +30,31 @@ export default class Account extends Component<IProps> { <NavigationBar> <BackBarItem action={this.props.onClose}> {// TRANSLATORS: Back button in navigation bar - pgettext('account-nav', 'Settings')} + messages.pgettext('account-nav', 'Settings')} </BackBarItem> </NavigationBar> <View style={styles.account__container}> <SettingsHeader> - <HeaderTitle>{pgettext('account-view', 'Account')}</HeaderTitle> + <HeaderTitle>{messages.pgettext('account-view', 'Account')}</HeaderTitle> </SettingsHeader> <View style={styles.account__content}> <View style={styles.account__main}> <View style={styles.account__row}> <Text style={styles.account__row_label}> - {pgettext('account-view', 'Account ID')} + {messages.pgettext('account-view', 'Account ID')} </Text> <ClipboardLabel style={styles.account__row_value} value={this.props.accountToken || ''} - message={pgettext('account-view', 'COPIED TO CLIPBOARD!')} + message={messages.pgettext('account-view', 'COPIED TO CLIPBOARD!')} /> </View> <View style={styles.account__row}> <Text style={styles.account__row_label}> - {pgettext('account-view', 'Paid until')} + {messages.pgettext('account-view', 'Paid until')} </Text> <FormattedAccountExpiry expiry={this.props.accountExpiry} @@ -68,12 +68,12 @@ export default class Account extends Component<IProps> { disabled={this.props.isOffline} onPress={this.props.onBuyMore}> <AppButton.Label> - {pgettext('account-view', 'Buy more credit')} + {messages.pgettext('account-view', 'Buy more credit')} </AppButton.Label> <AppButton.Icon source="icon-extLink" height={16} width={16} /> </AppButton.GreenButton> <AppButton.RedButton onPress={this.props.onLogout}> - {pgettext('account-view', 'Log out')} + {messages.pgettext('account-view', 'Log out')} </AppButton.RedButton> </View> </View> @@ -92,7 +92,9 @@ function FormattedAccountExpiry(props: { expiry?: string; locale: string }) { if (expiry.hasExpired()) { return ( - <Text style={styles.account__out_of_time}>{pgettext('account-view', 'OUT OF TIME')}</Text> + <Text style={styles.account__out_of_time}> + {messages.pgettext('account-view', 'OUT OF TIME')} + </Text> ); } else { return <Text style={styles.account__row_value}>{expiry.formattedDate()}</Text>; @@ -100,7 +102,7 @@ function FormattedAccountExpiry(props: { expiry?: string; locale: string }) { } else { return ( <Text style={styles.account__row_value}> - {pgettext('account-view', 'Currently unavailable')} + {messages.pgettext('account-view', 'Currently unavailable')} </Text> ); } diff --git a/gui/src/renderer/components/AdvancedSettings.tsx b/gui/src/renderer/components/AdvancedSettings.tsx index a1cb90e128..a1a844f8e6 100644 --- a/gui/src/renderer/components/AdvancedSettings.tsx +++ b/gui/src/renderer/components/AdvancedSettings.tsx @@ -3,7 +3,7 @@ import { Component, View } from 'reactxp'; import { sprintf } from 'sprintf-js'; import { colors } from '../../config.json'; import { RelayProtocol } from '../../shared/daemon-rpc-types'; -import { pgettext } from '../../shared/gettext'; +import { messages } from '../../shared/gettext'; import styles from './AdvancedSettingsStyles'; import * as Cell from './Cell'; import { Container, Layout } from './Layout'; @@ -90,26 +90,30 @@ export default class AdvancedSettings extends Component<IProps, IState> { <NavigationBar> <BackBarItem action={this.props.onClose}> {// TRANSLATORS: Back button in navigation bar - pgettext('advanced-settings-nav', 'Settings')} + messages.pgettext('advanced-settings-nav', 'Settings')} </BackBarItem> <TitleBarItem> {// TRANSLATORS: Title label in navigation bar - pgettext('advanced-settings-nav', 'Advanced')} + messages.pgettext('advanced-settings-nav', 'Advanced')} </TitleBarItem> </NavigationBar> <View style={styles.advanced_settings__container}> <NavigationScrollbars style={styles.advanced_settings__scrollview}> <SettingsHeader> - <HeaderTitle>{pgettext('advanced-settings-view', 'Advanced')}</HeaderTitle> + <HeaderTitle> + {messages.pgettext('advanced-settings-view', 'Advanced')} + </HeaderTitle> </SettingsHeader> <Cell.Container> - <Cell.Label>{pgettext('advanced-settings-view', 'Enable IPv6')}</Cell.Label> + <Cell.Label> + {messages.pgettext('advanced-settings-view', 'Enable IPv6')} + </Cell.Label> <Cell.Switch isOn={this.props.enableIpv6} onChange={this.props.setEnableIpv6} /> </Cell.Container> <Cell.Footer> - {pgettext( + {messages.pgettext( 'advanced-settings-view', 'Enable IPv6 communication through the tunnel.', )} @@ -117,7 +121,7 @@ export default class AdvancedSettings extends Component<IProps, IState> { <Cell.Container> <Cell.Label textStyle={styles.advanced_settings__block_when_disconnected_label}> - {pgettext('advanced-settings-view', 'Block when disconnected')} + {messages.pgettext('advanced-settings-view', 'Block when disconnected')} </Cell.Label> <Cell.Switch isOn={this.props.blockWhenDisconnected} @@ -125,7 +129,7 @@ export default class AdvancedSettings extends Component<IProps, IState> { /> </Cell.Container> <Cell.Footer> - {pgettext( + {messages.pgettext( 'advanced-settings-view', "Unless connected, always block all network traffic, even when you've disconnected or quit the app.", )} @@ -133,7 +137,7 @@ export default class AdvancedSettings extends Component<IProps, IState> { <View style={styles.advanced_settings__content}> <Selector - title={pgettext('advanced-settings-view', 'Network protocols')} + title={messages.pgettext('advanced-settings-view', 'Network protocols')} values={PROTOCOL_ITEMS} value={this.props.protocol} onSelect={this.onSelectProtocol} @@ -145,7 +149,7 @@ export default class AdvancedSettings extends Component<IProps, IState> { // TRANSLATORS: The title for the port selector section. // TRANSLATORS: Available placeholders: // TRANSLATORS: %(portType)s - a selected protocol (either TCP or UDP) - pgettext('advanced-settings-view', '%(portType)s port'), + messages.pgettext('advanced-settings-view', '%(portType)s port'), { portType: this.props.protocol.toUpperCase(), }, @@ -160,13 +164,13 @@ export default class AdvancedSettings extends Component<IProps, IState> { </View> <Cell.Container> - <Cell.Label>{pgettext('advanced-settings-view', 'Mssfix')}</Cell.Label> + <Cell.Label>{messages.pgettext('advanced-settings-view', 'Mssfix')}</Cell.Label> <Cell.InputFrame style={styles.advanced_settings__mssfix_frame}> <Cell.AutoSizingTextInputContainer> <Cell.Input keyboardType={'numeric'} maxLength={4} - placeholder={pgettext('advanced-settings-view', 'Default')} + placeholder={messages.pgettext('advanced-settings-view', 'Default')} value={mssfixValue ? mssfixValue.toString() : ''} style={[styles.advanced_settings__mssfix_input, mssfixStyle]} onChangeText={this.onMssfixChange} @@ -182,7 +186,7 @@ export default class AdvancedSettings extends Component<IProps, IState> { // TRANSLATORS: Available placeholders: // TRANSLATORS: %(max)d - the maximum possible mssfix value // TRANSLATORS: %(min)d - the minimum possible mssfix value - pgettext( + messages.pgettext( 'advanced-settings-view', 'Set OpenVPN MSS value. Valid range: %(min)d - %(max)d.', ), @@ -260,7 +264,7 @@ class Selector<T> extends Component<ISelectorProps<T>> { key={'auto'} selected={this.props.value === undefined} onSelect={this.props.onSelect}> - {pgettext('advanced-settings-view', 'Automatic')} + {messages.pgettext('advanced-settings-view', 'Automatic')} </SelectorCell> {this.props.values.map((item, i) => ( <SelectorCell diff --git a/gui/src/renderer/components/ExpiredAccountErrorView.tsx b/gui/src/renderer/components/ExpiredAccountErrorView.tsx index 3c8abd4223..ebd4fa1f4e 100644 --- a/gui/src/renderer/components/ExpiredAccountErrorView.tsx +++ b/gui/src/renderer/components/ExpiredAccountErrorView.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { Component, Styles, View } from 'reactxp'; import { colors } from '../../config.json'; -import { pgettext } from '../../shared/gettext'; +import { messages } from '../../shared/gettext'; import * as AppButton from './AppButton'; import ImageView from './ImageView'; @@ -75,7 +75,7 @@ export default class ExpiredAccountErrorView extends Component<IProps, IState> { <ImageView source="icon-fail" height={60} width={60} /> </View> <View style={styles.body}> - <View style={styles.title}>{pgettext('connect-view', 'Out of time')}</View> + <View style={styles.title}>{messages.pgettext('connect-view', 'Out of time')}</View> {this.renderContent()} </View> </View> @@ -103,7 +103,7 @@ class DisconnectAndOpenBrowserContentView extends Component<{ actionHandler: () return ( <View> <View style={styles.message}> - {pgettext( + {messages.pgettext( 'connect-view', 'You have no more VPN time left on this account. To buy more credit on our website, you will need to access the Internet with an unsecured connection.', )} @@ -111,7 +111,7 @@ class DisconnectAndOpenBrowserContentView extends Component<{ actionHandler: () <View> <AppButton.RedButton onPress={this.props.actionHandler}> <AppButton.Label> - {pgettext('connect-view', 'Disconnect and buy more credit')} + {messages.pgettext('connect-view', 'Disconnect and buy more credit')} </AppButton.Label> <AppButton.Icon source="icon-extLink" height={16} width={16} /> </AppButton.RedButton> @@ -126,14 +126,16 @@ class OpenBrowserContentView extends Component<{ actionHandler: () => void }> { return ( <View> <View style={styles.message}> - {pgettext( + {messages.pgettext( 'connect-view', 'You have no more VPN time left on this account. Please log in on our website to buy more credit.', )} </View> <View> <AppButton.GreenButton onPress={this.props.actionHandler}> - <AppButton.Label>{pgettext('connect-view', 'Buy more credit')}</AppButton.Label> + <AppButton.Label> + {messages.pgettext('connect-view', 'Buy more credit')} + </AppButton.Label> <AppButton.Icon source="icon-extLink" height={16} width={16} /> </AppButton.GreenButton> </View> @@ -147,14 +149,16 @@ class DisableBlockWhenDisconnectedContentView extends Component { return ( <View> <View style={styles.message}> - {pgettext( + {messages.pgettext( 'connect-view', 'You have no more VPN time left on this account. Before you can buy more credit on our website, you first need to turn off the app\'s "Block when disconnected" option under Advanced settings.', )} </View> <View> <AppButton.GreenButton disabled={true}> - <AppButton.Label>{pgettext('connect-view', 'Buy more credit')}</AppButton.Label> + <AppButton.Label> + {messages.pgettext('connect-view', 'Buy more credit')} + </AppButton.Label> <AppButton.Icon source="icon-extLink" height={16} width={16} /> </AppButton.GreenButton> </View> diff --git a/gui/src/renderer/components/Launch.tsx b/gui/src/renderer/components/Launch.tsx index 72fdde9d2f..e5a59df23b 100644 --- a/gui/src/renderer/components/Launch.tsx +++ b/gui/src/renderer/components/Launch.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { Component, Styles, Text, View } from 'reactxp'; import { colors } from '../../config.json'; -import { pgettext } from '../../shared/gettext'; +import { messages } from '../../shared/gettext'; import { SettingsBarButton } from './HeaderBar'; import ImageView from './ImageView'; import { Container, Header, Layout } from './Layout'; @@ -48,9 +48,9 @@ export default class Launch extends Component<IProps> { <Container> <View style={styles.container}> <ImageView height={120} width={120} source="logo-icon" style={styles.logo} /> - <Text style={styles.title}>{pgettext('launch-view', 'MULLVAD VPN')}</Text> + <Text style={styles.title}>{messages.pgettext('launch-view', 'MULLVAD VPN')}</Text> <Text style={styles.subtitle}> - {pgettext('launch-view', 'Connecting to daemon...')} + {messages.pgettext('launch-view', 'Connecting to daemon...')} </Text> </View> </Container> diff --git a/gui/src/renderer/components/Login.tsx b/gui/src/renderer/components/Login.tsx index 81b9e6f838..4773e40dbc 100644 --- a/gui/src/renderer/components/Login.tsx +++ b/gui/src/renderer/components/Login.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { Animated, Component, Styles, Text, TextInput, Types, UserInterface, View } from 'reactxp'; import { colors, links } from '../../config.json'; -import { pgettext } from '../../shared/gettext'; +import { messages } from '../../shared/gettext'; import Accordion from './Accordion'; import * as AppButton from './AppButton'; import * as Cell from './Cell'; @@ -212,13 +212,13 @@ export default class Login extends Component<IProps, IState> { private formTitle() { switch (this.props.loginState) { case 'logging in': - return pgettext('login-view', 'Logging in...'); + return messages.pgettext('login-view', 'Logging in...'); case 'failed': - return pgettext('login-view', 'Login failed'); + return messages.pgettext('login-view', 'Login failed'); case 'ok': - return pgettext('login-view', 'Logged in'); + return messages.pgettext('login-view', 'Logged in'); default: - return pgettext('login-view', 'Login'); + return messages.pgettext('login-view', 'Login'); } } @@ -226,13 +226,15 @@ export default class Login extends Component<IProps, IState> { const { loginState, loginError } = this.props; switch (loginState) { case 'failed': - return (loginError && loginError.message) || pgettext('login-view', 'Unknown error'); + return ( + (loginError && loginError.message) || messages.pgettext('login-view', 'Unknown error') + ); case 'logging in': - return pgettext('login-view', 'Checking account number'); + return messages.pgettext('login-view', 'Checking account number'); case 'ok': - return pgettext('login-view', 'Correct account number'); + return messages.pgettext('login-view', 'Correct account number'); default: - return pgettext('login-view', 'Enter your account number'); + return messages.pgettext('login-view', 'Enter your account number'); } } @@ -397,10 +399,10 @@ export default class Login extends Component<IProps, IState> { return ( <View> <Text style={styles.login_footer__prompt}> - {pgettext('login-view', "Don't have an account number?")} + {messages.pgettext('login-view', "Don't have an account number?")} </Text> <AppButton.BlueButton onPress={this.onCreateAccount}> - <AppButton.Label>{pgettext('login-view', 'Create account')}</AppButton.Label> + <AppButton.Label>{messages.pgettext('login-view', 'Create account')}</AppButton.Label> <AppButton.Icon source="icon-extLink" height={16} width={16} /> </AppButton.BlueButton> </View> diff --git a/gui/src/renderer/components/NotificationArea.tsx b/gui/src/renderer/components/NotificationArea.tsx index 033067df8a..6405e9828f 100644 --- a/gui/src/renderer/components/NotificationArea.tsx +++ b/gui/src/renderer/components/NotificationArea.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import { Component, Types } from 'reactxp'; import { sprintf } from 'sprintf-js'; import { links } from '../../config.json'; -import { pgettext } from '../../shared/gettext'; +import { messages } from '../../shared/gettext'; import { NotificationActions, NotificationBanner, @@ -45,28 +45,31 @@ function getBlockReasonMessage(blockReason: BlockReason): string { case 'auth_failed': return parseAuthFailure(blockReason.details).message; case 'ipv6_unavailable': - return pgettext( + return messages.pgettext( 'in-app-notifications', 'Could not configure IPv6, please enable it on your system or disable it in the app', ); case 'set_firewall_policy_error': - return pgettext( + return messages.pgettext( 'in-app-notifications', 'Failed to apply firewall rules. The device might currently be unsecured', ); case 'set_dns_error': - return pgettext('in-app-notifications', 'Failed to set system DNS server'); + return messages.pgettext('in-app-notifications', 'Failed to set system DNS server'); case 'start_tunnel_error': - return pgettext('in-app-notifications', 'Failed to start tunnel connection'); + return messages.pgettext('in-app-notifications', 'Failed to start tunnel connection'); case 'no_matching_relay': - return pgettext('in-app-notifications', 'No relay server matches the current settings'); + return messages.pgettext( + 'in-app-notifications', + 'No relay server matches the current settings', + ); case 'is_offline': - return pgettext( + return messages.pgettext( 'in-app-notifications', 'This device is offline, no tunnels can be established', ); case 'tap_adapter_problem': - return pgettext( + return messages.pgettext( 'in-app-notifications', "Unable to detect a working TAP adapter on this device. If you've disabled it, enable it again. Otherwise, please reinstall the app", ); @@ -185,7 +188,7 @@ export default class NotificationArea extends Component<IProps, State> { <NotificationIndicator type={'error'} /> <NotificationContent> <NotificationTitle> - {pgettext('in-app-notifications', 'FAILURE - UNSECURED')} + {messages.pgettext('in-app-notifications', 'FAILURE - UNSECURED')} </NotificationTitle> <NotificationSubtitle>{this.state.reason}</NotificationSubtitle> </NotificationContent> @@ -197,7 +200,7 @@ export default class NotificationArea extends Component<IProps, State> { <NotificationIndicator type={'error'} /> <NotificationContent> <NotificationTitle> - {pgettext('in-app-notifications', 'BLOCKING INTERNET')} + {messages.pgettext('in-app-notifications', 'BLOCKING INTERNET')} </NotificationTitle> <NotificationSubtitle>{this.state.reason}</NotificationSubtitle> </NotificationContent> @@ -209,10 +212,10 @@ export default class NotificationArea extends Component<IProps, State> { <NotificationIndicator type={'error'} /> <NotificationContent> <NotificationTitle> - {pgettext('in-app-notifications', 'INCONSISTENT VERSION')} + {messages.pgettext('in-app-notifications', 'INCONSISTENT VERSION')} </NotificationTitle> <NotificationSubtitle> - {pgettext( + {messages.pgettext( 'in-app-notifications', 'Inconsistent internal version information, please restart the app', )} @@ -226,14 +229,14 @@ export default class NotificationArea extends Component<IProps, State> { <NotificationIndicator type={'error'} /> <NotificationContent> <NotificationTitle> - {pgettext('in-app-notifications', 'UNSUPPORTED VERSION')} + {messages.pgettext('in-app-notifications', 'UNSUPPORTED VERSION')} </NotificationTitle> <NotificationSubtitle> {sprintf( // TRANSLATORS: The in-app banner displayed to the user when the running app becomes unsupported. // TRANSLATORS: Available placeholders: // TRANSLATORS: %(version)s - the newest available version of the app - pgettext( + messages.pgettext( 'in-app-notifications', 'You are running an unsupported app version. Please upgrade to %(version)s now to ensure your security', ), @@ -252,14 +255,14 @@ export default class NotificationArea extends Component<IProps, State> { <NotificationIndicator type={'warning'} /> <NotificationContent> <NotificationTitle> - {pgettext('in-app-notifications', 'UPDATE AVAILABLE')} + {messages.pgettext('in-app-notifications', 'UPDATE AVAILABLE')} </NotificationTitle> <NotificationSubtitle> {sprintf( // TRANSLATORS: The in-app banner displayed to the user when the app update is available. // TRANSLATORS: Available placeholders: // TRANSLATORS: %(version)s - the newest available version of the app - pgettext( + messages.pgettext( 'in-app-notifications', 'Install Mullvad VPN (%(version)s) to stay up to date', ), @@ -278,7 +281,7 @@ export default class NotificationArea extends Component<IProps, State> { <NotificationIndicator type={'warning'} /> <NotificationContent> <NotificationTitle> - {pgettext('in-app-notifications', 'ACCOUNT CREDIT EXPIRES SOON')} + {messages.pgettext('in-app-notifications', 'ACCOUNT CREDIT EXPIRES SOON')} </NotificationTitle> <NotificationSubtitle>{this.state.timeLeft}</NotificationSubtitle> </NotificationContent> diff --git a/gui/src/renderer/components/Preferences.tsx b/gui/src/renderer/components/Preferences.tsx index a3a1140253..d8c8887c0d 100644 --- a/gui/src/renderer/components/Preferences.tsx +++ b/gui/src/renderer/components/Preferences.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { Component, View } from 'reactxp'; -import { pgettext } from '../../shared/gettext'; +import { messages } from '../../shared/gettext'; import * as Cell from './Cell'; import { Container, Layout } from './Layout'; import { @@ -39,38 +39,42 @@ export default class Preferences extends Component<IPreferencesProps> { <NavigationBar> <BackBarItem action={this.props.onClose}> {// TRANSLATORS: Back button in navigation bar - pgettext('preferences-nav', 'Settings')} + messages.pgettext('preferences-nav', 'Settings')} </BackBarItem> <TitleBarItem> {// TRANSLATORS: Title label in navigation bar - pgettext('preferences-nav', 'Preferences')} + messages.pgettext('preferences-nav', 'Preferences')} </TitleBarItem> </NavigationBar> <View style={styles.preferences__container}> <NavigationScrollbars> <SettingsHeader> - <HeaderTitle>{pgettext('preferences-view', 'Preferences')}</HeaderTitle> + <HeaderTitle> + {messages.pgettext('preferences-view', 'Preferences')} + </HeaderTitle> </SettingsHeader> <View style={styles.preferences__content}> <Cell.Container> <Cell.Label> - {pgettext('preferences-view', 'Launch app on start-up')} + {messages.pgettext('preferences-view', 'Launch app on start-up')} </Cell.Label> <Cell.Switch isOn={this.props.autoStart} onChange={this.onChangeAutoStart} /> </Cell.Container> <View style={styles.preferences__separator} /> <Cell.Container> - <Cell.Label>{pgettext('preferences-view', 'Auto-connect')}</Cell.Label> + <Cell.Label> + {messages.pgettext('preferences-view', 'Auto-connect')} + </Cell.Label> <Cell.Switch isOn={this.props.autoConnect} onChange={this.props.setAutoConnect} /> </Cell.Container> <Cell.Footer> - {pgettext( + {messages.pgettext( 'preferences-view', 'Automatically connect to a server when the app launches.', )} @@ -78,12 +82,12 @@ export default class Preferences extends Component<IPreferencesProps> { <Cell.Container> <Cell.Label> - {pgettext('preferences-view', 'Local network sharing')} + {messages.pgettext('preferences-view', 'Local network sharing')} </Cell.Label> <Cell.Switch isOn={this.props.allowLan} onChange={this.props.setAllowLan} /> </Cell.Container> <Cell.Footer> - {pgettext( + {messages.pgettext( 'preferences-view', 'Allows access to other devices on the same network for sharing, printing etc.', )} @@ -127,11 +131,13 @@ class MonochromaticIconToggle extends Component<IMonochromaticIconProps> { return ( <View> <Cell.Container> - <Cell.Label>{pgettext('preferences-view', 'Monochromatic tray icon')}</Cell.Label> + <Cell.Label> + {messages.pgettext('preferences-view', 'Monochromatic tray icon')} + </Cell.Label> <Cell.Switch isOn={this.props.monochromaticIcon} onChange={this.props.onChange} /> </Cell.Container> <Cell.Footer> - {pgettext( + {messages.pgettext( 'preferences-view', 'Use a monochromatic tray icon instead of a colored one.', )} @@ -156,11 +162,11 @@ class StartMinimizedToggle extends Component<IStartMinimizedProps> { return ( <View> <Cell.Container> - <Cell.Label>{pgettext('preferences-view', 'Start minimized')}</Cell.Label> + <Cell.Label>{messages.pgettext('preferences-view', 'Start minimized')}</Cell.Label> <Cell.Switch isOn={this.props.startMinimized} onChange={this.props.onChange} /> </Cell.Container> <Cell.Footer> - {pgettext('preferences-view', 'Show only the tray icon when the app starts.')} + {messages.pgettext('preferences-view', 'Show only the tray icon when the app starts.')} </Cell.Footer> </View> ); diff --git a/gui/src/renderer/components/SecuredLabel.tsx b/gui/src/renderer/components/SecuredLabel.tsx index 6ae19ab7d1..dbe1791305 100644 --- a/gui/src/renderer/components/SecuredLabel.tsx +++ b/gui/src/renderer/components/SecuredLabel.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { Component, Styles, Text, Types } from 'reactxp'; -import { gettext } from '../../shared/gettext'; +import { messages } from '../../shared/gettext'; export enum SecuredDisplayStyle { secured, @@ -34,16 +34,16 @@ export default class SecuredLabel extends Component<IProps> { private getText() { switch (this.props.displayStyle) { case SecuredDisplayStyle.secured: - return gettext('SECURE CONNECTION'); + return messages.gettext('SECURE CONNECTION'); case SecuredDisplayStyle.blocked: - return gettext('BLOCKED CONNECTION'); + return messages.gettext('BLOCKED CONNECTION'); case SecuredDisplayStyle.securing: - return gettext('CREATING SECURE CONNECTION'); + return messages.gettext('CREATING SECURE CONNECTION'); case SecuredDisplayStyle.unsecured: - return gettext('UNSECURED CONNECTION'); + return messages.gettext('UNSECURED CONNECTION'); } } diff --git a/gui/src/renderer/components/SelectLocation.tsx b/gui/src/renderer/components/SelectLocation.tsx index c68893d314..16673c67e8 100644 --- a/gui/src/renderer/components/SelectLocation.tsx +++ b/gui/src/renderer/components/SelectLocation.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import ReactDOM from 'react-dom'; import { Component, View } from 'reactxp'; -import { pgettext } from '../../shared/gettext'; +import { countries, messages, relayLocations } from '../../shared/gettext'; import CustomScrollbars from './CustomScrollbars'; import { Container, Layout } from './Layout'; import { @@ -115,7 +115,7 @@ export default class SelectLocation extends Component<IProps, IState> { <CloseBarItem action={this.props.onClose} /> <TitleBarItem> {// TRANSLATORS: Title label in navigation bar - pgettext('select-location-nav', 'Select location')} + messages.pgettext('select-location-nav', 'Select location')} </TitleBarItem> </NavigationBar> <View style={styles.container}> @@ -123,10 +123,10 @@ export default class SelectLocation extends Component<IProps, IState> { <View style={styles.content}> <SettingsHeader style={styles.subtitle_header}> <HeaderTitle> - {pgettext('select-location-view', 'Select location')} + {messages.pgettext('select-location-view', 'Select location')} </HeaderTitle> <HeaderSubTitle> - {pgettext( + {messages.pgettext( 'select-location-view', 'While connected, your real location is masked with a private and secure location in the selected region', )} @@ -139,7 +139,7 @@ export default class SelectLocation extends Component<IProps, IState> { return ( <CountryRow key={getLocationKey(countryLocation)} - name={relayCountry.name} + name={countries.gettext(relayCountry.name)} hasActiveRelays={relayCountry.hasActiveRelays} expanded={this.isExpanded(countryLocation)} onSelect={this.handleSelection} @@ -153,7 +153,7 @@ export default class SelectLocation extends Component<IProps, IState> { return ( <CityRow key={getLocationKey(cityLocation)} - name={relayCity.name} + name={relayLocations.gettext(relayCity.name)} hasActiveRelays={relayCity.hasActiveRelays} expanded={this.isExpanded(cityLocation)} onSelect={this.handleSelection} diff --git a/gui/src/renderer/components/Settings.tsx b/gui/src/renderer/components/Settings.tsx index 6706a62b99..bf0e812a89 100644 --- a/gui/src/renderer/components/Settings.tsx +++ b/gui/src/renderer/components/Settings.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { Component, Text, View } from 'reactxp'; import { colors, links } from '../../config.json'; -import { pgettext } from '../../shared/gettext'; +import { messages } from '../../shared/gettext'; import AccountExpiry from '../lib/account-expiry'; import * as AppButton from './AppButton'; import * as Cell from './Cell'; @@ -47,7 +47,7 @@ export default class Settings extends Component<IProps> { <CloseBarItem action={this.props.onClose} /> <TitleBarItem> {// TRANSLATORS: Title label in navigation bar - pgettext('settings-view-nav', 'Settings')} + messages.pgettext('settings-view-nav', 'Settings')} </TitleBarItem> </NavigationBar> @@ -55,7 +55,7 @@ export default class Settings extends Component<IProps> { <NavigationScrollbars style={styles.settings__scrollview}> <View style={styles.settings__content}> <SettingsHeader> - <HeaderTitle>{pgettext('settings-view', 'Settings')}</HeaderTitle> + <HeaderTitle>{messages.pgettext('settings-view', 'Settings')}</HeaderTitle> </SettingsHeader> <View> {this.renderTopButtons()} @@ -77,7 +77,7 @@ export default class Settings extends Component<IProps> { return ( <View style={styles.settings__footer}> <AppButton.RedButton onPress={this.props.onQuit}> - {pgettext('settings-view', 'Quit app')} + {messages.pgettext('settings-view', 'Quit app')} </AppButton.RedButton> </View> ); @@ -95,13 +95,13 @@ export default class Settings extends Component<IProps> { const isOutOfTime = expiry ? expiry.hasExpired() : false; const formattedExpiry = expiry ? expiry.remainingTime().toUpperCase() : ''; - const outOfTimeMessage = pgettext('settings-view', 'OUT OF TIME'); + const outOfTimeMessage = messages.pgettext('settings-view', 'OUT OF TIME'); return ( <View> <View> <Cell.CellButton onPress={this.props.onViewAccount}> - <Cell.Label>{pgettext('settings-view', 'Account')}</Cell.Label> + <Cell.Label>{messages.pgettext('settings-view', 'Account')}</Cell.Label> <Cell.SubText style={isOutOfTime ? styles.settings__account_paid_until_label__error : undefined}> {isOutOfTime ? outOfTimeMessage : formattedExpiry} @@ -111,12 +111,12 @@ export default class Settings extends Component<IProps> { </View> <Cell.CellButton onPress={this.props.onViewPreferences}> - <Cell.Label>{pgettext('settings-view', 'Preferences')}</Cell.Label> + <Cell.Label>{messages.pgettext('settings-view', 'Preferences')}</Cell.Label> <Cell.Icon height={12} width={7} source="icon-chevron" /> </Cell.CellButton> <Cell.CellButton onPress={this.props.onViewAdvancedSettings}> - <Cell.Label>{pgettext('settings-view', 'Advanced')}</Cell.Label> + <Cell.Label>{messages.pgettext('settings-view', 'Advanced')}</Cell.Label> <Cell.Icon height={12} width={7} source="icon-chevron" /> </Cell.CellButton> <View style={styles.settings__cell_spacer} /> @@ -128,12 +128,12 @@ export default class Settings extends Component<IProps> { let icon; let footer; if (!this.props.consistentVersion || !this.props.upToDateVersion) { - const inconsistentVersionMessage = pgettext( + const inconsistentVersionMessage = messages.pgettext( 'settings-view', 'Inconsistent internal version information, please restart the app.', ); - const updateAvailableMessage = pgettext( + const updateAvailableMessage = messages.pgettext( 'settings-view', 'Update available, download to remain safe.', ); @@ -162,7 +162,7 @@ export default class Settings extends Component<IProps> { <View> <Cell.CellButton disabled={this.props.isOffline} onPress={this.openDownloadLink}> {icon} - <Cell.Label>{pgettext('settings-view', 'App version')}</Cell.Label> + <Cell.Label>{messages.pgettext('settings-view', 'App version')}</Cell.Label> <Cell.SubText style={styles.settings__appversion}>{this.props.appVersion}</Cell.SubText> <Cell.Icon height={16} width={16} source="icon-extLink" /> </Cell.CellButton> @@ -178,12 +178,12 @@ export default class Settings extends Component<IProps> { return ( <View> <Cell.CellButton onPress={this.props.onViewSupport}> - <Cell.Label>{pgettext('settings-view', 'Report a problem')}</Cell.Label> + <Cell.Label>{messages.pgettext('settings-view', 'Report a problem')}</Cell.Label> <Cell.Icon height={12} width={7} source="icon-chevron" /> </Cell.CellButton> <Cell.CellButton disabled={this.props.isOffline} onPress={this.openFaqLink}> - <Cell.Label>{pgettext('settings-view', 'FAQs & Guides')}</Cell.Label> + <Cell.Label>{messages.pgettext('settings-view', 'FAQs & Guides')}</Cell.Label> <Cell.Icon height={16} width={16} source="icon-extLink" /> </Cell.CellButton> </View> diff --git a/gui/src/renderer/components/Support.tsx b/gui/src/renderer/components/Support.tsx index 25e7c2d7d9..439b229dfc 100644 --- a/gui/src/renderer/components/Support.tsx +++ b/gui/src/renderer/components/Support.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { Component, Text, TextInput, View } from 'reactxp'; -import { gettext, pgettext } from '../../shared/gettext'; +import { messages } from '../../shared/gettext'; import * as AppButton from './AppButton'; import ImageView from './ImageView'; import { Container, Layout } from './Layout'; @@ -120,10 +120,10 @@ export default class Support extends Component<ISupportProps, ISupportState> { const { sendState } = this.state; const header = ( <SettingsHeader> - <HeaderTitle>{pgettext('support-view', 'Report a problem')}</HeaderTitle> + <HeaderTitle>{messages.pgettext('support-view', 'Report a problem')}</HeaderTitle> {(sendState === SendState.Initial || sendState === SendState.Confirm) && ( <HeaderSubTitle> - {pgettext( + {messages.pgettext( 'support-view', "To help you more effectively, your app's log file will be attached to this message. Your data will remain secure and private, as it is anonymised before being sent over an encrypted channel.", )} @@ -143,7 +143,7 @@ export default class Support extends Component<ISupportProps, ISupportState> { <NavigationBar> <BackBarItem action={this.props.onClose}> {// TRANSLATORS: Back button in navigation bar - pgettext('support-nav', 'Settings')} + messages.pgettext('support-nav', 'Settings')} </BackBarItem> </NavigationBar> <View style={styles.support__container}> @@ -239,7 +239,7 @@ export default class Support extends Component<ISupportProps, ISupportState> { <View style={styles.support__form_row_email}> <TextInput style={styles.support__form_email} - placeholder={pgettext('support-view', 'Your email (optional)')} + placeholder={messages.pgettext('support-view', 'Your email (optional)')} defaultValue={this.state.email} onChangeText={this.onChangeEmail} keyboardType="email-address" @@ -249,7 +249,7 @@ export default class Support extends Component<ISupportProps, ISupportState> { <View style={styles.support__form_message_scroll_wrap}> <TextInput style={styles.support__form_message} - placeholder={pgettext('support-view', 'Describe your problem')} + placeholder={messages.pgettext('support-view', 'Describe your problem')} defaultValue={this.state.message} multiline={true} onChangeText={this.onChangeDescription} @@ -258,11 +258,13 @@ export default class Support extends Component<ISupportProps, ISupportState> { </View> <View style={styles.support__footer}> <AppButton.BlueButton style={styles.view_logs_button} onPress={this.onViewLog}> - <AppButton.Label>{pgettext('support-view', 'View app logs')}</AppButton.Label> + <AppButton.Label> + {messages.pgettext('support-view', 'View app logs')} + </AppButton.Label> <AppButton.Icon source="icon-extLink" height={16} width={16} /> </AppButton.BlueButton> <AppButton.GreenButton disabled={!this.validate()} onPress={this.onSend}> - {pgettext('support-view', 'Send')} + {messages.pgettext('support-view', 'Send')} </AppButton.GreenButton> </View> </View> @@ -279,10 +281,10 @@ export default class Support extends Component<ISupportProps, ISupportState> { <ImageView source="icon-spinner" height={60} width={60} /> </View> <View style={styles.support__status_security__secure}> - {gettext('SECURE CONNECTION')} + {messages.gettext('SECURE CONNECTION')} </View> <Text style={styles.support__send_status}> - {pgettext('support-view', 'Sending...')} + {messages.pgettext('support-view', 'Sending...')} </Text> </View> </View> @@ -294,10 +296,9 @@ export default class Support extends Component<ISupportProps, ISupportState> { // TRANSLATORS: The message displayed to the user after submitting the problem report, given that the user left his or her email for us to reach back. // TRANSLATORS: Available placeholders: // TRANSLATORS: %(email)s - const reachBackMessage: React.ReactNodeArray = pgettext( - 'support-view', - 'If needed we will contact you on %(email)s', - ).split('%(email)s', 2); + const reachBackMessage: React.ReactNodeArray = messages + .pgettext('support-view', 'If needed we will contact you on %(email)s') + .split('%(email)s', 2); reachBackMessage.splice( 1, 0, @@ -314,12 +315,14 @@ export default class Support extends Component<ISupportProps, ISupportState> { <ImageView source="icon-success" height={60} width={60} /> </View> <Text style={styles.support__status_security__secure}> - {gettext('SECURE CONNECTION')} + {messages.gettext('SECURE CONNECTION')} + </Text> + <Text style={styles.support__send_status}> + {messages.pgettext('support-view', 'Sent')} </Text> - <Text style={styles.support__send_status}>{pgettext('support-view', 'Sent')}</Text> <Text style={styles.support__sent_message}> - {pgettext('support-view', 'Thanks! We will look into this.')} + {messages.pgettext('support-view', 'Thanks! We will look into this.')} </Text> {this.state.email.trim().length > 0 ? ( <Text style={styles.support__sent_message}>{reachBackMessage}</Text> @@ -339,13 +342,13 @@ export default class Support extends Component<ISupportProps, ISupportState> { <ImageView source="icon-fail" height={60} width={60} /> </View> <Text style={styles.support__status_security__secure}> - {gettext('SECURE CONNECTION')} + {messages.gettext('SECURE CONNECTION')} </Text> <Text style={styles.support__send_status}> - {pgettext('support-view', 'Failed to send')} + {messages.pgettext('support-view', 'Failed to send')} </Text> <Text style={styles.support__sent_message}> - {pgettext( + {messages.pgettext( 'support-view', "You may need to go back to the app's main screen and click Disconnect before trying again. Don't worry, the information you entered will remain in the form.", )} @@ -354,10 +357,10 @@ export default class Support extends Component<ISupportProps, ISupportState> { </View> <View style={styles.support__footer}> <AppButton.BlueButton style={styles.edit_message_button} onPress={this.handleEditMessage}> - {pgettext('support-view', 'Edit message')} + {messages.pgettext('support-view', 'Edit message')} </AppButton.BlueButton> <AppButton.GreenButton onPress={this.onSend}> - {pgettext('support-view', 'Try again')} + {messages.pgettext('support-view', 'Try again')} </AppButton.GreenButton> </View> </View> @@ -380,16 +383,16 @@ class ConfirmNoEmailDialog extends Component<IConfirmNoEmailDialogProps> { <View style={styles.confirm_no_email_background}> <View style={styles.confirm_no_email_dialog}> <Text style={styles.confirm_no_email_warning}> - {pgettext( + {messages.pgettext( 'support-view', 'You are about to send the problem report without a way for us to get back to you. If you want an answer to your report you will have to enter an email address.', )} </Text> <AppButton.GreenButton onPress={this.confirm}> - {pgettext('support-view', 'Send anyway')} + {messages.pgettext('support-view', 'Send anyway')} </AppButton.GreenButton> <AppButton.RedButton onPress={this.dismiss} style={styles.confirm_no_email_back_button}> - {pgettext('support-view', 'Back')} + {messages.pgettext('support-view', 'Back')} </AppButton.RedButton> </View> </View> diff --git a/gui/src/renderer/components/SvgMap.tsx b/gui/src/renderer/components/SvgMap.tsx index 25c77de7f2..aae8e151b7 100644 --- a/gui/src/renderer/components/SvgMap.tsx +++ b/gui/src/renderer/components/SvgMap.tsx @@ -9,6 +9,7 @@ import { Markers, ZoomableGroup, } from 'react-simple-maps'; +import { cities, countries } from '../../shared/gettext'; import geographyData from '../../../assets/geo/geometry.json'; import statesProvincesLinesData from '../../../assets/geo/states-provinces-lines.json'; @@ -179,7 +180,7 @@ export default class SvgMap extends React.Component<IProps, IState> { marker={{ coordinates: item.geometry.coordinates }} style={markerStyle}> <text fill="rgba(255,255,255,.6)" fontSize="22" textAnchor="middle"> - {item.properties.name} + {countries.gettext(item.properties.name)} </text> </Marker> )); @@ -191,7 +192,7 @@ export default class SvgMap extends React.Component<IProps, IState> { style={markerStyle}> <circle r="2" fill="rgba(255,255,255,.6)" /> <text x="0" y="-10" fill="rgba(255,255,255,.6)" fontSize="16" textAnchor="middle"> - {item.properties.name} + {cities.gettext(item.properties.name)} </text> </Marker> )); diff --git a/gui/src/renderer/components/TunnelControl.tsx b/gui/src/renderer/components/TunnelControl.tsx index 862b22cf05..0a8751e797 100644 --- a/gui/src/renderer/components/TunnelControl.tsx +++ b/gui/src/renderer/components/TunnelControl.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { Component, Styles, Text, Types, View } from 'reactxp'; import { colors } from '../../config.json'; -import { pgettext } from '../../shared/gettext'; +import { messages } from '../../shared/gettext'; import * as AppButton from './AppButton'; import ConnectionInfo from './ConnectionInfo'; import SecuredLabel, { SecuredDisplayStyle } from './SecuredLabel'; @@ -90,7 +90,7 @@ export default class TunnelControl extends Component<ITunnelControlProps> { <AppButton.TransparentButton style={styles.switch_location_button} onPress={this.props.onSelectLocation}> - {pgettext('tunnel-control', 'Switch location')} + {messages.pgettext('tunnel-control', 'Switch location')} </AppButton.TransparentButton> ); }; @@ -106,19 +106,19 @@ export default class TunnelControl extends Component<ITunnelControlProps> { const Connect = () => ( <AppButton.GreenButton onPress={this.props.onConnect}> - {pgettext('tunnel-control', 'Secure my connection')} + {messages.pgettext('tunnel-control', 'Secure my connection')} </AppButton.GreenButton> ); const Disconnect = () => ( <AppButton.RedTransparentButton onPress={this.props.onDisconnect}> - {pgettext('tunnel-control', 'Disconnect')} + {messages.pgettext('tunnel-control', 'Disconnect')} </AppButton.RedTransparentButton> ); const Cancel = () => ( <AppButton.RedTransparentButton onPress={this.props.onDisconnect}> - {pgettext('tunnel-control', 'Cancel')} + {messages.pgettext('tunnel-control', 'Cancel')} </AppButton.RedTransparentButton> ); diff --git a/gui/src/renderer/containers/ConnectPage.tsx b/gui/src/renderer/containers/ConnectPage.tsx index dbbf898457..2e22ca9fab 100644 --- a/gui/src/renderer/containers/ConnectPage.tsx +++ b/gui/src/renderer/containers/ConnectPage.tsx @@ -4,7 +4,11 @@ import log from 'electron-log'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import { sprintf } from 'sprintf-js'; -import { pgettext } from '../../shared/gettext'; +import { + countries, + messages, + relayLocations as relayLocationsLocalization, +} from '../../shared/gettext'; import Connect from '../components/Connect'; import AccountExpiry from '../lib/account-expiry'; import userInterfaceActions from '../redux/userinterface/actions'; @@ -26,8 +30,7 @@ function getRelayName( } else if ('country' in location) { const country = relayLocations.find(({ code }) => code === location.country); if (country) { - // TODO: translate - return country.name; + return countries.gettext(country.name); } } else if ('city' in location) { const [countryCode, cityCode] = location.city; @@ -35,8 +38,7 @@ function getRelayName( if (country) { const city = country.cities.find(({ code }) => code === cityCode); if (city) { - // TODO: translate - return city.name; + return relayLocationsLocalization.gettext(city.name); } } } else if ('hostname' in location) { @@ -51,7 +53,7 @@ function getRelayName( // TRANSLATORS: Available placeholders: // TRANSLATORS: %(city)s - a city name // TRANSLATORS: %(hostname)s - a hostname - pgettext('connect-container', '%(city)s (%(hostname)s)'), + messages.pgettext('connect-container', '%(city)s (%(hostname)s)'), { city: city.name, hostname, diff --git a/gui/src/renderer/lib/account-expiry.ts b/gui/src/renderer/lib/account-expiry.ts index 5a8e5e8ec2..5393238dcd 100644 --- a/gui/src/renderer/lib/account-expiry.ts +++ b/gui/src/renderer/lib/account-expiry.ts @@ -1,6 +1,6 @@ import moment from 'moment'; import { sprintf } from 'sprintf-js'; -import { pgettext } from '../../shared/gettext'; +import { messages } from '../../shared/gettext'; export default class AccountExpiry { private expiry: moment.Moment; @@ -28,7 +28,7 @@ export default class AccountExpiry { // TRANSLATORS: The remaining time left on the account displayed across the app. // TRANSLATORS: Available placeholders: // TRANSLATORS: %(duration)s - a localized remaining time (in minutes, hours, or days) until the account expiry - pgettext('account-expiry', '%(duration)s left'), + messages.pgettext('account-expiry', '%(duration)s left'), { duration }, ); } diff --git a/gui/src/renderer/lib/auth-failure.ts b/gui/src/renderer/lib/auth-failure.ts index 6428d0f65a..ead0b08a81 100644 --- a/gui/src/renderer/lib/auth-failure.ts +++ b/gui/src/renderer/lib/auth-failure.ts @@ -1,4 +1,4 @@ -import { pgettext } from '../../shared/gettext'; +import { messages } from '../../shared/gettext'; export enum AuthFailureKind { invalidAccount, @@ -58,24 +58,24 @@ function parseRawFailureKind(failureId: string): AuthFailureKind { function messageForFailureKind(kind: AuthFailureKind): string { switch (kind) { case AuthFailureKind.invalidAccount: - return pgettext( + return messages.pgettext( 'auth-failure', "You've logged in with an account number that is not valid. Please log out and try another one.", ); case AuthFailureKind.expiredAccount: - return pgettext( + return messages.pgettext( 'auth-failure', 'You have no more VPN time left on this account. Please log in on our website to buy more credit.', ); case AuthFailureKind.tooManyConnections: - return pgettext( + return messages.pgettext( 'auth-failure', 'This account has too many simultaneous connections. Disconnect another device or try connecting again shortly.', ); case AuthFailureKind.unknown: - return pgettext('auth-failure', 'Account authentication failed.'); + return messages.pgettext('auth-failure', 'Account authentication failed.'); } } diff --git a/gui/src/shared/gettext.ts b/gui/src/shared/gettext.ts index 45ff8d3637..07a560bdd8 100644 --- a/gui/src/shared/gettext.ts +++ b/gui/src/shared/gettext.ts @@ -5,23 +5,9 @@ import Gettext from 'node-gettext'; import path from 'path'; const SOURCE_LANGUAGE = 'en'; -let SELECTED_LANGUAGE = SOURCE_LANGUAGE; const LOCALES_DIR = path.resolve(__dirname, '../../locales'); -// `{debug: false}` option prevents Gettext from printing the warnings to console in development -// the errors are handled separately in the "error" handler below -const catalogue = new Gettext({ debug: false }); -catalogue.setTextDomain('messages'); -catalogue.on('error', (error) => { - // Filter out the "no translation was found" errors for the source language - if (SELECTED_LANGUAGE === SOURCE_LANGUAGE && error.indexOf('No translation was found') !== -1) { - return; - } - - log.warn(`Gettext error: ${error}`); -}); - -export function loadTranslations(currentLocale: string) { +export function loadTranslations(currentLocale: string, catalogue: Gettext) { // First look for exact match of the current locale const preferredLocales = []; @@ -36,17 +22,18 @@ export function loadTranslations(currentLocale: string) { } for (const locale of preferredLocales) { - if (parseTranslation(locale, 'messages')) { + // NOTE: domain is not publicly exposed + const domain = (catalogue as any).domain; + + if (parseTranslation(locale, domain, catalogue)) { log.info(`Loaded translations for ${locale}`); catalogue.setLocale(locale); - - SELECTED_LANGUAGE = locale; return; } } } -function parseTranslation(locale: string, domain: string): boolean { +function parseTranslation(locale: string, domain: string, catalogue: Gettext): boolean { const filename = path.join(LOCALES_DIR, locale, `${domain}.po`); let buffer: Buffer; @@ -72,9 +59,34 @@ function parseTranslation(locale: string, domain: string): boolean { return true; } -export const gettext = (msgid: string): string => { - return catalogue.gettext(msgid); -}; -export const pgettext = (msgctx: string, msgid: string): string => { - return catalogue.pgettext(msgctx, msgid); -}; +function setErrorHandler(catalogue: Gettext) { + catalogue.on('error', (error) => { + // NOTE: locale is not publicly exposed + const catalogueLocale = (catalogue as any).locale; + + // Filter out the "no translation was found" errors for the source language + if (catalogueLocale === SOURCE_LANGUAGE && error.indexOf('No translation was found') !== -1) { + return; + } + + log.warn(`Gettext error: ${error}`); + }); +} + +// `{debug: false}` option prevents Gettext from printing the warnings to console in development +// the errors are handled separately in the "error" handler below +export const messages = new Gettext({ debug: false }); +messages.setTextDomain('messages'); +setErrorHandler(messages); + +export const countries = new Gettext({ debug: false }); +countries.setTextDomain('countries'); +setErrorHandler(countries); + +export const cities = new Gettext({ debug: false }); +cities.setTextDomain('cities'); +setErrorHandler(cities); + +export const relayLocations = new Gettext({ debug: false }); +relayLocations.setTextDomain('relay-locations'); +setErrorHandler(relayLocations); |
