summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2019-02-27 14:53:36 +0100
committerAndrej Mihajlov <and@mullvad.net>2019-02-27 14:53:36 +0100
commit08418a76dce2c1008ff4f31b157eb8a112917fe1 (patch)
treeb5a0f9b035fb77b390b40219ecf2572457605b7c
parent341dcee16ad96cfcb873b69c071f99dcab9bc98b (diff)
parentdba5d2a272719a59448cc1fc1f60a6e1075a1853 (diff)
downloadmullvadvpn-08418a76dce2c1008ff4f31b157eb8a112917fe1.tar.xz
mullvadvpn-08418a76dce2c1008ff4f31b157eb8a112917fe1.zip
Merge branch 'gettext'
-rw-r--r--gui/package.json2
-rw-r--r--gui/packages/components/src/HeaderBar.tsx7
-rw-r--r--gui/packages/components/src/SecuredLabel.tsx4
-rw-r--r--gui/packages/config/package.json2
-rw-r--r--gui/packages/desktop/locales/README.md43
-rw-r--r--gui/packages/desktop/locales/de/.gitkeep0
-rw-r--r--gui/packages/desktop/locales/es/.gitkeep0
-rw-r--r--gui/packages/desktop/locales/fr/.gitkeep0
-rw-r--r--gui/packages/desktop/locales/it/.gitkeep0
-rw-r--r--gui/packages/desktop/locales/ja/.gitkeep0
-rw-r--r--gui/packages/desktop/locales/messages.pot622
-rw-r--r--gui/packages/desktop/locales/nl/.gitkeep0
-rw-r--r--gui/packages/desktop/locales/no/.gitkeep0
-rw-r--r--gui/packages/desktop/locales/pt/.gitkeep0
-rw-r--r--gui/packages/desktop/locales/ru/.gitkeep0
-rw-r--r--gui/packages/desktop/locales/sv/.gitkeep0
-rw-r--r--gui/packages/desktop/locales/tr/.gitkeep0
-rw-r--r--gui/packages/desktop/locales/zh/.gitkeep0
-rw-r--r--gui/packages/desktop/package.json10
-rw-r--r--gui/packages/desktop/scripts/extract-translations.js48
-rwxr-xr-xgui/packages/desktop/scripts/update-translations.sh25
-rw-r--r--gui/packages/desktop/src/main/index.ts46
-rw-r--r--gui/packages/desktop/src/main/notification-controller.ts35
-rw-r--r--gui/packages/desktop/src/main/proc.ts3
-rw-r--r--gui/packages/desktop/src/renderer/app.tsx6
-rw-r--r--gui/packages/desktop/src/renderer/components/Account.tsx30
-rw-r--r--gui/packages/desktop/src/renderer/components/AdvancedSettings.tsx65
-rw-r--r--gui/packages/desktop/src/renderer/components/Connect.tsx17
-rw-r--r--gui/packages/desktop/src/renderer/components/Launch.tsx7
-rw-r--r--gui/packages/desktop/src/renderer/components/Login.tsx23
-rw-r--r--gui/packages/desktop/src/renderer/components/NotificationArea.tsx87
-rw-r--r--gui/packages/desktop/src/renderer/components/Preferences.tsx48
-rw-r--r--gui/packages/desktop/src/renderer/components/SelectLocation.tsx16
-rw-r--r--gui/packages/desktop/src/renderer/components/Settings.tsx60
-rw-r--r--gui/packages/desktop/src/renderer/components/Support.tsx104
-rw-r--r--gui/packages/desktop/src/renderer/components/TunnelControl.tsx9
-rw-r--r--gui/packages/desktop/src/renderer/containers/ConnectPage.tsx17
-rw-r--r--gui/packages/desktop/src/renderer/lib/account-expiry.ts12
-rw-r--r--gui/packages/desktop/src/renderer/lib/auth-failure.ts25
-rw-r--r--gui/packages/desktop/src/shared/gettext.ts80
-rw-r--r--gui/types/gettext-parser/index.d.ts5
-rw-r--r--gui/yarn.lock133
42 files changed, 1393 insertions, 198 deletions
diff --git a/gui/package.json b/gui/package.json
index 200e25d076..4801f9ce35 100644
--- a/gui/package.json
+++ b/gui/package.json
@@ -13,6 +13,6 @@
"pack:linux": "yarn workspace desktop pack:linux"
},
"devDependencies": {
- "prettier": "1.15.3"
+ "prettier": "1.16.4"
}
}
diff --git a/gui/packages/components/src/HeaderBar.tsx b/gui/packages/components/src/HeaderBar.tsx
index 7fd1a96f24..50a5aace31 100644
--- a/gui/packages/components/src/HeaderBar.tsx
+++ b/gui/packages/components/src/HeaderBar.tsx
@@ -100,7 +100,12 @@ export class Brand extends Component {
return (
<View style={brandStyles.container}>
<ImageView width={50} height={50} source="logo-icon" />
- <Text style={brandStyles.title}>{'MULLVAD VPN'}</Text>
+ <Text style={brandStyles.title}>
+ {
+ // TODO: perhaps translate?
+ 'MULLVAD VPN'
+ }
+ </Text>
</View>
);
}
diff --git a/gui/packages/components/src/SecuredLabel.tsx b/gui/packages/components/src/SecuredLabel.tsx
index 26fc425383..1d33755d11 100644
--- a/gui/packages/components/src/SecuredLabel.tsx
+++ b/gui/packages/components/src/SecuredLabel.tsx
@@ -33,15 +33,19 @@ export default class SecuredLabel extends Component<IProps> {
private getText() {
switch (this.props.displayStyle) {
case SecuredDisplayStyle.secured:
+ // TODO: translate
return 'SECURE CONNECTION';
case SecuredDisplayStyle.blocked:
+ // TODO: translate
return 'BLOCKED CONNECTION';
case SecuredDisplayStyle.securing:
+ // TODO: translate
return 'CREATING SECURE CONNECTION';
case SecuredDisplayStyle.unsecured:
+ // TODO: translate
return 'UNSECURED CONNECTION';
}
}
diff --git a/gui/packages/config/package.json b/gui/packages/config/package.json
index fac7c77025..2e1bd83c66 100644
--- a/gui/packages/config/package.json
+++ b/gui/packages/config/package.json
@@ -3,7 +3,7 @@
"name": "@mullvad/config",
"version": "0.1.0",
"devDependencies": {
- "tslint-config-prettier": "^1.17.0",
+ "tslint-config-prettier": "^1.18.0",
"tslint-react": "^3.6.0"
}
}
diff --git a/gui/packages/desktop/locales/README.md b/gui/packages/desktop/locales/README.md
new file mode 100644
index 0000000000..0845e981be
--- /dev/null
+++ b/gui/packages/desktop/locales/README.md
@@ -0,0 +1,43 @@
+This is a folder with gettext translations for Mullvad VPN app.
+
+## Dependency installation notes
+
+Make sure to install the GNU Gettext utilities.
+
+### Linux
+
+Normally shipped with the OS.
+
+### macOS
+
+Install `gettext` via Homebrew:
+
+```
+brew install gettext
+```
+
+### Windows
+
+Please follow the downlaod instructions at https://www.gnu.org/software/gettext/
+
+
+## Adding new translations
+
+Create a new sub-folder under `gui/packages/desktop/locales`, use the locale identifier for the
+folder name.
+
+The complete list of supported locale identifiers can be found at:
+
+https://electronjs.org/docs/api/locales
+
+In order to initialize the translations catalogue for the new locale, simple follow the update
+procedure, described in the section below.
+
+
+## Updating translations
+
+Run `yarn workspace desktop update-translations` to extract the new translations from the source
+code and update all of the existing catalogues.
+
+The new translations are automatically added to empty sub-folders using the POT template at
+`gui/packages/desktop/locales/messages.pot`. Folders that contain a `.gitkeep` file are ignored.
diff --git a/gui/packages/desktop/locales/de/.gitkeep b/gui/packages/desktop/locales/de/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/gui/packages/desktop/locales/de/.gitkeep
diff --git a/gui/packages/desktop/locales/es/.gitkeep b/gui/packages/desktop/locales/es/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/gui/packages/desktop/locales/es/.gitkeep
diff --git a/gui/packages/desktop/locales/fr/.gitkeep b/gui/packages/desktop/locales/fr/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/gui/packages/desktop/locales/fr/.gitkeep
diff --git a/gui/packages/desktop/locales/it/.gitkeep b/gui/packages/desktop/locales/it/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/gui/packages/desktop/locales/it/.gitkeep
diff --git a/gui/packages/desktop/locales/ja/.gitkeep b/gui/packages/desktop/locales/ja/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/gui/packages/desktop/locales/ja/.gitkeep
diff --git a/gui/packages/desktop/locales/messages.pot b/gui/packages/desktop/locales/messages.pot
new file mode 100644
index 0000000000..1ca98edfb9
--- /dev/null
+++ b/gui/packages/desktop/locales/messages.pot
@@ -0,0 +1,622 @@
+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+
+#. The remaining time left on the account displayed across the app.
+#. Available placeholders:
+#. %(duration)s - a localized remaining time (in minutes, hours, or days) until the account expiry
+#: src/renderer/lib/account-expiry.ts:27
+msgctxt "account-expiry"
+msgid "%(duration)s left"
+msgstr ""
+
+#. Back button in navigation bar
+#: src/renderer/components/Account.tsx:32
+msgctxt "account-nav"
+msgid "Settings"
+msgstr ""
+
+#: src/renderer/components/Account.tsx:38
+msgctxt "account-view"
+msgid "Account"
+msgstr ""
+
+#: src/renderer/components/Account.tsx:45
+msgctxt "account-view"
+msgid "Account ID"
+msgstr ""
+
+#: src/renderer/components/Account.tsx:68
+msgctxt "account-view"
+msgid "Buy more credit"
+msgstr ""
+
+#: src/renderer/components/Account.tsx:50
+msgctxt "account-view"
+msgid "COPIED TO CLIPBOARD!"
+msgstr ""
+
+#: src/renderer/components/Account.tsx:90
+msgctxt "account-view"
+msgid "Currently unavailable"
+msgstr ""
+
+#: src/renderer/components/Account.tsx:73
+msgctxt "account-view"
+msgid "Log out"
+msgstr ""
+
+#: src/renderer/components/Account.tsx:99
+msgctxt "account-view"
+msgid "OUT OF TIME"
+msgstr ""
+
+#. Title label in navigation bar
+#: src/renderer/components/AdvancedSettings.tsx:98
+msgctxt "advanced-settings-nav"
+msgid "Advanced"
+msgstr ""
+
+#. Back button in navigation bar
+#: src/renderer/components/AdvancedSettings.tsx:94
+msgctxt "advanced-settings-nav"
+msgid "Settings"
+msgstr ""
+
+#. The title for the port selector section.
+#. Available placeholders:
+#. %(portType)s - a selected protocol (either TCP or UDP)
+#: src/renderer/components/AdvancedSettings.tsx:151
+msgctxt "advanced-settings-view"
+msgid "%(portType)s port"
+msgstr ""
+
+#: src/renderer/components/AdvancedSettings.tsx:105
+msgctxt "advanced-settings-view"
+msgid "Advanced"
+msgstr ""
+
+#: src/renderer/components/AdvancedSettings.tsx:264
+msgctxt "advanced-settings-view"
+msgid "Automatic"
+msgstr ""
+
+#: src/renderer/components/AdvancedSettings.tsx:121
+msgctxt "advanced-settings-view"
+msgid "Block when disconnected"
+msgstr ""
+
+#: src/renderer/components/AdvancedSettings.tsx:171
+msgctxt "advanced-settings-view"
+msgid "Default"
+msgstr ""
+
+#: src/renderer/components/AdvancedSettings.tsx:109
+msgctxt "advanced-settings-view"
+msgid "Enable IPv6"
+msgstr ""
+
+#: src/renderer/components/AdvancedSettings.tsx:113
+msgctxt "advanced-settings-view"
+msgid "Enable IPv6 communication through the tunnel."
+msgstr ""
+
+#: src/renderer/components/AdvancedSettings.tsx:166
+msgctxt "advanced-settings-view"
+msgid "Mssfix"
+msgstr ""
+
+#: src/renderer/components/AdvancedSettings.tsx:137
+msgctxt "advanced-settings-view"
+msgid "Network protocols"
+msgstr ""
+
+#. The hint displayed below the Mssfix input field.
+#. Available placeholders:
+#. %(max)d - the maximum possible mssfix value
+#. %(min)d - the minimum possible mssfix value
+#: src/renderer/components/AdvancedSettings.tsx:186
+msgctxt "advanced-settings-view"
+msgid "Set OpenVPN MSS value. Valid range: %(min)d - %(max)d."
+msgstr ""
+
+#: src/renderer/components/AdvancedSettings.tsx:129
+msgctxt "advanced-settings-view"
+msgid "Unless connected, always block all network traffic, even when you've disconnected or quit the app."
+msgstr ""
+
+#: src/renderer/lib/auth-failure.ts:12
+msgctxt "auth-failure"
+msgid "Account authentication failed."
+msgstr ""
+
+#: src/renderer/lib/auth-failure.ts:24
+msgctxt "auth-failure"
+msgid "This account has too many simultaneous connections. Disconnect another device or try connecting again shortly."
+msgstr ""
+
+#: src/renderer/lib/auth-failure.ts:19
+msgctxt "auth-failure"
+msgid "You have no more VPN time left on this account. Please log in on our website to buy more credit."
+msgstr ""
+
+#: src/renderer/lib/auth-failure.ts:14
+msgctxt "auth-failure"
+msgid "You've logged in with an account number that is not valid. Please log out and try another one."
+msgstr ""
+
+#. The selected location label displayed on the main view, when a user selected a specific host to connect to.
+#. Example: Malmö (se-mma-001)
+#. Available placeholders:
+#. %(city)s - a city name
+#. %(hostname)s - a hostname
+#: src/renderer/containers/ConnectPage.tsx:54
+msgctxt "connect-container"
+msgid "%(city)s (%(hostname)s)"
+msgstr ""
+
+#: src/renderer/components/Connect.tsx:59
+msgctxt "connect-view"
+msgid "Buy more time, so you can continue using the internet securely"
+msgstr ""
+
+#: src/renderer/components/Connect.tsx:66
+msgctxt "connect-view"
+msgid "Offline"
+msgstr ""
+
+#: src/renderer/components/Connect.tsx:57
+msgctxt "connect-view"
+msgid "Out of time"
+msgstr ""
+
+#: src/renderer/components/Connect.tsx:68
+msgctxt "connect-view"
+msgid "Your internet connection will be secured when you get back online"
+msgstr ""
+
+#: src/renderer/components/NotificationArea.tsx:275
+msgctxt "in-app-notifications"
+msgid "ACCOUNT CREDIT EXPIRES SOON"
+msgstr ""
+
+#: src/renderer/components/NotificationArea.tsx:194
+msgctxt "in-app-notifications"
+msgid "BLOCKING INTERNET"
+msgstr ""
+
+#: src/renderer/components/NotificationArea.tsx:49
+msgctxt "in-app-notifications"
+msgid "Could not configure IPv6, please enable it on your system or disable it in the app"
+msgstr ""
+
+#: src/renderer/components/NotificationArea.tsx:54
+msgctxt "in-app-notifications"
+msgid "Failed to apply firewall rules. The device might currently be unsecured"
+msgstr ""
+
+#: src/renderer/components/NotificationArea.tsx:59
+msgctxt "in-app-notifications"
+msgid "Failed to set system DNS server"
+msgstr ""
+
+#: src/renderer/components/NotificationArea.tsx:61
+msgctxt "in-app-notifications"
+msgid "Failed to start tunnel connection"
+msgstr ""
+
+#: src/renderer/components/NotificationArea.tsx:182
+msgctxt "in-app-notifications"
+msgid "FAILURE - UNSECURED"
+msgstr ""
+
+#: src/renderer/components/NotificationArea.tsx:209
+msgctxt "in-app-notifications"
+msgid "Inconsistent internal version information, please restart the app"
+msgstr ""
+
+#: src/renderer/components/NotificationArea.tsx:206
+msgctxt "in-app-notifications"
+msgid "INCONSISTENT VERSION"
+msgstr ""
+
+#. The in-app banner displayed to the user when the app update is available.
+#. Available placeholders:
+#. %(version)s - the newest available version of the app
+#: src/renderer/components/NotificationArea.tsx:256
+msgctxt "in-app-notifications"
+msgid "Install Mullvad VPN (%(version)s) to stay up to date"
+msgstr ""
+
+#: src/renderer/components/NotificationArea.tsx:63
+msgctxt "in-app-notifications"
+msgid "No relay server matches the current settings"
+msgstr ""
+
+#: src/renderer/components/NotificationArea.tsx:65
+msgctxt "in-app-notifications"
+msgid "This device is offline, no tunnels can be established"
+msgstr ""
+
+#: src/renderer/components/NotificationArea.tsx:70
+msgctxt "in-app-notifications"
+msgid "Unable to detect a working TAP adapter on this device. If you've disabled it, enable it again. Otherwise, please reinstall the app"
+msgstr ""
+
+#: src/renderer/components/NotificationArea.tsx:223
+msgctxt "in-app-notifications"
+msgid "UNSUPPORTED VERSION"
+msgstr ""
+
+#: src/renderer/components/NotificationArea.tsx:249
+msgctxt "in-app-notifications"
+msgid "UPDATE AVAILABLE"
+msgstr ""
+
+#. The in-app banner displayed to the user when the running app becomes unsupported.
+#. Available placeholders:
+#. %(version)s - the newest available version of the app
+#: src/renderer/components/NotificationArea.tsx:230
+msgctxt "in-app-notifications"
+msgid "You are running an unsupported app version. Please upgrade to %(version)s now to ensure your security"
+msgstr ""
+
+#: src/renderer/components/Launch.tsx:52
+msgctxt "launch-view"
+msgid "Connecting to daemon..."
+msgstr ""
+
+#: src/renderer/components/Launch.tsx:50
+msgctxt "launch-view"
+msgid "MULLVAD VPN"
+msgstr ""
+
+#: src/renderer/components/Login.tsx:229
+msgctxt "login-view"
+msgid "Checking account number"
+msgstr ""
+
+#: src/renderer/components/Login.tsx:231
+msgctxt "login-view"
+msgid "Correct account number"
+msgstr ""
+
+#: src/renderer/components/Login.tsx:401
+msgctxt "login-view"
+msgid "Create account"
+msgstr ""
+
+#: src/renderer/components/Login.tsx:398
+msgctxt "login-view"
+msgid "Don't have an account number?"
+msgstr ""
+
+#: src/renderer/components/Login.tsx:233
+msgctxt "login-view"
+msgid "Enter your account number"
+msgstr ""
+
+#: src/renderer/components/Login.tsx:217
+msgctxt "login-view"
+msgid "Logged in"
+msgstr ""
+
+#: src/renderer/components/Login.tsx:213
+msgctxt "login-view"
+msgid "Logging in..."
+msgstr ""
+
+#: src/renderer/components/Login.tsx:219
+msgctxt "login-view"
+msgid "Login"
+msgstr ""
+
+#: src/renderer/components/Login.tsx:215
+msgctxt "login-view"
+msgid "Login failed"
+msgstr ""
+
+#: src/renderer/components/Login.tsx:227
+msgctxt "login-view"
+msgid "Unknown error"
+msgstr ""
+
+#: src/main/notification-controller.ts:46
+msgctxt "notifications"
+msgid "Blocked all connections"
+msgstr ""
+
+#: src/main/notification-controller.ts:29
+msgctxt "notifications"
+msgid "Connecting"
+msgstr ""
+
+#: src/main/notification-controller.ts:42
+msgctxt "notifications"
+msgid "Critical failure - Unsecured"
+msgstr ""
+
+#: src/main/notification-controller.ts:71
+msgctxt "notifications"
+msgid "Inconsistent internal version information, please restart the app"
+msgstr ""
+
+#: src/main/notification-controller.ts:57
+msgctxt "notifications"
+msgid "Reconnecting"
+msgstr ""
+
+#: src/main/notification-controller.ts:33
+msgctxt "notifications"
+msgid "Secured"
+msgstr ""
+
+#: src/main/notification-controller.ts:36
+msgctxt "notifications"
+msgid "Unsecured"
+msgstr ""
+
+#. The system notification displayed to the user when the running app becomes unsupported.
+#. Available placeholder:
+#. %(version) - the newest available version of the app
+#: src/main/notification-controller.ts:90
+msgctxt "notifications"
+msgid "You are running an unsupported app version. Please upgrade to %(version)s now to ensure your security"
+msgstr ""
+
+#. Title label in navigation bar
+#: src/renderer/components/Preferences.tsx:47
+msgctxt "preferences-nav"
+msgid "Preferences"
+msgstr ""
+
+#. Back button in navigation bar
+#: src/renderer/components/Preferences.tsx:43
+msgctxt "preferences-nav"
+msgid "Settings"
+msgstr ""
+
+#: src/renderer/components/Preferences.tsx:84
+msgctxt "preferences-view"
+msgid "Allows access to other devices on the same network for sharing, printing etc."
+msgstr ""
+
+#: src/renderer/components/Preferences.tsx:67
+msgctxt "preferences-view"
+msgid "Auto-connect"
+msgstr ""
+
+#: src/renderer/components/Preferences.tsx:71
+msgctxt "preferences-view"
+msgid "Automatically connect to a server when the app launches."
+msgstr ""
+
+#: src/renderer/components/Preferences.tsx:60
+msgctxt "preferences-view"
+msgid "Launch app on start-up"
+msgstr ""
+
+#: src/renderer/components/Preferences.tsx:79
+msgctxt "preferences-view"
+msgid "Local network sharing"
+msgstr ""
+
+#: src/renderer/components/Preferences.tsx:128
+msgctxt "preferences-view"
+msgid "Monochromatic tray icon"
+msgstr ""
+
+#: src/renderer/components/Preferences.tsx:54
+msgctxt "preferences-view"
+msgid "Preferences"
+msgstr ""
+
+#: src/renderer/components/Preferences.tsx:161
+msgctxt "preferences-view"
+msgid "Show only the tray icon when the app starts."
+msgstr ""
+
+#: src/renderer/components/Preferences.tsx:157
+msgctxt "preferences-view"
+msgid "Start minimized"
+msgstr ""
+
+#: src/renderer/components/Preferences.tsx:132
+msgctxt "preferences-view"
+msgid "Use a monochromatic tray icon instead of a colored one."
+msgstr ""
+
+#. Title label in navigation bar
+#: src/renderer/components/SelectLocation.tsx:118
+msgctxt "select-location-nav"
+msgid "Select location"
+msgstr ""
+
+#: src/renderer/components/SelectLocation.tsx:126
+msgctxt "select-location-view"
+msgid "Select location"
+msgstr ""
+
+#: src/renderer/components/SelectLocation.tsx:129
+msgctxt "select-location-view"
+msgid "While connected, your real location is masked with a private and secure location in the selected region"
+msgstr ""
+
+#: src/renderer/components/Settings.tsx:100
+msgctxt "settings-view"
+msgid "Account"
+msgstr ""
+
+#: src/renderer/components/Settings.tsx:114
+msgctxt "settings-view"
+msgid "Advanced"
+msgstr ""
+
+#: src/renderer/components/Settings.tsx:160
+msgctxt "settings-view"
+msgid "App version"
+msgstr ""
+
+#: src/renderer/components/Settings.tsx:181
+msgctxt "settings-view"
+msgid "FAQs & Guides"
+msgstr ""
+
+#: src/renderer/components/Settings.tsx:126
+msgctxt "settings-view"
+msgid "Inconsistent internal version information, please restart the app."
+msgstr ""
+
+#: src/renderer/components/Settings.tsx:94
+msgctxt "settings-view"
+msgid "OUT OF TIME"
+msgstr ""
+
+#: src/renderer/components/Settings.tsx:109
+msgctxt "settings-view"
+msgid "Preferences"
+msgstr ""
+
+#: src/renderer/components/Settings.tsx:78
+msgctxt "settings-view"
+msgid "Quit app"
+msgstr ""
+
+#: src/renderer/components/Settings.tsx:176
+msgctxt "settings-view"
+msgid "Report a problem"
+msgstr ""
+
+#: src/renderer/components/Settings.tsx:56
+msgctxt "settings-view"
+msgid "Settings"
+msgstr ""
+
+#: src/renderer/components/Settings.tsx:131
+msgctxt "settings-view"
+msgid "Update available, download to remain safe."
+msgstr ""
+
+#. Title label in navigation bar
+#: src/renderer/components/Settings.tsx:48
+msgctxt "settings-view-nav"
+msgid "Settings"
+msgstr ""
+
+#. Back button in navigation bar
+#: src/renderer/components/Support.tsx:152
+msgctxt "support-nav"
+msgid "Settings"
+msgstr ""
+
+#: src/renderer/components/Support.tsx:398
+msgctxt "support-view"
+msgid "Back"
+msgstr ""
+
+#: src/renderer/components/Support.tsx:258
+msgctxt "support-view"
+msgid "Describe your problem"
+msgstr ""
+
+#: src/renderer/components/Support.tsx:363
+msgctxt "support-view"
+msgid "Edit message"
+msgstr ""
+
+#: src/renderer/components/Support.tsx:351
+msgctxt "support-view"
+msgid "Failed to send"
+msgstr ""
+
+#: src/renderer/components/Support.tsx:303
+msgctxt "support-view"
+msgid "If needed we will contact you on %(email)s"
+msgstr ""
+
+#: src/renderer/components/Support.tsx:129
+msgctxt "support-view"
+msgid "Report a problem"
+msgstr ""
+
+#: src/renderer/components/Support.tsx:288
+#: src/renderer/components/Support.tsx:323
+#: src/renderer/components/Support.tsx:348
+msgctxt "support-view"
+msgid "SECURE CONNECTION"
+msgstr ""
+
+#: src/renderer/components/Support.tsx:271
+msgctxt "support-view"
+msgid "Send"
+msgstr ""
+
+#: src/renderer/components/Support.tsx:395
+msgctxt "support-view"
+msgid "Send anyway"
+msgstr ""
+
+#: src/renderer/components/Support.tsx:291
+msgctxt "support-view"
+msgid "Sending..."
+msgstr ""
+
+#: src/renderer/components/Support.tsx:325
+msgctxt "support-view"
+msgid "Sent"
+msgstr ""
+
+#: src/renderer/components/Support.tsx:328
+msgctxt "support-view"
+msgid "Thanks! We will look into this."
+msgstr ""
+
+#: src/renderer/components/Support.tsx:132
+msgctxt "support-view"
+msgid "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."
+msgstr ""
+
+#: src/renderer/components/Support.tsx:366
+msgctxt "support-view"
+msgid "Try again"
+msgstr ""
+
+#: src/renderer/components/Support.tsx:267
+msgctxt "support-view"
+msgid "View app logs"
+msgstr ""
+
+#: src/renderer/components/Support.tsx:389
+msgctxt "support-view"
+msgid "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."
+msgstr ""
+
+#: src/renderer/components/Support.tsx:354
+msgctxt "support-view"
+msgid "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."
+msgstr ""
+
+#: src/renderer/components/Support.tsx:248
+msgctxt "support-view"
+msgid "Your email (optional)"
+msgstr ""
+
+#: src/renderer/components/TunnelControl.tsx:120
+msgctxt "tunnel-control"
+msgid "Cancel"
+msgstr ""
+
+#: src/renderer/components/TunnelControl.tsx:114
+msgctxt "tunnel-control"
+msgid "Disconnect"
+msgstr ""
+
+#: src/renderer/components/TunnelControl.tsx:108
+msgctxt "tunnel-control"
+msgid "Secure my connection"
+msgstr ""
+
+#: src/renderer/components/TunnelControl.tsx:92
+msgctxt "tunnel-control"
+msgid "Switch location"
+msgstr ""
diff --git a/gui/packages/desktop/locales/nl/.gitkeep b/gui/packages/desktop/locales/nl/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/gui/packages/desktop/locales/nl/.gitkeep
diff --git a/gui/packages/desktop/locales/no/.gitkeep b/gui/packages/desktop/locales/no/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/gui/packages/desktop/locales/no/.gitkeep
diff --git a/gui/packages/desktop/locales/pt/.gitkeep b/gui/packages/desktop/locales/pt/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/gui/packages/desktop/locales/pt/.gitkeep
diff --git a/gui/packages/desktop/locales/ru/.gitkeep b/gui/packages/desktop/locales/ru/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/gui/packages/desktop/locales/ru/.gitkeep
diff --git a/gui/packages/desktop/locales/sv/.gitkeep b/gui/packages/desktop/locales/sv/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/gui/packages/desktop/locales/sv/.gitkeep
diff --git a/gui/packages/desktop/locales/tr/.gitkeep b/gui/packages/desktop/locales/tr/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/gui/packages/desktop/locales/tr/.gitkeep
diff --git a/gui/packages/desktop/locales/zh/.gitkeep b/gui/packages/desktop/locales/zh/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/gui/packages/desktop/locales/zh/.gitkeep
diff --git a/gui/packages/desktop/package.json b/gui/packages/desktop/package.json
index b3e658dd8a..c334cd8e28 100644
--- a/gui/packages/desktop/package.json
+++ b/gui/packages/desktop/package.json
@@ -14,14 +14,18 @@
"dependencies": {
"@mullvad/components": "0.1.0",
"@mullvad/config": "0.1.0",
+ "@types/node-gettext": "^2.0.0",
+ "@types/sprintf-js": "^1.1.2",
"JSONStream": "^1.3.5",
"connected-react-router": "^5.0.1",
"d3-geo-projection": "^2.4.1",
"electron-log": "^2.2.8",
+ "gettext-parser": "^3.1.0",
"history": "^4.6.1",
"jsonrpc-lite": "^2.0.1",
"mkdirp": "^0.5.1",
"moment": "^2.20.1",
+ "node-gettext": "^2.0.0",
"rbush": "^2.0.2",
"react": "^16.5.0",
"react-dom": "^16.5.0",
@@ -30,6 +34,7 @@
"react-simple-maps": "^0.12.1",
"reactxp": "^1.5.0",
"redux": "^4.0.1",
+ "sprintf-js": "^1.1.2",
"uuid": "^3.0.1",
"validated": "^2.0.1"
},
@@ -63,6 +68,7 @@
"electron-mocha": "^6.0.4",
"enzyme": "^3.7.0",
"enzyme-adapter-react-16": "^1.7.0",
+ "gettext-extractor": "^3.4.2",
"mock-socket": "^8.0.5",
"npm-run-all": "^4.0.1",
"rimraf": "^2.5.4",
@@ -78,6 +84,7 @@
"lint": "tslint -t stylish -p .",
"develop": "cross-env run-s private:copy-assets private:watch",
"test": "electron-mocha --renderer -R spec --require ts-node/register --require-main ts-node/register --require-main \"test/setup/main.ts\" --preload \"test/setup/renderer.ts\" \"test/*.spec.ts\" \"test/**/*.spec.ts\" \"test/**/*.spec.tsx\" || true",
+ "update-translations": "node scripts/extract-translations && ./scripts/update-translations.sh",
"pack:mac": "run-s build private:build:mac private:postbuild:mac",
"pack:win": "run-s build private:build:win",
"pack:linux": "run-s build private:build:linux",
@@ -86,10 +93,11 @@
"private:build:win": "yarn run private:build --win",
"private:build:linux": "yarn run private:build --linux",
"private:build": "electron-builder",
- "private:copy-assets": "run-s private:assets:main private:assets:html private:assets:css",
+ "private:copy-assets": "run-s private:assets:main private:assets:html private:assets:css private:assets:locales",
"private:assets:main": "cross-env mkdir -p ./build/assets && cp -R ./assets ./build",
"private:assets:html": "cross-env mkdir -p ./build/src/renderer && cp ./src/renderer/index.html ./build/src/renderer",
"private:assets:css": "cross-env mkdir -p ./build/src/renderer/components && cp ./src/renderer/components/*.css ./build/src/renderer/components",
+ "private:assets:locales": "cross-env mkdir -p ./build/locales && cp -R ./locales ./build",
"private:watch": "cross-env node \"scripts/serve.js\"",
"private:compile": "tsc",
"private:clean": "rimraf build"
diff --git a/gui/packages/desktop/scripts/extract-translations.js b/gui/packages/desktop/scripts/extract-translations.js
new file mode 100644
index 0000000000..07383ae553
--- /dev/null
+++ b/gui/packages/desktop/scripts/extract-translations.js
@@ -0,0 +1,48 @@
+const { GettextExtractor, JsExtractors, HtmlExtractors } = require('gettext-extractor');
+const path = require('path');
+
+const extractor = new GettextExtractor();
+const outputPotFile = path.resolve(__dirname, '../locales/messages.pot');
+const comments = {
+ otherLineLeading: true,
+ sameLineLeading: true,
+ regex: /^TRANSLATORS:\s*(.*)$/,
+};
+
+extractor
+ .createJsParser([
+ JsExtractors.callExpression('gettext', {
+ arguments: {
+ text: 0,
+ },
+ comments,
+ }),
+ JsExtractors.callExpression('pgettext', {
+ arguments: {
+ context: 0,
+ text: 1,
+ },
+ comments,
+ }),
+ JsExtractors.callExpression('ngettext', {
+ arguments: {
+ text: 0,
+ textPlural: 1,
+ },
+ comments,
+ }),
+ JsExtractors.callExpression('npgettext', {
+ arguments: {
+ context: 0,
+ text: 1,
+ textPlural: 2,
+ },
+ comments,
+ }),
+ ])
+ .parseFilesGlob('./src/**/*.@(ts|tsx)', {
+ cwd: path.resolve(__dirname, '..'),
+ });
+
+extractor.savePotFile(outputPotFile);
+extractor.printStats();
diff --git a/gui/packages/desktop/scripts/update-translations.sh b/gui/packages/desktop/scripts/update-translations.sh
new file mode 100755
index 0000000000..030d0c78a8
--- /dev/null
+++ b/gui/packages/desktop/scripts/update-translations.sh
@@ -0,0 +1,25 @@
+#!/usr/bin/env bash
+
+# This script creates or updates the existing gettext catalogues using the POT template located
+# under locales/messages.pot
+
+ROOT_DIR=$(dirname $(dirname "${BASH_SOURCE[0]}"))
+POT_FILE="$ROOT_DIR/locales/messages.pot"
+
+for PO_FILE_DIR in $ROOT_DIR/locales/* ; do
+ if [ -d $PO_FILE_DIR ] ; then
+ PO_FILE="$PO_FILE_DIR/messages.po"
+ GITKEEP_FILE="$PO_FILE_DIR/.gitkeep"
+
+ if [ -f $PO_FILE ] ; then
+ echo "Update $PO_FILE_DIR\c"
+ msgmerge --no-fuzzy-matching --update $PO_FILE $POT_FILE
+ else
+ if [ -f $GITKEEP_FILE ] ; then
+ echo "Remove $GITKEEP_FILE to initialize the new translation"
+ else
+ msginit --input $POT_FILE --output $PO_FILE --no-translator
+ fi
+ fi
+ fi
+done
diff --git a/gui/packages/desktop/src/main/index.ts b/gui/packages/desktop/src/main/index.ts
index bd56ed0423..c017e1e61c 100644
--- a/gui/packages/desktop/src/main/index.ts
+++ b/gui/packages/desktop/src/main/index.ts
@@ -14,6 +14,7 @@ import {
RelaySettingsUpdate,
TunnelStateTransition,
} from '../shared/daemon-rpc-types';
+import { loadTranslations } from '../shared/gettext';
import { IpcMainEventChannel } from '../shared/ipc-event-channel';
import { getOpenAtLogin, setOpenAtLogin } from './autostart';
import { ConnectionObserver, DaemonRpc, SubscriptionListener } from './daemon-rpc';
@@ -133,22 +134,10 @@ class ApplicationMain {
this.guiSettings.load();
- app.on('activate', () => this.onActivate());
- app.on('ready', () => this.onReady());
+ app.on('activate', this.onActivate);
+ app.on('ready', this.onReady);
app.on('window-all-closed', () => app.quit());
- app.on('before-quit', (event: Event) => this.onBeforeQuit(event));
-
- const connectionObserver = new ConnectionObserver(
- () => {
- this.onDaemonConnected();
- },
- (error) => {
- this.onDaemonDisconnected(error);
- },
- );
-
- this.daemonRpc.addConnectionObserver(connectionObserver);
- this.connectToDaemon();
+ app.on('before-quit', this.onBeforeQuit);
}
private ensureSingleInstance() {
@@ -230,13 +219,13 @@ class ApplicationMain {
}
}
- private onActivate() {
+ private onActivate = () => {
if (this.windowController) {
this.windowController.show();
}
- }
+ };
- private async onBeforeQuit(event: Event) {
+ private onBeforeQuit = async (event: Event) => {
switch (this.quitStage) {
case AppQuitStage.unready:
// postpone the app shutdown
@@ -259,7 +248,7 @@ class ApplicationMain {
// let the app quit freely at this point
break;
}
- }
+ };
private async prepareToQuit() {
if (this.connectedToDaemon) {
@@ -274,7 +263,14 @@ class ApplicationMain {
}
}
- private async onReady() {
+ private onReady = async () => {
+ loadTranslations(app.getLocale());
+
+ this.daemonRpc.addConnectionObserver(
+ new ConnectionObserver(this.onDaemonConnected, this.onDaemonDisconnected),
+ );
+ this.connectToDaemon();
+
const window = this.createWindow();
const tray = this.createTray();
@@ -338,9 +334,9 @@ class ApplicationMain {
}
window.loadFile(path.resolve(path.join(__dirname, '../renderer/index.html')));
- }
+ };
- private async onDaemonConnected() {
+ private onDaemonConnected = async () => {
this.connectedToDaemon = true;
// subscribe to events
@@ -420,9 +416,9 @@ class ApplicationMain {
if (this.windowController) {
IpcMainEventChannel.daemonConnected.notify(this.windowController.webContents);
}
- }
+ };
- private onDaemonDisconnected(error?: Error) {
+ private onDaemonDisconnected = (error?: Error) => {
// make sure we were connected before to distinguish between a failed attempt to reconnect and
// connection loss.
const wasConnected = this.connectedToDaemon;
@@ -455,7 +451,7 @@ class ApplicationMain {
} else {
log.info('Disconnected from the daemon');
}
- }
+ };
private connectToDaemon() {
this.daemonRpc.connect({ path: DAEMON_RPC_PATH });
diff --git a/gui/packages/desktop/src/main/notification-controller.ts b/gui/packages/desktop/src/main/notification-controller.ts
index 0ecad34566..e1df2823b8 100644
--- a/gui/packages/desktop/src/main/notification-controller.ts
+++ b/gui/packages/desktop/src/main/notification-controller.ts
@@ -1,8 +1,9 @@
import { app, nativeImage, NativeImage, Notification, shell } from 'electron';
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';
export default class NotificationController {
private lastTunnelStateAnnouncement?: { body: string; notification: Notification };
@@ -25,22 +26,24 @@ export default class NotificationController {
switch (tunnelState.state) {
case 'connecting':
if (!this.reconnecting) {
- this.showTunnelStateNotification('Connecting');
+ this.showTunnelStateNotification(pgettext('notifications', 'Connecting'));
}
break;
case 'connected':
- this.showTunnelStateNotification('Secured');
+ this.showTunnelStateNotification(pgettext('notifications', 'Secured'));
break;
case 'disconnected':
- this.showTunnelStateNotification('Unsecured');
+ this.showTunnelStateNotification(pgettext('notifications', 'Unsecured'));
break;
case 'blocked':
switch (tunnelState.details.reason) {
case 'set_firewall_policy_error':
- this.showTunnelStateNotification('Critical failure - Unsecured');
+ this.showTunnelStateNotification(
+ pgettext('notifications', 'Critical failure - Unsecured'),
+ );
break;
default:
- this.showTunnelStateNotification('Blocked all connections');
+ this.showTunnelStateNotification(pgettext('notifications', 'Blocked all connections'));
break;
}
break;
@@ -51,7 +54,7 @@ export default class NotificationController {
// no-op
break;
case 'reconnect':
- this.showTunnelStateNotification('Reconnecting');
+ this.showTunnelStateNotification(pgettext('notifications', 'Reconnecting'));
this.reconnecting = true;
return;
}
@@ -65,7 +68,10 @@ export default class NotificationController {
this.presentNotificationOnce('inconsistent-version', () => {
const notification = new Notification({
title: this.notificationTitle,
- body: 'Inconsistent internal version information, please restart the app',
+ body: pgettext(
+ 'notifications',
+ 'Inconsistent internal version information, please restart the app',
+ ),
silent: true,
icon: this.notificationIcon,
});
@@ -77,7 +83,18 @@ export default class NotificationController {
this.presentNotificationOnce('unsupported-version', () => {
const notification = new Notification({
title: this.notificationTitle,
- body: `You are running an unsupported app version. Please upgrade to ${upgradeVersion} now to ensure your security`,
+ body: sprintf(
+ // 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(
+ 'notifications',
+ 'You are running an unsupported app version. Please upgrade to %(version)s now to ensure your security',
+ ),
+ {
+ version: upgradeVersion,
+ },
+ ),
silent: true,
icon: this.notificationIcon,
});
diff --git a/gui/packages/desktop/src/main/proc.ts b/gui/packages/desktop/src/main/proc.ts
index 5b118c1ec8..b9c1bbe743 100644
--- a/gui/packages/desktop/src/main/proc.ts
+++ b/gui/packages/desktop/src/main/proc.ts
@@ -7,7 +7,8 @@ export function resolveBin(binaryName: string) {
function getBasePath(): string {
if (process.env.NODE_ENV === 'development') {
return (
- process.env.MULLVAD_PATH || path.resolve(path.join(__dirname, '../../../../../target/debug'))
+ process.env.MULLVAD_PATH ||
+ path.resolve(path.join(__dirname, '../../../../../../target/debug'))
);
} else {
return process.resourcesPath!;
diff --git a/gui/packages/desktop/src/renderer/app.tsx b/gui/packages/desktop/src/renderer/app.tsx
index fe4047187f..8b5678f0eb 100644
--- a/gui/packages/desktop/src/renderer/app.tsx
+++ b/gui/packages/desktop/src/renderer/app.tsx
@@ -3,7 +3,7 @@ import {
push as pushHistory,
replace as replaceHistory,
} from 'connected-react-router';
-import { ipcRenderer, webFrame } from 'electron';
+import { ipcRenderer, remote, webFrame } from 'electron';
import log from 'electron-log';
import { createMemoryHistory } from 'history';
import * as React from 'react';
@@ -22,6 +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 { IGuiSettingsState } from '../shared/gui-settings-state';
import { IpcRendererEventChannel } from '../shared/ipc-event-channel';
@@ -165,6 +166,9 @@ export default class AppRenderer {
// disable pinch to zoom
webFrame.setVisualZoomLevelLimits(1, 1);
+
+ // Load translations
+ loadTranslations(remote.app.getLocale());
}
public renderView() {
diff --git a/gui/packages/desktop/src/renderer/components/Account.tsx b/gui/packages/desktop/src/renderer/components/Account.tsx
index e49bfc0a61..4906ab899d 100644
--- a/gui/packages/desktop/src/renderer/components/Account.tsx
+++ b/gui/packages/desktop/src/renderer/components/Account.tsx
@@ -2,6 +2,7 @@ import { ClipboardLabel, HeaderTitle, SettingsHeader } from '@mullvad/components
import moment from 'moment';
import * as React from 'react';
import { Component, Text, View } from 'reactxp';
+import { pgettext } from '../../shared/gettext';
import styles from './AccountStyles';
import * as AppButton from './AppButton';
import { Container, Layout } from './Layout';
@@ -26,22 +27,27 @@ export default class Account extends Component<IProps> {
<Container>
<View style={styles.account}>
<NavigationBar>
- <BackBarItem action={this.props.onClose}>Settings</BackBarItem>
+ <BackBarItem action={this.props.onClose}>
+ {// TRANSLATORS: Back button in navigation bar
+ pgettext('account-nav', 'Settings')}
+ </BackBarItem>
</NavigationBar>
<View style={styles.account__container}>
<SettingsHeader>
- <HeaderTitle>Account</HeaderTitle>
+ <HeaderTitle>{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}>Account ID</Text>
+ <Text style={styles.account__row_label}>
+ {pgettext('account-view', 'Account ID')}
+ </Text>
<ClipboardLabel
style={styles.account__row_value}
value={this.props.accountToken || ''}
- message={'COPIED TO CLIPBOARD!'}
+ message={pgettext('account-view', 'COPIED TO CLIPBOARD!')}
/>
</View>
@@ -58,11 +64,13 @@ export default class Account extends Component<IProps> {
style={styles.account__buy_button}
disabled={this.props.isOffline}
onPress={this.props.onBuyMore}>
- <AppButton.Label>Buy more credit</AppButton.Label>
+ <AppButton.Label>
+ {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}>
- {'Log out'}
+ {pgettext('account-view', 'Log out')}
</AppButton.RedButton>
</View>
</View>
@@ -77,13 +85,19 @@ export default class Account extends Component<IProps> {
function FormattedAccountExpiry(props: { expiry?: string; locale: string }) {
if (!props.expiry) {
- return <Text style={styles.account__row_value}>{'Currently unavailable'}</Text>;
+ return (
+ <Text style={styles.account__row_value}>
+ {pgettext('account-view', 'Currently unavailable')}
+ </Text>
+ );
}
const expiry = moment(props.expiry);
if (expiry.isSameOrBefore(moment())) {
- return <Text style={styles.account__out_of_time}>{'OUT OF TIME'}</Text>;
+ return (
+ <Text style={styles.account__out_of_time}>{pgettext('account-view', 'OUT OF TIME')}</Text>
+ );
}
const formatOptions = {
diff --git a/gui/packages/desktop/src/renderer/components/AdvancedSettings.tsx b/gui/packages/desktop/src/renderer/components/AdvancedSettings.tsx
index 7fdcb609eb..c60980444d 100644
--- a/gui/packages/desktop/src/renderer/components/AdvancedSettings.tsx
+++ b/gui/packages/desktop/src/renderer/components/AdvancedSettings.tsx
@@ -1,8 +1,10 @@
import { HeaderTitle, SettingsHeader } from '@mullvad/components';
import * as React from 'react';
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 styles from './AdvancedSettingsStyles';
import * as Cell from './Cell';
import { Container, Layout } from './Layout';
@@ -87,25 +89,36 @@ export default class AdvancedSettings extends Component<IProps, IState> {
<View style={styles.advanced_settings}>
<NavigationContainer>
<NavigationBar>
- <BackBarItem action={this.props.onClose}>Settings</BackBarItem>
- <TitleBarItem>Advanced</TitleBarItem>
+ <BackBarItem action={this.props.onClose}>
+ {// TRANSLATORS: Back button in navigation bar
+ pgettext('advanced-settings-nav', 'Settings')}
+ </BackBarItem>
+ <TitleBarItem>
+ {// TRANSLATORS: Title label in navigation bar
+ pgettext('advanced-settings-nav', 'Advanced')}
+ </TitleBarItem>
</NavigationBar>
<View style={styles.advanced_settings__container}>
<NavigationScrollbars style={styles.advanced_settings__scrollview}>
<SettingsHeader>
- <HeaderTitle>Advanced</HeaderTitle>
+ <HeaderTitle>{pgettext('advanced-settings-view', 'Advanced')}</HeaderTitle>
</SettingsHeader>
<Cell.Container>
- <Cell.Label>Enable IPv6</Cell.Label>
+ <Cell.Label>{pgettext('advanced-settings-view', 'Enable IPv6')}</Cell.Label>
<Switch isOn={this.props.enableIpv6} onChange={this.props.setEnableIpv6} />
</Cell.Container>
- <Cell.Footer>Enable IPv6 communication through the tunnel.</Cell.Footer>
+ <Cell.Footer>
+ {pgettext(
+ 'advanced-settings-view',
+ 'Enable IPv6 communication through the tunnel.',
+ )}
+ </Cell.Footer>
<Cell.Container>
<Cell.Label textStyle={styles.advanced_settings__block_when_disconnected_label}>
- Block when disconnected
+ {pgettext('advanced-settings-view', 'Block when disconnected')}
</Cell.Label>
<Switch
isOn={this.props.blockWhenDisconnected}
@@ -113,14 +126,15 @@ export default class AdvancedSettings extends Component<IProps, IState> {
/>
</Cell.Container>
<Cell.Footer>
- {
- "Unless connected, always block all network traffic, even when you've disconnected or quit the app."
- }
+ {pgettext(
+ 'advanced-settings-view',
+ "Unless connected, always block all network traffic, even when you've disconnected or quit the app.",
+ )}
</Cell.Footer>
<View style={styles.advanced_settings__content}>
<Selector
- title={'Network protocols'}
+ title={pgettext('advanced-settings-view', 'Network protocols')}
values={PROTOCOL_ITEMS}
value={this.props.protocol}
onSelect={this.onSelectProtocol}
@@ -130,7 +144,15 @@ export default class AdvancedSettings extends Component<IProps, IState> {
{this.props.protocol ? (
<Selector
- title={`${this.props.protocol.toUpperCase()} port`}
+ title={sprintf(
+ // 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'),
+ {
+ portType: this.props.protocol.toUpperCase(),
+ },
+ )}
values={PORT_ITEMS[this.props.protocol]}
value={this.props.port}
onSelect={this.onSelectPort}
@@ -141,12 +163,12 @@ export default class AdvancedSettings extends Component<IProps, IState> {
</View>
<Cell.Container>
- <Cell.Label>Mssfix</Cell.Label>
+ <Cell.Label>{pgettext('advanced-settings-view', 'Mssfix')}</Cell.Label>
<Cell.InputFrame style={styles.advanced_settings__mssfix_frame}>
<Cell.Input
keyboardType={'numeric'}
maxLength={4}
- placeholder={'Default'}
+ placeholder={pgettext('advanced-settings-view', 'Default')}
value={mssfixValue ? mssfixValue.toString() : ''}
style={mssfixStyle}
onChangeText={this.onMssfixChange}
@@ -156,7 +178,20 @@ export default class AdvancedSettings extends Component<IProps, IState> {
</Cell.InputFrame>
</Cell.Container>
<Cell.Footer>
- Set OpenVPN MSS value. Valid range: {MIN_MSSFIX_VALUE} - {MAX_MSSFIX_VALUE}.
+ {sprintf(
+ // TRANSLATORS: The hint displayed below the Mssfix input field.
+ // TRANSLATORS: Available placeholders:
+ // TRANSLATORS: %(max)d - the maximum possible mssfix value
+ // TRANSLATORS: %(min)d - the minimum possible mssfix value
+ pgettext(
+ 'advanced-settings-view',
+ 'Set OpenVPN MSS value. Valid range: %(min)d - %(max)d.',
+ ),
+ {
+ min: MIN_MSSFIX_VALUE,
+ max: MAX_MSSFIX_VALUE,
+ },
+ )}
</Cell.Footer>
</NavigationScrollbars>
</View>
@@ -226,7 +261,7 @@ class Selector<T> extends Component<ISelectorProps<T>> {
key={'auto'}
selected={this.props.value === undefined}
onSelect={this.props.onSelect}>
- {'Automatic'}
+ {pgettext('advanced-settings-view', 'Automatic')}
</SelectorCell>
{this.props.values.map((item, i) => (
<SelectorCell
diff --git a/gui/packages/desktop/src/renderer/components/Connect.tsx b/gui/packages/desktop/src/renderer/components/Connect.tsx
index 6d9552cdc6..0283450756 100644
--- a/gui/packages/desktop/src/renderer/components/Connect.tsx
+++ b/gui/packages/desktop/src/renderer/components/Connect.tsx
@@ -4,6 +4,7 @@ import { Component, View } from 'reactxp';
import { links } from '../../config.json';
import { NoCreditError, NoInternetError } from '../../main/errors';
import { ITunnelEndpoint, parseSocketAddress } from '../../shared/daemon-rpc-types';
+import { pgettext } from '../../shared/gettext';
import * as AppButton from './AppButton';
import styles from './ConnectStyles';
import { Container, Header, Layout } from './Layout';
@@ -53,13 +54,21 @@ export default class Connect extends Component<IProps> {
let message = '';
if (error instanceof NoCreditError) {
- title = 'Out of time';
- message = 'Buy more time, so you can continue using the internet securely';
+ title = pgettext('connect-view', 'Out of time');
+
+ message = pgettext(
+ 'connect-view',
+ 'Buy more time, so you can continue using the internet securely',
+ );
}
if (error instanceof NoInternetError) {
- title = 'Offline';
- message = 'Your internet connection will be secured when you get back online';
+ title = pgettext('connect-view', 'Offline');
+
+ message = pgettext(
+ 'connect-view',
+ 'Your internet connection will be secured when you get back online',
+ );
}
const { isBlocked } = this.props.connection;
diff --git a/gui/packages/desktop/src/renderer/components/Launch.tsx b/gui/packages/desktop/src/renderer/components/Launch.tsx
index facf5942e5..e1aab10a9f 100644
--- a/gui/packages/desktop/src/renderer/components/Launch.tsx
+++ b/gui/packages/desktop/src/renderer/components/Launch.tsx
@@ -2,6 +2,7 @@ import { ImageView, SettingsBarButton } from '@mullvad/components';
import * as React from 'react';
import { Component, Styles, Text, View } from 'reactxp';
import { colors } from '../../config.json';
+import { pgettext } from '../../shared/gettext';
import { Container, Header, Layout } from './Layout';
const styles = {
@@ -46,8 +47,10 @@ 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}>{'MULLVAD VPN'}</Text>
- <Text style={styles.subtitle}>{'Connecting to daemon...'}</Text>
+ <Text style={styles.title}>{pgettext('launch-view', 'MULLVAD VPN')}</Text>
+ <Text style={styles.subtitle}>
+ {pgettext('launch-view', 'Connecting to daemon...')}
+ </Text>
</View>
</Container>
</Layout>
diff --git a/gui/packages/desktop/src/renderer/components/Login.tsx b/gui/packages/desktop/src/renderer/components/Login.tsx
index 4327bc1f60..90e53ce9d7 100644
--- a/gui/packages/desktop/src/renderer/components/Login.tsx
+++ b/gui/packages/desktop/src/renderer/components/Login.tsx
@@ -2,6 +2,7 @@ import { Accordion, Brand, ImageView, SettingsBarButton } from '@mullvad/compone
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 * as AppButton from './AppButton';
import * as Cell from './Cell';
import { Container, Header, Layout } from './Layout';
@@ -209,13 +210,13 @@ export default class Login extends Component<IProps, IState> {
private formTitle() {
switch (this.props.loginState) {
case 'logging in':
- return 'Logging in...';
+ return pgettext('login-view', 'Logging in...');
case 'failed':
- return 'Login failed';
+ return pgettext('login-view', 'Login failed');
case 'ok':
- return 'Logged in';
+ return pgettext('login-view', 'Logged in');
default:
- return 'Login';
+ return pgettext('login-view', 'Login');
}
}
@@ -223,13 +224,13 @@ export default class Login extends Component<IProps, IState> {
const { loginState, loginError } = this.props;
switch (loginState) {
case 'failed':
- return (loginError && loginError.message) || 'Unknown error';
+ return (loginError && loginError.message) || pgettext('login-view', 'Unknown error');
case 'logging in':
- return 'Checking account number';
+ return pgettext('login-view', 'Checking account number');
case 'ok':
- return 'Correct account number';
+ return pgettext('login-view', 'Correct account number');
default:
- return 'Enter your account number';
+ return pgettext('login-view', 'Enter your account number');
}
}
@@ -393,9 +394,11 @@ export default class Login extends Component<IProps, IState> {
private createFooter() {
return (
<View>
- <Text style={styles.login_footer__prompt}>{"Don't have an account number?"}</Text>
+ <Text style={styles.login_footer__prompt}>
+ {pgettext('login-view', "Don't have an account number?")}
+ </Text>
<AppButton.BlueButton onPress={this.onCreateAccount}>
- <AppButton.Label>Create account</AppButton.Label>
+ <AppButton.Label>{pgettext('login-view', 'Create account')}</AppButton.Label>
<AppButton.Icon source="icon-extLink" height={16} width={16} />
</AppButton.BlueButton>
</View>
diff --git a/gui/packages/desktop/src/renderer/components/NotificationArea.tsx b/gui/packages/desktop/src/renderer/components/NotificationArea.tsx
index 0f339c2979..bf2969480b 100644
--- a/gui/packages/desktop/src/renderer/components/NotificationArea.tsx
+++ b/gui/packages/desktop/src/renderer/components/NotificationArea.tsx
@@ -1,7 +1,9 @@
import moment from 'moment';
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 {
NotificationActions,
NotificationBanner,
@@ -44,19 +46,31 @@ function getBlockReasonMessage(blockReason: BlockReason): string {
return new AuthFailure(blockReason.details).show();
}
case 'ipv6_unavailable':
- return 'Could not configure IPv6, please enable it on your system or disable it in the app';
+ return 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 'Failed to apply firewall rules. The device might currently be unsecured';
+ return pgettext(
+ 'in-app-notifications',
+ 'Failed to apply firewall rules. The device might currently be unsecured',
+ );
case 'set_dns_error':
- return 'Failed to set system DNS server';
+ return pgettext('in-app-notifications', 'Failed to set system DNS server');
case 'start_tunnel_error':
- return 'Failed to start tunnel connection';
+ return pgettext('in-app-notifications', 'Failed to start tunnel connection');
case 'no_matching_relay':
- return 'No relay server matches the current settings';
+ return pgettext('in-app-notifications', 'No relay server matches the current settings');
case 'is_offline':
- return 'This device is offline, no tunnels can be established';
+ return pgettext(
+ 'in-app-notifications',
+ 'This device is offline, no tunnels can be established',
+ );
case 'tap_adapter_problem':
- return "Unable to detect a working TAP adapter on this device. If you've disabled it, enable it again. Otherwise, please reinstall the app";
+ return 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",
+ );
}
}
@@ -164,7 +178,9 @@ export default class NotificationArea extends Component<IProps, State> {
<React.Fragment>
<NotificationIndicator type={'error'} />
<NotificationContent>
- <NotificationTitle>{'FAILURE - UNSECURED'}</NotificationTitle>
+ <NotificationTitle>
+ {pgettext('in-app-notifications', 'FAILURE - UNSECURED')}
+ </NotificationTitle>
<NotificationSubtitle>{this.state.reason}</NotificationSubtitle>
</NotificationContent>
</React.Fragment>
@@ -174,7 +190,9 @@ export default class NotificationArea extends Component<IProps, State> {
<React.Fragment>
<NotificationIndicator type={'error'} />
<NotificationContent>
- <NotificationTitle>{'BLOCKING INTERNET'}</NotificationTitle>
+ <NotificationTitle>
+ {pgettext('in-app-notifications', 'BLOCKING INTERNET')}
+ </NotificationTitle>
<NotificationSubtitle>{this.state.reason}</NotificationSubtitle>
</NotificationContent>
</React.Fragment>
@@ -184,9 +202,14 @@ export default class NotificationArea extends Component<IProps, State> {
<React.Fragment>
<NotificationIndicator type={'error'} />
<NotificationContent>
- <NotificationTitle>{'INCONSISTENT VERSION'}</NotificationTitle>
+ <NotificationTitle>
+ {pgettext('in-app-notifications', 'INCONSISTENT VERSION')}
+ </NotificationTitle>
<NotificationSubtitle>
- {'Inconsistent internal version information, please restart the app'}
+ {pgettext(
+ 'in-app-notifications',
+ 'Inconsistent internal version information, please restart the app',
+ )}
</NotificationSubtitle>
</NotificationContent>
</React.Fragment>
@@ -196,10 +219,21 @@ export default class NotificationArea extends Component<IProps, State> {
<React.Fragment>
<NotificationIndicator type={'error'} />
<NotificationContent>
- <NotificationTitle>{'UNSUPPORTED VERSION'}</NotificationTitle>
- <NotificationSubtitle>{`You are running an unsupported app version. Please upgrade to ${
- this.state.upgradeVersion
- } now to ensure your security`}</NotificationSubtitle>
+ <NotificationTitle>
+ {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(
+ 'in-app-notifications',
+ 'You are running an unsupported app version. Please upgrade to %(version)s now to ensure your security',
+ ),
+ { version: this.state.upgradeVersion },
+ )}
+ </NotificationSubtitle>
</NotificationContent>
<NotificationActions>
<NotificationOpenLinkAction onPress={this.handleOpenDownloadLink} />
@@ -211,10 +245,21 @@ export default class NotificationArea extends Component<IProps, State> {
<React.Fragment>
<NotificationIndicator type={'warning'} />
<NotificationContent>
- <NotificationTitle>{`UPDATE AVAILABLE`}</NotificationTitle>
- <NotificationSubtitle>{`Install Mullvad VPN (${
- this.state.upgradeVersion
- }) to stay up to date`}</NotificationSubtitle>
+ <NotificationTitle>
+ {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(
+ 'in-app-notifications',
+ 'Install Mullvad VPN (%(version)s) to stay up to date',
+ ),
+ { version: this.state.upgradeVersion },
+ )}
+ </NotificationSubtitle>
</NotificationContent>
<NotificationActions>
<NotificationOpenLinkAction onPress={this.handleOpenDownloadLink} />
@@ -226,7 +271,9 @@ export default class NotificationArea extends Component<IProps, State> {
<React.Fragment>
<NotificationIndicator type={'warning'} />
<NotificationContent>
- <NotificationTitle>{'ACCOUNT CREDIT EXPIRES SOON'}</NotificationTitle>
+ <NotificationTitle>
+ {pgettext('in-app-notifications', 'ACCOUNT CREDIT EXPIRES SOON')}
+ </NotificationTitle>
<NotificationSubtitle>{this.state.timeLeft}</NotificationSubtitle>
</NotificationContent>
<NotificationActions>
diff --git a/gui/packages/desktop/src/renderer/components/Preferences.tsx b/gui/packages/desktop/src/renderer/components/Preferences.tsx
index 91c2c6b050..1e61051e5c 100644
--- a/gui/packages/desktop/src/renderer/components/Preferences.tsx
+++ b/gui/packages/desktop/src/renderer/components/Preferences.tsx
@@ -1,6 +1,7 @@
import { HeaderTitle, SettingsHeader } from '@mullvad/components';
import * as React from 'react';
import { Component, View } from 'reactxp';
+import { pgettext } from '../../shared/gettext';
import * as Cell from './Cell';
import { Container, Layout } from './Layout';
import {
@@ -37,37 +38,53 @@ export default class Preferences extends Component<IPreferencesProps> {
<View style={styles.preferences}>
<NavigationContainer>
<NavigationBar>
- <BackBarItem action={this.props.onClose}>Settings</BackBarItem>
- <TitleBarItem>Preferences</TitleBarItem>
+ <BackBarItem action={this.props.onClose}>
+ {// TRANSLATORS: Back button in navigation bar
+ pgettext('preferences-nav', 'Settings')}
+ </BackBarItem>
+ <TitleBarItem>
+ {// TRANSLATORS: Title label in navigation bar
+ pgettext('preferences-nav', 'Preferences')}
+ </TitleBarItem>
</NavigationBar>
<View style={styles.preferences__container}>
<NavigationScrollbars>
<SettingsHeader>
- <HeaderTitle>Preferences</HeaderTitle>
+ <HeaderTitle>{pgettext('preferences-view', 'Preferences')}</HeaderTitle>
</SettingsHeader>
<View style={styles.preferences__content}>
<Cell.Container>
- <Cell.Label>Launch app on start-up</Cell.Label>
+ <Cell.Label>
+ {pgettext('preferences-view', 'Launch app on start-up')}
+ </Cell.Label>
<Switch isOn={this.props.autoStart} onChange={this.onChangeAutoStart} />
</Cell.Container>
<View style={styles.preferences__separator} />
<Cell.Container>
- <Cell.Label>Auto-connect</Cell.Label>
+ <Cell.Label>{pgettext('preferences-view', 'Auto-connect')}</Cell.Label>
<Switch isOn={this.props.autoConnect} onChange={this.props.setAutoConnect} />
</Cell.Container>
<Cell.Footer>
- Automatically connect to a server when the app launches.
+ {pgettext(
+ 'preferences-view',
+ 'Automatically connect to a server when the app launches.',
+ )}
</Cell.Footer>
<Cell.Container>
- <Cell.Label>Local network sharing</Cell.Label>
+ <Cell.Label>
+ {pgettext('preferences-view', 'Local network sharing')}
+ </Cell.Label>
<Switch isOn={this.props.allowLan} onChange={this.props.setAllowLan} />
</Cell.Container>
<Cell.Footer>
- Allows access to other devices on the same network for sharing, printing etc.
+ {pgettext(
+ 'preferences-view',
+ 'Allows access to other devices on the same network for sharing, printing etc.',
+ )}
</Cell.Footer>
<MonochromaticIconToggle
@@ -108,10 +125,15 @@ class MonochromaticIconToggle extends Component<IMonochromaticIconProps> {
return (
<View>
<Cell.Container>
- <Cell.Label>Monochromatic tray icon</Cell.Label>
+ <Cell.Label>{pgettext('preferences-view', 'Monochromatic tray icon')}</Cell.Label>
<Switch isOn={this.props.monochromaticIcon} onChange={this.props.onChange} />
</Cell.Container>
- <Cell.Footer>Use a monochromatic tray icon instead of a colored one.</Cell.Footer>
+ <Cell.Footer>
+ {pgettext(
+ 'preferences-view',
+ 'Use a monochromatic tray icon instead of a colored one.',
+ )}
+ </Cell.Footer>
</View>
);
} else {
@@ -132,10 +154,12 @@ class StartMinimizedToggle extends Component<IStartMinimizedProps> {
return (
<View>
<Cell.Container>
- <Cell.Label>Start minimized</Cell.Label>
+ <Cell.Label>{pgettext('preferences-view', 'Start minimized')}</Cell.Label>
<Switch isOn={this.props.startMinimized} onChange={this.props.onChange} />
</Cell.Container>
- <Cell.Footer>Show only the tray icon when the app starts.</Cell.Footer>
+ <Cell.Footer>
+ {pgettext('preferences-view', 'Show only the tray icon when the app starts.')}
+ </Cell.Footer>
</View>
);
} else {
diff --git a/gui/packages/desktop/src/renderer/components/SelectLocation.tsx b/gui/packages/desktop/src/renderer/components/SelectLocation.tsx
index ba7a24a79e..9c63e25464 100644
--- a/gui/packages/desktop/src/renderer/components/SelectLocation.tsx
+++ b/gui/packages/desktop/src/renderer/components/SelectLocation.tsx
@@ -2,6 +2,7 @@ import { HeaderSubTitle, HeaderTitle, SettingsHeader } from '@mullvad/components
import * as React from 'react';
import ReactDOM from 'react-dom';
import { Component, View } from 'reactxp';
+import { pgettext } from '../../shared/gettext';
import CustomScrollbars from './CustomScrollbars';
import { Container, Layout } from './Layout';
import {
@@ -112,16 +113,23 @@ export default class SelectLocation extends Component<IProps, IState> {
<NavigationContainer>
<NavigationBar>
<CloseBarItem action={this.props.onClose} />
- <TitleBarItem>{'Select location'}</TitleBarItem>
+ <TitleBarItem>
+ {// TRANSLATORS: Title label in navigation bar
+ pgettext('select-location-nav', 'Select location')}
+ </TitleBarItem>
</NavigationBar>
<View style={styles.container}>
<NavigationScrollbars ref={this.scrollViewRef}>
<View style={styles.content}>
<SettingsHeader style={styles.subtitle_header}>
- <HeaderTitle>Select location</HeaderTitle>
+ <HeaderTitle>
+ {pgettext('select-location-view', 'Select location')}
+ </HeaderTitle>
<HeaderSubTitle>
- While connected, your real location is masked with a private and secure
- location in the selected region
+ {pgettext(
+ 'select-location-view',
+ 'While connected, your real location is masked with a private and secure location in the selected region',
+ )}
</HeaderSubTitle>
</SettingsHeader>
diff --git a/gui/packages/desktop/src/renderer/components/Settings.tsx b/gui/packages/desktop/src/renderer/components/Settings.tsx
index 913911c410..1b1b88926a 100644
--- a/gui/packages/desktop/src/renderer/components/Settings.tsx
+++ b/gui/packages/desktop/src/renderer/components/Settings.tsx
@@ -2,6 +2,7 @@ import { HeaderTitle, ImageView, SettingsHeader } from '@mullvad/components';
import * as React from 'react';
import { Component, Text, View } from 'reactxp';
import { colors, links } from '../../config.json';
+import { pgettext } from '../../shared/gettext';
import AccountExpiry from '../lib/account-expiry';
import * as AppButton from './AppButton';
import * as Cell from './Cell';
@@ -42,14 +43,17 @@ export default class Settings extends Component<IProps> {
<NavigationContainer>
<NavigationBar>
<CloseBarItem action={this.props.onClose} />
- <TitleBarItem>Settings</TitleBarItem>
+ <TitleBarItem>
+ {// TRANSLATORS: Title label in navigation bar
+ pgettext('settings-view-nav', 'Settings')}
+ </TitleBarItem>
</NavigationBar>
<View style={styles.settings__container}>
<NavigationScrollbars style={styles.settings__scrollview}>
<View style={styles.settings__content}>
<SettingsHeader>
- <HeaderTitle>Settings</HeaderTitle>
+ <HeaderTitle>{pgettext('settings-view', 'Settings')}</HeaderTitle>
</SettingsHeader>
<View>
{this.renderTopButtons()}
@@ -70,7 +74,9 @@ export default class Settings extends Component<IProps> {
private renderQuitButton() {
return (
<View style={styles.settings__footer}>
- <AppButton.RedButton onPress={this.props.onQuit}>{'Quit app'}</AppButton.RedButton>
+ <AppButton.RedButton onPress={this.props.onQuit}>
+ {pgettext('settings-view', 'Quit app')}
+ </AppButton.RedButton>
</View>
);
}
@@ -85,33 +91,27 @@ 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');
+
return (
<View>
<View>
- {isOutOfTime ? (
- <Cell.CellButton onPress={this.props.onViewAccount}>
- <Cell.Label>Account</Cell.Label>
- <Cell.SubText style={styles.settings__account_paid_until_label__error}>
- {'OUT OF TIME'}
- </Cell.SubText>
- <Cell.Icon height={12} width={7} source="icon-chevron" />
- </Cell.CellButton>
- ) : (
- <Cell.CellButton onPress={this.props.onViewAccount}>
- <Cell.Label>Account</Cell.Label>
- <Cell.SubText>{formattedExpiry}</Cell.SubText>
- <Cell.Icon height={12} width={7} source="icon-chevron" />
- </Cell.CellButton>
- )}
+ <Cell.CellButton onPress={this.props.onViewAccount}>
+ <Cell.Label>{pgettext('settings-view', 'Account')}</Cell.Label>
+ <Cell.SubText style={styles.settings__account_paid_until_label__error}>
+ {isOutOfTime ? outOfTimeMessage : formattedExpiry}
+ </Cell.SubText>
+ <Cell.Icon height={12} width={7} source="icon-chevron" />
+ </Cell.CellButton>
</View>
<Cell.CellButton onPress={this.props.onViewPreferences}>
- <Cell.Label>Preferences</Cell.Label>
+ <Cell.Label>{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>Advanced</Cell.Label>
+ <Cell.Label>{pgettext('settings-view', 'Advanced')}</Cell.Label>
<Cell.Icon height={12} width={7} source="icon-chevron" />
</Cell.CellButton>
<View style={styles.settings__cell_spacer} />
@@ -123,9 +123,19 @@ export default class Settings extends Component<IProps> {
let icon;
let footer;
if (!this.props.consistentVersion || !this.props.upToDateVersion) {
+ const inconsistentVersionMessage = pgettext(
+ 'settings-view',
+ 'Inconsistent internal version information, please restart the app.',
+ );
+
+ const updateAvailableMessage = pgettext(
+ 'settings-view',
+ 'Update available, download to remain safe.',
+ );
+
const message = !this.props.consistentVersion
- ? 'Inconsistent internal version information, please restart the app.'
- : 'Update available, download to remain safe.';
+ ? inconsistentVersionMessage
+ : updateAvailableMessage;
icon = (
<ImageView
@@ -147,7 +157,7 @@ export default class Settings extends Component<IProps> {
<View>
<Cell.CellButton disabled={this.props.isOffline} onPress={this.openDownloadLink}>
{icon}
- <Cell.Label>App version</Cell.Label>
+ <Cell.Label>{pgettext('settings-view', 'App version')}</Cell.Label>
<Cell.SubText>{this.props.appVersion}</Cell.SubText>
<Cell.Icon height={16} width={16} source="icon-extLink" />
</Cell.CellButton>
@@ -163,12 +173,12 @@ export default class Settings extends Component<IProps> {
return (
<View>
<Cell.CellButton onPress={this.props.onViewSupport}>
- <Cell.Label>Report a problem</Cell.Label>
+ <Cell.Label>{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>{'FAQs & Guides'}</Cell.Label>
+ <Cell.Label>{pgettext('settings-view', 'FAQs & Guides')}</Cell.Label>
<Cell.Icon height={16} width={16} source="icon-extLink" />
</Cell.CellButton>
</View>
diff --git a/gui/packages/desktop/src/renderer/components/Support.tsx b/gui/packages/desktop/src/renderer/components/Support.tsx
index 95e96bebe8..b36c3cb154 100644
--- a/gui/packages/desktop/src/renderer/components/Support.tsx
+++ b/gui/packages/desktop/src/renderer/components/Support.tsx
@@ -9,6 +9,7 @@ import {
} from '@mullvad/components';
import * as React from 'react';
import { Component, Text, TextInput, View } from 'reactxp';
+import { pgettext } from '../../shared/gettext';
import * as AppButton from './AppButton';
import { Container, Layout } from './Layout';
import { BackBarItem, NavigationBar } from './NavigationBar';
@@ -20,7 +21,7 @@ import { ISupportReportForm } from '../redux/support/actions';
enum SendState {
Initial,
Confirm,
- Loading,
+ Sending,
Success,
Failed,
}
@@ -125,12 +126,13 @@ export default class Support extends Component<ISupportProps, ISupportState> {
const { sendState } = this.state;
const header = (
<SettingsHeader>
- <HeaderTitle>Report a problem</HeaderTitle>
+ <HeaderTitle>{pgettext('support-view', 'Report a problem')}</HeaderTitle>
{(sendState === SendState.Initial || sendState === SendState.Confirm) && (
<HeaderSubTitle>
- {
- "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."
- }
+ {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.",
+ )}
</HeaderSubTitle>
)}
</SettingsHeader>
@@ -145,7 +147,10 @@ export default class Support extends Component<ISupportProps, ISupportState> {
<ModalContent>
<View style={styles.support}>
<NavigationBar>
- <BackBarItem action={this.props.onClose}>Settings</BackBarItem>
+ <BackBarItem action={this.props.onClose}>
+ {// TRANSLATORS: Back button in navigation bar
+ pgettext('support-nav', 'Settings')}
+ </BackBarItem>
</NavigationBar>
<View style={styles.support__container}>
{header}
@@ -195,7 +200,7 @@ export default class Support extends Component<ISupportProps, ISupportState> {
private sendReport(): Promise<void> {
return new Promise((resolve, reject) => {
- this.setState({ sendState: SendState.Loading }, async () => {
+ this.setState({ sendState: SendState.Sending }, async () => {
try {
const { email, message } = this.state;
const reportPath = await this.collectLog();
@@ -218,8 +223,8 @@ export default class Support extends Component<ISupportProps, ISupportState> {
case SendState.Initial:
case SendState.Confirm:
return this.renderForm();
- case SendState.Loading:
- return this.renderLoading();
+ case SendState.Sending:
+ return this.renderSending();
case SendState.Success:
return this.renderSent();
case SendState.Failed:
@@ -240,7 +245,7 @@ export default class Support extends Component<ISupportProps, ISupportState> {
<View style={styles.support__form_row_email}>
<TextInput
style={styles.support__form_email}
- placeholder="Your email (optional)"
+ placeholder={pgettext('support-view', 'Your email (optional)')}
defaultValue={this.state.email}
onChangeText={this.onChangeEmail}
keyboardType="email-address"
@@ -250,7 +255,7 @@ export default class Support extends Component<ISupportProps, ISupportState> {
<View style={styles.support__form_message_scroll_wrap}>
<TextInput
style={styles.support__form_message}
- placeholder="Describe your problem"
+ placeholder={pgettext('support-view', 'Describe your problem')}
defaultValue={this.state.message}
multiline={true}
onChangeText={this.onChangeDescription}
@@ -259,11 +264,11 @@ 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>View app logs</AppButton.Label>
+ <AppButton.Label>{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}>
- Send
+ {pgettext('support-view', 'Send')}
</AppButton.GreenButton>
</View>
</View>
@@ -271,7 +276,7 @@ export default class Support extends Component<ISupportProps, ISupportState> {
);
}
- private renderLoading() {
+ private renderSending() {
return (
<View style={styles.support__content}>
<View style={styles.support__form}>
@@ -279,8 +284,12 @@ export default class Support extends Component<ISupportProps, ISupportState> {
<View style={styles.support__status_icon}>
<ImageView source="icon-spinner" height={60} width={60} />
</View>
- <View style={styles.support__status_security__secure}>{'SECURE CONNECTION'}</View>
- <Text style={styles.support__send_status}>{'Sending...'}</Text>
+ <View style={styles.support__status_security__secure}>
+ {pgettext('support-view', 'SECURE CONNECTION')}
+ </View>
+ <Text style={styles.support__send_status}>
+ {pgettext('support-view', 'Sending...')}
+ </Text>
</View>
</View>
</View>
@@ -288,6 +297,21 @@ export default class Support extends Component<ISupportProps, ISupportState> {
}
private renderSent() {
+ // 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);
+ reachBackMessage.splice(
+ 1,
+ 0,
+ <Text key={'email'} style={styles.support__sent_email}>
+ {this.state.email}
+ </Text>,
+ );
+
return (
<View style={styles.support__content}>
<View style={styles.support__form}>
@@ -295,15 +319,16 @@ export default class Support extends Component<ISupportProps, ISupportState> {
<View style={styles.support__status_icon}>
<ImageView source="icon-success" height={60} width={60} />
</View>
- <Text style={styles.support__status_security__secure}>{'SECURE CONNECTION'}</Text>
- <Text style={styles.support__send_status}>{'Sent'}</Text>
+ <Text style={styles.support__status_security__secure}>
+ {pgettext('support-view', 'SECURE CONNECTION')}
+ </Text>
+ <Text style={styles.support__send_status}>{pgettext('support-view', 'Sent')}</Text>
- <Text style={styles.support__sent_message}>Thanks! We will look into this.</Text>
+ <Text style={styles.support__sent_message}>
+ {pgettext('support-view', 'Thanks! We will look into this.')}
+ </Text>
{this.state.email.trim().length > 0 ? (
- <Text style={styles.support__sent_message}>
- {'If needed we will contact you on '}
- <Text style={styles.support__sent_email}>{this.state.email}</Text>
- </Text>
+ <Text style={styles.support__sent_message}>{reachBackMessage}</Text>
) : null}
</View>
</View>
@@ -319,20 +344,27 @@ export default class Support extends Component<ISupportProps, ISupportState> {
<View style={styles.support__status_icon}>
<ImageView source="icon-fail" height={60} width={60} />
</View>
- <Text style={styles.support__status_security__secure}>{'SECURE CONNECTION'}</Text>
- <Text style={styles.support__send_status}>{'Failed to send'}</Text>
+ <Text style={styles.support__status_security__secure}>
+ {pgettext('support-view', 'SECURE CONNECTION')}
+ </Text>
+ <Text style={styles.support__send_status}>
+ {pgettext('support-view', 'Failed to send')}
+ </Text>
<Text style={styles.support__sent_message}>
- {
- "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."
- }
+ {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.",
+ )}
</Text>
</View>
</View>
<View style={styles.support__footer}>
<AppButton.BlueButton style={styles.edit_message_button} onPress={this.handleEditMessage}>
- {'Edit message'}
+ {pgettext('support-view', 'Edit message')}
</AppButton.BlueButton>
- <AppButton.GreenButton onPress={this.onSend}>Try again</AppButton.GreenButton>
+ <AppButton.GreenButton onPress={this.onSend}>
+ {pgettext('support-view', 'Try again')}
+ </AppButton.GreenButton>
</View>
</View>
);
@@ -354,12 +386,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}>
- 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.
+ {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}>{'Send anyway'}</AppButton.GreenButton>
+ <AppButton.GreenButton onPress={this.confirm}>
+ {pgettext('support-view', 'Send anyway')}
+ </AppButton.GreenButton>
<AppButton.RedButton onPress={this.dismiss} style={styles.confirm_no_email_back_button}>
- {'Back'}
+ {pgettext('support-view', 'Back')}
</AppButton.RedButton>
</View>
</View>
diff --git a/gui/packages/desktop/src/renderer/components/TunnelControl.tsx b/gui/packages/desktop/src/renderer/components/TunnelControl.tsx
index aa21556fae..436d896582 100644
--- a/gui/packages/desktop/src/renderer/components/TunnelControl.tsx
+++ b/gui/packages/desktop/src/renderer/components/TunnelControl.tsx
@@ -2,6 +2,7 @@ import { ConnectionInfo, SecuredDisplayStyle, SecuredLabel } from '@mullvad/comp
import * as React from 'react';
import { Component, Styles, Text, Types, View } from 'reactxp';
import { colors } from '../../config.json';
+import { pgettext } from '../../shared/gettext';
import * as AppButton from './AppButton';
import { RelayProtocol, TunnelStateTransition } from '../../shared/daemon-rpc-types';
@@ -88,7 +89,7 @@ export default class TunnelControl extends Component<ITunnelControlProps> {
<AppButton.TransparentButton
style={styles.switch_location_button}
onPress={this.props.onSelectLocation}>
- {'Switch location'}
+ {pgettext('tunnel-control', 'Switch location')}
</AppButton.TransparentButton>
);
};
@@ -104,19 +105,19 @@ export default class TunnelControl extends Component<ITunnelControlProps> {
const Connect = () => (
<AppButton.GreenButton onPress={this.props.onConnect}>
- {'Secure my connection'}
+ {pgettext('tunnel-control', 'Secure my connection')}
</AppButton.GreenButton>
);
const Disconnect = () => (
<AppButton.RedTransparentButton onPress={this.props.onDisconnect}>
- {'Disconnect'}
+ {pgettext('tunnel-control', 'Disconnect')}
</AppButton.RedTransparentButton>
);
const Cancel = () => (
<AppButton.RedTransparentButton onPress={this.props.onDisconnect}>
- {'Cancel'}
+ {pgettext('tunnel-control', 'Cancel')}
</AppButton.RedTransparentButton>
);
diff --git a/gui/packages/desktop/src/renderer/containers/ConnectPage.tsx b/gui/packages/desktop/src/renderer/containers/ConnectPage.tsx
index 75071e3a1f..cb78c2e8b3 100644
--- a/gui/packages/desktop/src/renderer/containers/ConnectPage.tsx
+++ b/gui/packages/desktop/src/renderer/containers/ConnectPage.tsx
@@ -3,6 +3,8 @@ import { shell } from 'electron';
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 Connect from '../components/Connect';
import AccountExpiry from '../lib/account-expiry';
import userInterfaceActions from '../redux/userinterface/actions';
@@ -24,6 +26,7 @@ function getRelayName(
} else if ('country' in location) {
const country = relayLocations.find(({ code }) => code === location.country);
if (country) {
+ // TODO: translate
return country.name;
}
} else if ('city' in location) {
@@ -32,6 +35,7 @@ function getRelayName(
if (country) {
const city = country.cities.find(({ code }) => code === cityCode);
if (city) {
+ // TODO: translate
return city.name;
}
}
@@ -41,7 +45,18 @@ function getRelayName(
if (country) {
const city = country.cities.find(({ code }) => code === cityCode);
if (city) {
- return `${city.name} (${hostname})`;
+ return sprintf(
+ // TRANSLATORS: The selected location label displayed on the main view, when a user selected a specific host to connect to.
+ // TRANSLATORS: Example: Malmö (se-mma-001)
+ // TRANSLATORS: Available placeholders:
+ // TRANSLATORS: %(city)s - a city name
+ // TRANSLATORS: %(hostname)s - a hostname
+ pgettext('connect-container', '%(city)s (%(hostname)s)'),
+ {
+ city: city.name,
+ hostname,
+ },
+ );
}
}
}
diff --git a/gui/packages/desktop/src/renderer/lib/account-expiry.ts b/gui/packages/desktop/src/renderer/lib/account-expiry.ts
index f7f85afbda..e781caeffe 100644
--- a/gui/packages/desktop/src/renderer/lib/account-expiry.ts
+++ b/gui/packages/desktop/src/renderer/lib/account-expiry.ts
@@ -1,4 +1,6 @@
import moment from 'moment';
+import { sprintf } from 'sprintf-js';
+import { pgettext } from '../../shared/gettext';
export default class AccountExpiry {
private expiry: moment.Moment;
@@ -16,6 +18,14 @@ export default class AccountExpiry {
}
public remainingTime(): string {
- return this.expiry.fromNow(true) + ' left';
+ const duration = this.expiry.fromNow(true);
+
+ return sprintf(
+ // 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'),
+ { duration },
+ );
}
}
diff --git a/gui/packages/desktop/src/renderer/lib/auth-failure.ts b/gui/packages/desktop/src/renderer/lib/auth-failure.ts
index 3f6f8b4609..f200ed824d 100644
--- a/gui/packages/desktop/src/renderer/lib/auth-failure.ts
+++ b/gui/packages/desktop/src/renderer/lib/auth-failure.ts
@@ -1,4 +1,5 @@
import log from 'electron-log';
+import { pgettext } from '../../shared/gettext';
export type AuthFailureKind =
| 'INVALID_ACCOUNT'
@@ -7,13 +8,23 @@ export type AuthFailureKind =
| 'UNKNOWN';
// These strings should match up with mullvad-types/src/auth_failed.rs
-const GENERIC_FAILURE_MSG = 'Account authentication failed';
-const INVALID_ACCOUNT_MSG =
- "You've logged in with an account number that is not valid. Please log out and try another one.";
-const EXPIRED_ACCOUNT_MSG =
- 'You have no more VPN time left on this account. Please log in on our website to buy more credit.';
-const TOO_MANY_CONNECTIONS_MSG =
- 'This account has too many simultaneous connections. Disconnect another device or try connecting again shortly.';
+
+const GENERIC_FAILURE_MSG = pgettext('auth-failure', 'Account authentication failed.');
+
+const INVALID_ACCOUNT_MSG = pgettext(
+ 'auth-failure',
+ "You've logged in with an account number that is not valid. Please log out and try another one.",
+);
+
+const EXPIRED_ACCOUNT_MSG = pgettext(
+ 'auth-failure',
+ 'You have no more VPN time left on this account. Please log in on our website to buy more credit.',
+);
+
+const TOO_MANY_CONNECTIONS_MSG = pgettext(
+ 'auth-failure',
+ 'This account has too many simultaneous connections. Disconnect another device or try connecting again shortly.',
+);
export class AuthFailure {
private reasonId: AuthFailureKind;
diff --git a/gui/packages/desktop/src/shared/gettext.ts b/gui/packages/desktop/src/shared/gettext.ts
new file mode 100644
index 0000000000..4722e16915
--- /dev/null
+++ b/gui/packages/desktop/src/shared/gettext.ts
@@ -0,0 +1,80 @@
+import log from 'electron-log';
+import fs from 'fs';
+import { po } from 'gettext-parser';
+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: string) => {
+ // 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) {
+ // 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);
+ }
+
+ for (const locale of preferredLocales) {
+ if (parseTranslation(locale, 'messages')) {
+ log.info(`Loaded translations for ${locale}`);
+ catalogue.setLocale(locale);
+
+ SELECTED_LANGUAGE = locale;
+ return;
+ }
+ }
+}
+
+function parseTranslation(locale: string, domain: string): boolean {
+ const filename = path.join(LOCALES_DIR, locale, `${domain}.po`);
+ let buffer: Buffer;
+
+ try {
+ buffer = fs.readFileSync(filename);
+ } catch (error) {
+ if (error.code !== 'ENOENT') {
+ log.error(`Cannot read the gettext file "${filename}": ${error.message}`);
+ }
+ return false;
+ }
+
+ let translations: object;
+ try {
+ translations = po.parse(buffer);
+ } catch (error) {
+ log.error(`Cannot parse the gettext file "${filename}": ${error.message}`);
+ return false;
+ }
+
+ catalogue.addTranslations(locale, domain, translations);
+
+ 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);
+};
diff --git a/gui/types/gettext-parser/index.d.ts b/gui/types/gettext-parser/index.d.ts
new file mode 100644
index 0000000000..61207951f4
--- /dev/null
+++ b/gui/types/gettext-parser/index.d.ts
@@ -0,0 +1,5 @@
+declare module 'gettext-parser' {
+ export namespace po {
+ export function parse(input: string | Buffer, defaultCharset?: string): object;
+ }
+}
diff --git a/gui/yarn.lock b/gui/yarn.lock
index 49679a0bd1..c9c043d558 100644
--- a/gui/yarn.lock
+++ b/gui/yarn.lock
@@ -784,11 +784,25 @@
"@types/cheerio" "*"
"@types/react" "*"
+"@types/events@*":
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
+ integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==
+
"@types/geojson@*":
version "7946.0.5"
resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.5.tgz#9aea839ea5af4b1bc079f1d9fa977d48665e02b0"
integrity sha512-rLlMXpd3rdlrp0+xsrda/hFfOpIxgqFcRpk005UKbHtcdFK+QXAjhBAPnvO58qF4O1LdDXrcaiJxMgstCIlcaw==
+"@types/glob@5 - 7":
+ version "7.1.1"
+ resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575"
+ integrity sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==
+ dependencies:
+ "@types/events" "*"
+ "@types/minimatch" "*"
+ "@types/node" "*"
+
"@types/history@*":
version "4.7.2"
resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.2.tgz#0e670ea254d559241b6eeb3894f8754991e73220"
@@ -808,6 +822,11 @@
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.118.tgz#247bab39bfcc6d910d4927c6e06cbc70ec376f27"
integrity sha512-iiJbKLZbhSa6FYRip/9ZDX6HXhayXLDGY2Fqws9cOkEQ6XeKfaxB0sC541mowZJueYyMnVUmmG+al5/4fCDrgw==
+"@types/minimatch@*":
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
+ integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
+
"@types/mkdirp@^0.5.2":
version "0.5.2"
resolved "https://registry.yarnpkg.com/@types/mkdirp/-/mkdirp-0.5.2.tgz#503aacfe5cc2703d5484326b1b27efa67a339c1f"
@@ -820,6 +839,11 @@
resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.2.5.tgz#8a4accfc403c124a0bafe8a9fc61a05ec1032073"
integrity sha512-lAVp+Kj54ui/vLUFxsJTMtWvZraZxum3w3Nwkble2dNuV5VnPA+Mi2oGX9XYJAaIvZi3tn3cbjS/qcJXRb6Bww==
+"@types/node-gettext@^2.0.0":
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/@types/node-gettext/-/node-gettext-2.0.0.tgz#b4d7fdf8311c19eb3d828051745310a4365472eb"
+ integrity sha512-70HykF9FKN8sMTlmSCiwqEzTwHMwZeJ/6tHt7qQaRqFx5czIvu2HQ0ipSWu+YTINe9AT9sKHCJ5jQT2SLtCjxQ==
+
"@types/node@*":
version "10.7.1"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.7.1.tgz#b704d7c259aa40ee052eec678758a68d07132a2e"
@@ -835,6 +859,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.26.tgz#950e3d4e6b316ba6e1ae4e84d9155aba67f88c2f"
integrity sha512-opk6bLLErLSwyVVJeSH5Ek7ZWOBSsN0JrvXTNVGLXLAXKB9xlTYajrplR44xVyMrmbut94H6uJ9jqzM/12jxkA==
+"@types/parse5@^5":
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-5.0.0.tgz#9ae2106efc443d7c1e26570aa8247828c9c80f11"
+ integrity sha512-J5D3z703XTDIGQFYXsnU9uRCW9e9mMEFO0Kpe6kykyiboqziru/RlZ0hM2P+PKTG4NHG1SjLrqae/NrV2iJApQ==
+
"@types/prop-types@*":
version "15.5.5"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.5.5.tgz#17038dd322c2325f5da650a94d5f9974943625e3"
@@ -898,6 +927,11 @@
resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-7.0.5.tgz#f7dea19400c193a3b36a804a7f1f4b26dacf452b"
integrity sha512-4DShbH857bZVOY4tPi1RQJNrLcf89hEtU0klZ9aYTMbtt95Ok4XdPqqcbtGOHIbAHMLSzQP8Uw/6qtBBqyloww==
+"@types/sprintf-js@^1.1.2":
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/@types/sprintf-js/-/sprintf-js-1.1.2.tgz#a4fcb84c7344f39f70dc4eec0e1e7f10a48597a3"
+ integrity sha512-hkgzYF+qnIl8uTO8rmUSVSfQ8BIfMXC4yJAF4n8BE758YsKBZvFC4NumnAegj7KmylP0liEZNpb9RRGFMbFejA==
+
"@types/tough-cookie@*":
version "2.3.3"
resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-2.3.3.tgz#7f226d67d654ec9070e755f46daebf014628e9d9"
@@ -2656,6 +2690,11 @@ css-select@~1.2.0:
domutils "1.5.1"
nth-check "~1.0.1"
+css-selector-parser@^1.3:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/css-selector-parser/-/css-selector-parser-1.3.0.tgz#5f1ad43e2d8eefbfdc304fcd39a521664943e3eb"
+ integrity sha1-XxrUPi2O77/cME/NOaUhZklD4+s=
+
css-what@2.1:
version "2.1.0"
resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.0.tgz#9467d032c38cfaefb9f2d79501253062f87fa1bd"
@@ -3116,7 +3155,7 @@ encodeurl@~1.0.1, encodeurl@~1.0.2:
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
-encoding@^0.1.11:
+encoding@^0.1.11, encoding@^0.1.12:
version "0.1.12"
resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb"
integrity sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=
@@ -3812,6 +3851,28 @@ getpass@^0.1.1:
dependencies:
assert-plus "^1.0.0"
+gettext-extractor@^3.4.2:
+ version "3.4.2"
+ resolved "https://registry.yarnpkg.com/gettext-extractor/-/gettext-extractor-3.4.2.tgz#fa2b867285c5a44ea7bb792dcee8dce34f20bd44"
+ integrity sha512-P8Afsy3YqcNExSAKB9jkIK04tvRe9L/zNZHP2bsDoptTuhy6Ny/MWW5OOxriYxyWXzlWGk2jmcJmYaDTIESKyg==
+ dependencies:
+ "@types/glob" "5 - 7"
+ "@types/parse5" "^5"
+ css-selector-parser "^1.3"
+ glob "5 - 7"
+ parse5 "^5"
+ pofile "^1"
+ typescript "2 - 3"
+
+gettext-parser@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/gettext-parser/-/gettext-parser-3.1.0.tgz#a92f4aa09cdaa944ce71832677749d84ac683649"
+ integrity sha512-eVD8RxFMeHg8pjl5zsk7xlEDaKdcYlotLztiMaYGLvI13LMXwWlybLg7rg6eagct79vyGkPGZrMPBsdjsQOnWg==
+ dependencies:
+ encoding "^0.1.12"
+ readable-stream "^3.0.6"
+ safe-buffer "^5.1.2"
+
glob-base@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4"
@@ -3835,6 +3896,18 @@ glob-parent@^3.1.0:
is-glob "^3.1.0"
path-dirname "^1.0.0"
+"glob@5 - 7":
+ version "7.1.3"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1"
+ integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==
+ dependencies:
+ fs.realpath "^1.0.0"
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "^3.0.4"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
glob@7.1.2, glob@^7.0.0, glob@^7.0.5, glob@^7.1.1:
version "7.1.2"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
@@ -5551,6 +5624,13 @@ node-fetch@^1.0.1, node-fetch@^1.3.3:
encoding "^0.1.11"
is-stream "^1.0.1"
+node-gettext@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/node-gettext/-/node-gettext-2.0.0.tgz#f1dc1237cdc546f51593da340304b8beba5b8525"
+ integrity sha1-8dwSN83FRvUVk9o0AwS4vrpbhSU=
+ dependencies:
+ lodash.get "^4.4.2"
+
node-int64@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
@@ -6025,7 +6105,7 @@ parse-json@^4.0.0:
error-ex "^1.3.1"
json-parse-better-errors "^1.0.1"
-parse5@5.1.0:
+parse5@5.1.0, parse5@^5:
version "5.1.0"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.0.tgz#c59341c9723f414c452975564c7c00a68d58acd2"
integrity sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==
@@ -6239,6 +6319,11 @@ pn@^1.1.0:
resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb"
integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==
+pofile@^1:
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/pofile/-/pofile-1.0.11.tgz#35aff58c17491d127a07336d5522ebc9df57c954"
+ integrity sha512-Vy9eH1dRD9wHjYt/QqXcTz+RnX/zg53xK+KljFSX30PvdDMb2z+c6uDUeblUGqqJgz3QFsdlA0IJvHziPmWtQg==
+
portscanner@2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/portscanner/-/portscanner-2.1.1.tgz#eabb409e4de24950f5a2a516d35ae769343fbb96"
@@ -6267,10 +6352,10 @@ preserve@^0.2.0:
resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
integrity sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=
-prettier@1.15.3:
- version "1.15.3"
- resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.15.3.tgz#1feaac5bdd181237b54dbe65d874e02a1472786a"
- integrity sha512-gAU9AGAPMaKb3NNSUUuhhFAS7SCO4ALTN4nRIn6PJ075Qd28Yn2Ig2ahEJWdJwJmlEBTUfC7mMUSFy8MwsOCfg==
+prettier@1.16.4:
+ version "1.16.4"
+ resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.16.4.tgz#73e37e73e018ad2db9c76742e2647e21790c9717"
+ integrity sha512-ZzWuos7TI5CKUeQAtFd6Zhm2s6EpAD/ZLApIhsF9pRvRtM1RFo61dM/4MSRUA0SuLugA/zgrZD8m0BaY46Og7g==
pretty-bytes@^1.0.2:
version "1.0.4"
@@ -6701,6 +6786,15 @@ readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable
string_decoder "~1.1.1"
util-deprecate "~1.0.1"
+readable-stream@^3.0.6:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.1.1.tgz#ed6bbc6c5ba58b090039ff18ce670515795aeb06"
+ integrity sha512-DkN66hPyqDhnIQ6Jcsvx9bFjhw214O4poMBcIMgPVpQvNy9a0e0Uhg5SqySyDKAmUlwt8LonTBz1ezOnM8pUdA==
+ dependencies:
+ inherits "^2.0.3"
+ string_decoder "^1.1.1"
+ util-deprecate "^1.0.1"
+
readable-stream@~1.1.9:
version "1.1.14"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
@@ -7443,6 +7537,11 @@ split@0.3:
dependencies:
through "2"
+sprintf-js@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673"
+ integrity sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==
+
sprintf-js@~1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
@@ -7562,6 +7661,13 @@ string.prototype.trim@^1.1.2:
es-abstract "^1.5.0"
function-bind "^1.0.2"
+string_decoder@^1.1.1:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d"
+ integrity sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==
+ dependencies:
+ safe-buffer "~5.1.0"
+
string_decoder@~0.10.x:
version "0.10.31"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
@@ -7881,10 +7987,10 @@ tslib@^1.8.0, tslib@^1.8.1:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"
integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==
-tslint-config-prettier@^1.17.0:
- version "1.17.0"
- resolved "https://registry.yarnpkg.com/tslint-config-prettier/-/tslint-config-prettier-1.17.0.tgz#946ed6117f98f3659a65848279156d87628c33dc"
- integrity sha512-NKWNkThwqE4Snn4Cm6SZB7lV5RMDDFsBwz6fWUkTxOKGjMx8ycOHnjIbhn7dZd5XmssW3CwqUjlANR6EhP9YQw==
+tslint-config-prettier@^1.18.0:
+ version "1.18.0"
+ resolved "https://registry.yarnpkg.com/tslint-config-prettier/-/tslint-config-prettier-1.18.0.tgz#75f140bde947d35d8f0d238e0ebf809d64592c37"
+ integrity sha512-xPw9PgNPLG3iKRxmK7DWr+Ea/SzrvfHtjFt5LBl61gk2UBG/DB9kCXRjv+xyIU1rUtnayLeMUVJBcMX8Z17nDg==
tslint-react@^3.6.0:
version "3.6.0"
@@ -7947,6 +8053,11 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
+"typescript@2 - 3":
+ version "3.3.3333"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.3.3333.tgz#171b2c5af66c59e9431199117a3bcadc66fdcfd6"
+ integrity sha512-JjSKsAfuHBE/fB2oZ8NxtRTk5iGcg6hkYXMnZ3Wc+b2RSqejEqTaem11mHASMnFilHrax3sLK0GDzcJrekZYLw==
+
typescript@^3.3.3:
version "3.3.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.3.3.tgz#f1657fc7daa27e1a8930758ace9ae8da31403221"
@@ -8106,7 +8217,7 @@ utf8-byte-length@^1.0.1:
resolved "https://registry.yarnpkg.com/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz#f45f150c4c66eee968186505ab93fcbb8ad6bf61"
integrity sha1-9F8VDExm7uloGGUFq5P8u4rWv2E=
-util-deprecate@~1.0.1:
+util-deprecate@^1.0.1, util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=