Localize all the things

Signed-off-by: Roopesh Chander <roop@roopc.net>
This commit is contained in:
Roopesh Chander 2018-12-18 16:30:16 +05:30
parent e47a8232d8
commit 0552d75aa1
13 changed files with 408 additions and 135 deletions

View File

@ -53,6 +53,8 @@
6FDEF802218646BA00D8FBF6 /* ZipArchive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF801218646B900D8FBF6 /* ZipArchive.swift */; }; 6FDEF802218646BA00D8FBF6 /* ZipArchive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF801218646B900D8FBF6 /* ZipArchive.swift */; };
6FDEF806218725D200D8FBF6 /* SettingsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF805218725D200D8FBF6 /* SettingsTableViewController.swift */; }; 6FDEF806218725D200D8FBF6 /* SettingsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF805218725D200D8FBF6 /* SettingsTableViewController.swift */; };
6FDEF8082187442100D8FBF6 /* WgQuickConfigFileWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF8072187442100D8FBF6 /* WgQuickConfigFileWriter.swift */; }; 6FDEF8082187442100D8FBF6 /* WgQuickConfigFileWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF8072187442100D8FBF6 /* WgQuickConfigFileWriter.swift */; };
6FE1765621C90BBE002690EA /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6FE1765421C90BBE002690EA /* Localizable.strings */; };
6FE1765A21C90E87002690EA /* LocalizationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE1765921C90E87002690EA /* LocalizationHelper.swift */; };
6FE254FB219C10800028284D /* ZipImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE254FA219C10800028284D /* ZipImporter.swift */; }; 6FE254FB219C10800028284D /* ZipImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE254FA219C10800028284D /* ZipImporter.swift */; };
6FE254FF219C60290028284D /* ZipExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE254FE219C60290028284D /* ZipExporter.swift */; }; 6FE254FF219C60290028284D /* ZipExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE254FE219C60290028284D /* ZipExporter.swift */; };
6FF3527021C240160008484E /* ringlogger.c in Sources */ = {isa = PBXBuildFile; fileRef = 6FF3526C21C23F960008484E /* ringlogger.c */; }; 6FF3527021C240160008484E /* ringlogger.c in Sources */ = {isa = PBXBuildFile; fileRef = 6FF3526C21C23F960008484E /* ringlogger.c */; };
@ -160,6 +162,8 @@
6FDEF801218646B900D8FBF6 /* ZipArchive.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZipArchive.swift; sourceTree = "<group>"; }; 6FDEF801218646B900D8FBF6 /* ZipArchive.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZipArchive.swift; sourceTree = "<group>"; };
6FDEF805218725D200D8FBF6 /* SettingsTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsTableViewController.swift; sourceTree = "<group>"; }; 6FDEF805218725D200D8FBF6 /* SettingsTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsTableViewController.swift; sourceTree = "<group>"; };
6FDEF8072187442100D8FBF6 /* WgQuickConfigFileWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WgQuickConfigFileWriter.swift; sourceTree = "<group>"; }; 6FDEF8072187442100D8FBF6 /* WgQuickConfigFileWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WgQuickConfigFileWriter.swift; sourceTree = "<group>"; };
6FE1765521C90BBE002690EA /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = WireGuard/Base.lproj/Localizable.strings; sourceTree = "<group>"; };
6FE1765921C90E87002690EA /* LocalizationHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationHelper.swift; sourceTree = "<group>"; };
6FE254FA219C10800028284D /* ZipImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZipImporter.swift; sourceTree = "<group>"; }; 6FE254FA219C10800028284D /* ZipImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZipImporter.swift; sourceTree = "<group>"; };
6FE254FE219C60290028284D /* ZipExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZipExporter.swift; sourceTree = "<group>"; }; 6FE254FE219C60290028284D /* ZipExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZipExporter.swift; sourceTree = "<group>"; };
6FF3526B21C23F960008484E /* ringlogger.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ringlogger.h; sourceTree = "<group>"; }; 6FF3526B21C23F960008484E /* ringlogger.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ringlogger.h; sourceTree = "<group>"; };
@ -377,6 +381,7 @@
6FF4AC0B211EC46F002C96EB = { 6FF4AC0B211EC46F002C96EB = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
6FE1765421C90BBE002690EA /* Localizable.strings */,
6F5D0C432183B4A4000F85AD /* Shared */, 6F5D0C432183B4A4000F85AD /* Shared */,
6FF4AC16211EC46F002C96EB /* WireGuard */, 6FF4AC16211EC46F002C96EB /* WireGuard */,
6F5D0C1B218352EF000F85AD /* WireGuardNetworkExtension */, 6F5D0C1B218352EF000F85AD /* WireGuardNetworkExtension */,
@ -405,6 +410,7 @@
6FDEF7E72186320E00D8FBF6 /* ZipArchive */, 6FDEF7E72186320E00D8FBF6 /* ZipArchive */,
6F61F1E821B932F700483816 /* WireGuardAppError.swift */, 6F61F1E821B932F700483816 /* WireGuardAppError.swift */,
6F61F1EA21B937EF00483816 /* WireGuardResult.swift */, 6F61F1EA21B937EF00483816 /* WireGuardResult.swift */,
6FE1765921C90E87002690EA /* LocalizationHelper.swift */,
6FF4AC482120B9E0002C96EB /* WireGuard.entitlements */, 6FF4AC482120B9E0002C96EB /* WireGuard.entitlements */,
6FF4AC1E211EC472002C96EB /* Assets.xcassets */, 6FF4AC1E211EC472002C96EB /* Assets.xcassets */,
6FF4AC20211EC472002C96EB /* LaunchScreen.storyboard */, 6FF4AC20211EC472002C96EB /* LaunchScreen.storyboard */,
@ -549,6 +555,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
6F919ED9218C65C50023B400 /* wireguard_doc_logo_22x29.png in Resources */, 6F919ED9218C65C50023B400 /* wireguard_doc_logo_22x29.png in Resources */,
6FE1765621C90BBE002690EA /* Localizable.strings in Resources */,
6FF4AC22211EC472002C96EB /* LaunchScreen.storyboard in Resources */, 6FF4AC22211EC472002C96EB /* LaunchScreen.storyboard in Resources */,
6F919EDA218C65C50023B400 /* wireguard_doc_logo_44x58.png in Resources */, 6F919EDA218C65C50023B400 /* wireguard_doc_logo_44x58.png in Resources */,
6FF4AC1F211EC472002C96EB /* Assets.xcassets in Resources */, 6FF4AC1F211EC472002C96EB /* Assets.xcassets in Resources */,
@ -660,6 +667,7 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
6FE1765A21C90E87002690EA /* LocalizationHelper.swift in Sources */,
6FF3527221C2616C0008484E /* ringlogger.c in Sources */, 6FF3527221C2616C0008484E /* ringlogger.c in Sources */,
6FF3527321C2616C0008484E /* Logger.swift in Sources */, 6FF3527321C2616C0008484E /* Logger.swift in Sources */,
6F6899AC218099F00012E523 /* WgQuickConfigFileParser.swift in Sources */, 6F6899AC218099F00012E523 /* WgQuickConfigFileParser.swift in Sources */,
@ -723,6 +731,14 @@
/* End PBXTargetDependency section */ /* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */ /* Begin PBXVariantGroup section */
6FE1765421C90BBE002690EA /* Localizable.strings */ = {
isa = PBXVariantGroup;
children = (
6FE1765521C90BBE002690EA /* Base */,
);
name = Localizable.strings;
sourceTree = "<group>";
};
6FF4AC20211EC472002C96EB /* LaunchScreen.storyboard */ = { 6FF4AC20211EC472002C96EB /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup; isa = PBXVariantGroup;
children = ( children = (

View File

@ -0,0 +1,210 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
// Generic alert action names
"actionOK" = "OK";
"actionCancel" = "Cancel";
"actionSave" = "Save";
// Tunnels list UI
"tunnelsListTitle" = "WireGuard";
"tunnelsListSettingsButtonTitle" = "Settings";
"tunnelsListCenteredAddTunnelButtonTitle" = "Add a tunnel";
"tunnelsListSwipeDeleteButtonTitle" = "Delete";
// Tunnels list menu
"addTunnelMenuHeader" = "Add a new WireGuard tunnel";
"addTunnelMenuImportFile" = "Create from file or archive";
"addTunnelMenuQRCode" = "Create from QR code";
"addTunnelMenuFromScratch" = "Create from scratch";
// Tunnels list alerts
"alertImportedFromZipTitle (%d)" = "Created %d tunnels";
"alertImportedFromZipMessage (%1$d of %2$d)" = "Created %1$d of %2$d tunnels from zip archive";
"alertUnableToImportTitle" = "Unable to import tunnel";
"alertUnableToImportMessage" = "An error occured when importing the tunnel configuration.";
// Tunnel detail and edit UI
"newTunnelViewTitle" = "New configuration";
"editTunnelViewTitle" = "Edit configuration";
"tunnelSectionTitleStatus" = "Status";
"tunnelStatusInactive" = "Inactive";
"tunnelStatusActivating" = "Activating";
"tunnelStatusActive" = "Active";
"tunnelStatusDeactivating" = "Deactivating";
"tunnelStatusReasserting" = "Reactivating";
"tunnelStatusRestarting" = "Restarting";
"tunnelStatusWaiting" = "Waiting";
"tunnelSectionTitleInterface" = "Interface";
"tunnelInterfaceName" = "Name";
"tunnelInterfacePrivateKey" = "Private key";
"tunnelInterfacePublicKey" = "Public key";
"tunnelInterfaceGenerateKeypair" = "Generate keypair";
"tunnelInterfaceAddresses" = "Addresses";
"tunnelInterfaceListenPort" = "Listen port";
"tunnelInterfaceMTU" = "MTU";
"tunnelInterfaceDNS" = "DNS servers";
"tunnelSectionTitlePeer" = "Peer";
"tunnelPeerPublicKey" = "Public key";
"tunnelPeerPreSharedKey" = "Preshared key";
"tunnelPeerEndpoint" = "Endpoint";
"tunnelPeerPersistentKeepalive" = "Persistent keepalive";
"tunnelPeerAllowedIPs" = "Allowed IPs";
"tunnelPeerExcludePrivateIPs" = "Exclude private IPs";
"tunnelSectionTitleOnDemand" = "On-Demand Activation";
"tunnelOnDemandKey" = "Activate on demand";
"tunnelOnDemandOptionOff" = "Off";
"tunnelOnDemandOptionWiFiOrCellular" = "Wi-Fi or cellular";
"tunnelOnDemandOptionWiFiOnly" = "Wi-Fi only";
"tunnelOnDemandOptionCellularOnly" = "Cellular only";
"addPeerButtonTitle" = "Add peer";
"deletePeerButtonTitle" = "Delete peer";
"deletePeerConfirmationAlertButtonTitle" = "Delete";
"deletePeerConfirmationAlertMessage" = "Delete this peer?";
"deleteTunnelButtonTitle" = "Delete tunnel";
"deleteTunnelConfirmationAlertButtonTitle" = "Delete";
"deleteTunnelConfirmationAlertMessage" = "Delete this tunnel?";
"tunnelEditPlaceholderTextRequired" = "Required";
"tunnelEditPlaceholderTextOptional" = "Optional";
"tunnelEditPlaceholderTextAutomatic" = "Automatic";
"tunnelEditPlaceholderTextOff" = "Off";
// Error alerts while creating / editing a tunnel configuration
/* Alert title for error in the interface data */
"alertInvalidInterfaceTitle" = "Invalid interface";
/* Any one of the following alert messages can go with the above title */
"alertInvalidInterfaceMessageNameRequired" = "Interface name is required";
"alertInvalidInterfaceMessagePrivateKeyRequired" = "Interface's private key is required";
"alertInvalidInterfaceMessagePrivateKeyInvalid" = "Interface's private key must be a 32-byte key in base64 encoding";
"alertInvalidInterfaceMessageAddressInvalid" = "Interface addresses must be a list of comma-separated IP addresses, optionally in CIDR notation";
"alertInvalidInterfaceMessageListenPortInvalid" = "Interface's listen port must be between 0 and 65535, or unspecified";
"alertInvalidInterfaceMessageMTUInvalid" = "Interface's MTU must be between 576 and 65535, or unspecified";
"alertInvalidInterfaceMessageDNSInvalid" = "Interface's DNS servers must be a list of comma-separated IP addresses";
/* Alert title for error in the peer data */
"alertInvalidPeerTitle" = "Invalid peer";
/* Any one of the following alert messages can go with the above title */
"alertInvalidPeerMessagePublicKeyRequired" = "Peer's public key is required";
"alertInvalidPeerMessagePublicKeyInvalid" = "Peer's public key must be a 32-byte key in base64 encoding";
"alertInvalidPeerMessagePreSharedKeyInvalid" = "Peer's preshared key must be a 32-byte key in base64 encoding";
"alertInvalidPeerMessageAllowedIPsInvalid" = "Peer's allowed IPs must be a list of comma-separated IP addresses, optionally in CIDR notation";
"alertInvalidPeerMessageEndpointInvalid" = "Peer's endpoint must be of the form 'host:port' or '[host]:port'";
"alertInvalidPeerMessagePersistentKeepaliveInvalid" = "Peer's persistent keepalive must be between 0 to 65535, or unspecified";
"alertInvalidPeerMessagePublicKeyDuplicated" = "Two or more peers cannot have the same public key";
// Scanning QR code UI
"scanQRCodeViewTitle" = "Scan QR code";
"scanQRCodeTipText" = "Tip: Generate with `qrencode -t ansiutf8 < tunnel.conf`";
// Scanning QR code alerts
"alertScanQRCodeCameraUnsupportedTitle" = "Camera Unsupported";
"alertScanQRCodeCameraUnsupportedMessage" = "This device is not able to scan QR codes";
"alertScanQRCodeInvalidQRCodeTitle" = "Invalid QR Code";
"alertScanQRCodeInvalidQRCodeMessage" = "The scanned QR code is not a valid WireGuard configuration";
"alertScanQRCodeUnreadableQRCodeTitle" = "Invalid Code";
"alertScanQRCodeUnreadableQRCodeMessage" = "The scanned code could not be read";
"alertScanQRCodeNamePromptTitle" = "Please name the scanned tunnel";
// Settings UI
"settingsViewTitle" = "Settings";
"settingsSectionTitleAbout" = "About";
"settingsVersionKeyWireGuardForIOS" = "WireGuard for iOS";
"settingsVersionKeyWireGuardGoBackend" = "WireGuard Go Backend";
"settingsSectionTitleExportConfigurations" = "Export configurations";
"settingsExportZipButtonTitle" = "Export zip archive";
"settingsSectionTitleTunnelLog" = "Tunnel log";
"settingsExportLogFileButtonTitle" = "Export log file";
// Settings alerts
"alertUnableToRemovePreviousLogTitle" = "Log export failed";
"alertUnableToRemovePreviousLogMessage" = "The pre-existing log could not be cleared";
"alertUnableToFindExtensionLogPathTitle" = "Log export failed";
"alertUnableToFindExtensionLogPathMessage" = "Unable to determine extension log path";
"alertUnableToWriteLogTitle" = "Log export failed";
"alertUnableToWriteLogMessage" = "Unable to write logs to file";
// Zip import / export error alerts
"alertCantOpenInputZipFileTitle" = "Unable to read zip archive";
"alertCantOpenInputZipFileMessage" = "The zip archive could not be read.";
"alertCantOpenOutputZipFileForWritingTitle" = "Unable to create zip archive";
"alertCantOpenOutputZipFileForWritingMessage" = "Could not open zip file for writing.";
"alertBadArchiveTitle" = "Unable to read zip archive";
"alertBadArchiveMessage" = "Bad or corrupt zip archive.";
"alertNoTunnelsToExportTitle" = "Nothing to export";
"alertNoTunnelsToExportMessage" = "There are no tunnels to export";
"alertNoTunnelsInImportedZipArchiveTitle" = "No tunnels in zip archive";
"alertNoTunnelsInImportedZipArchiveMessage" = "No .conf tunnel files were found inside the zip archive.";
// Tunnel management error alerts
"alertTunnelActivationFailureTitle" = "Activation failure";
"alertTunnelActivationFailureMessage" = "The tunnel could not be activated. Please ensure that you are connected to the Internet.";
"alertTunnelNameEmptyTitle" = "No name provided";
"alertTunnelNameEmptyMessage" = "Cannot create tunnel with an empty name";
"alertTunnelAlreadyExistsWithThatNameTitle" = "Name already exists";
"alertTunnelAlreadyExistsWithThatNameMessage" = "A tunnel with that name already exists";
"alertTunnelActivationErrorTunnelIsNotInactiveTitle" = "Activation failure";
"alertTunnelActivationErrorTunnelIsNotInactiveMessage" = "The tunnel is already active or in the process of being activated";
// Tunnel management error alerts on system error
/* The alert message that goes with the following titles would be
one of the alertSystemErrorMessage* listed further down */
"alertSystemErrorOnListingTunnelsTitle" = "Unable to list tunnels";
"alertSystemErrorOnAddTunnelTitle" = "Unable to create tunnel";
"alertSystemErrorOnModifyTunnelTitle" = "Unable to modify tunnel";
"alertSystemErrorOnRemoveTunnelTitle" = "Unable to remove tunnel";
/* The alert message for this alert shall include
one of the alertSystemErrorMessage* listed further down */
"alertTunnelActivationSystemErrorTitle" = "Activation failure";
"alertTunnelActivationSystemErrorMessage (%@)" = "The tunnel could not be activated. %@";
/* alertSystemErrorMessage* messages */
"alertSystemErrorMessageTunnelConfigurationInvalid" = "The configuration is invalid.";
"alertSystemErrorMessageTunnelConfigurationDisabled" = "The configuration is disabled.";
"alertSystemErrorMessageTunnelConnectionFailed" = "The connection failed.";
"alertSystemErrorMessageTunnelConfigurationStale" = "The configuration is stale.";
"alertSystemErrorMessageTunnelConfigurationReadWriteFailed" = "Reading or writing the configuration failed.";
"alertSystemErrorMessageTunnelConfigurationUnknown" = "Unknown system error.";

View File

@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import Foundation
func tr(_ key: String) -> String {
return NSLocalizedString(key, comment: "")
}
func tr(format: String, _ arguments: CVarArg...) -> String {
return String(format: NSLocalizedString(format, comment: ""), arguments: arguments)
}

View File

@ -14,17 +14,17 @@ enum TunnelsManagerError: WireGuardAppError {
var alertText: AlertText { var alertText: AlertText {
switch self { switch self {
case .tunnelNameEmpty: case .tunnelNameEmpty:
return ("No name provided", "Cannot create tunnel with an empty name") return (tr("alertTunnelNameEmptyTitle"), tr("alertTunnelNameEmptyMessage"))
case .tunnelAlreadyExistsWithThatName: case .tunnelAlreadyExistsWithThatName:
return ("Name already exists", "A tunnel with that name already exists") return (tr("alertTunnelAlreadyExistsWithThatNameTitle"), tr("alertTunnelAlreadyExistsWithThatNameMessage"))
case .systemErrorOnListingTunnels(let systemError): case .systemErrorOnListingTunnels(let systemError):
return ("Unable to list tunnels", systemError.UIString) return (tr("alertSystemErrorOnListingTunnelsTitle"), systemError.localizedUIString)
case .systemErrorOnAddTunnel(let systemError): case .systemErrorOnAddTunnel(let systemError):
return ("Unable to create tunnel", systemError.UIString) return (tr("alertSystemErrorOnAddTunnelTitle"), systemError.localizedUIString)
case .systemErrorOnModifyTunnel(let systemError): case .systemErrorOnModifyTunnel(let systemError):
return ("Unable to modify tunnel", systemError.UIString) return (tr("alertSystemErrorOnModifyTunnelTitle"), systemError.localizedUIString)
case .systemErrorOnRemoveTunnel(let systemError): case .systemErrorOnRemoveTunnel(let systemError):
return ("Unable to remove tunnel", systemError.UIString) return (tr("alertSystemErrorOnRemoveTunnelTitle"), systemError.localizedUIString)
} }
} }
} }
@ -39,12 +39,13 @@ enum TunnelsManagerActivationAttemptError: WireGuardAppError {
var alertText: AlertText { var alertText: AlertText {
switch self { switch self {
case .tunnelIsNotInactive: case .tunnelIsNotInactive:
return ("Activation failure", "The tunnel is already active or in the process of being activated") return (tr("alertTunnelActivationErrorTunnelIsNotInactiveTitle"), tr("alertTunnelActivationErrorTunnelIsNotInactiveMessage"))
case .failedWhileStarting(let systemError), case .failedWhileStarting(let systemError),
.failedWhileSaving(let systemError), .failedWhileSaving(let systemError),
.failedWhileLoading(let systemError), .failedWhileLoading(let systemError),
.failedBecauseOfTooManyErrors(let systemError): .failedBecauseOfTooManyErrors(let systemError):
return ("Activation failure", "The tunnel could not be activated. " + systemError.UIString) return (tr("alertTunnelActivationSystemErrorTitle"),
tr(format: "alertTunnelActivationSystemErrorMessage (%@)", systemError.localizedUIString))
} }
} }
} }
@ -56,7 +57,7 @@ enum TunnelsManagerActivationError: WireGuardAppError {
var alertText: AlertText { var alertText: AlertText {
switch self { switch self {
case .activationFailed: case .activationFailed:
return ("Activation failure", "The tunnel could not be activated. Please ensure that you are connected to the Internet.") return (tr("alertTunnelActivationFailureTitle"), tr("alertTunnelActivationFailureMessage"))
case .activationFailedWithExtensionError(let title, let message): case .activationFailedWithExtensionError(let title, let message):
return (title, message) return (title, message)
} }
@ -64,21 +65,21 @@ enum TunnelsManagerActivationError: WireGuardAppError {
} }
extension Error { extension Error {
var UIString: String { var localizedUIString: String {
if let systemError = self as? NEVPNError { if let systemError = self as? NEVPNError {
switch systemError { switch systemError {
case NEVPNError.configurationInvalid: case NEVPNError.configurationInvalid:
return "The configuration is invalid." return tr("alertSystemErrorMessageTunnelConfigurationInvalid")
case NEVPNError.configurationDisabled: case NEVPNError.configurationDisabled:
return "The configuration is disabled." return tr("alertSystemErrorMessageTunnelConfigurationDisabled")
case NEVPNError.connectionFailed: case NEVPNError.connectionFailed:
return "The connection failed." return tr("alertSystemErrorMessageTunnelConnectionFailed")
case NEVPNError.configurationStale: case NEVPNError.configurationStale:
return "The configuration is stale." return tr("alertSystemErrorMessageTunnelConfigurationStale")
case NEVPNError.configurationReadWriteFailed: case NEVPNError.configurationReadWriteFailed:
return "Reading or writing the configuration failed." return tr("alertSystemErrorMessageTunnelConfigurationReadWriteFailed")
case NEVPNError.configurationUnknown: case NEVPNError.configurationUnknown:
return "Unknown system error." return tr("alertSystemErrorMessageTunnelConfigurationUnknown")
default: default:
return "" return ""
} }

View File

@ -6,29 +6,54 @@ import UIKit
//swiftlint:disable:next type_body_length //swiftlint:disable:next type_body_length
class TunnelViewModel { class TunnelViewModel {
enum InterfaceField: String { enum InterfaceField {
case name = "Name" case name
case privateKey = "Private key" case privateKey
case publicKey = "Public key" case publicKey
case generateKeyPair = "Generate keypair" case generateKeyPair
case addresses = "Addresses" case addresses
case listenPort = "Listen port" case listenPort
case mtu = "MTU" case mtu
case dns = "DNS servers" case dns
var localizedUIString: String {
switch self {
case .name: return tr("tunnelInterfaceName")
case .privateKey: return tr("tunnelInterfacePrivateKey")
case .publicKey: return tr("tunnelInterfacePublicKey")
case .generateKeyPair: return tr("tunnelInterfaceGenerateKeypair")
case .addresses: return tr("tunnelInterfaceAddresses")
case .listenPort: return tr("tunnelInterfaceListenPort")
case .mtu: return tr("tunnelInterfaceMTU")
case .dns: return tr("tunnelInterfaceDNS")
}
}
} }
static let interfaceFieldsWithControl: Set<InterfaceField> = [ static let interfaceFieldsWithControl: Set<InterfaceField> = [
.generateKeyPair .generateKeyPair
] ]
enum PeerField: String { enum PeerField {
case publicKey = "Public key" case publicKey
case preSharedKey = "Preshared key" case preSharedKey
case endpoint = "Endpoint" case endpoint
case persistentKeepAlive = "Persistent keepalive" case persistentKeepAlive
case allowedIPs = "Allowed IPs" case allowedIPs
case excludePrivateIPs = "Exclude private IPs" case excludePrivateIPs
case deletePeer = "Delete peer" case deletePeer
var localizedUIString: String {
switch self {
case .publicKey: return tr("tunnelPeerPublicKey")
case .preSharedKey: return tr("tunnelPeerPreSharedKey")
case .endpoint: return tr("tunnelPeerEndpoint")
case .persistentKeepAlive: return tr("tunnelPeerPersistentKeepalive")
case .allowedIPs: return tr("tunnelPeerAllowedIPs")
case .excludePrivateIPs: return tr("tunnelPeerExcludePrivateIPs")
case .deletePeer: return tr("deletePeerButtonTitle")
}
}
} }
static let peerFieldsWithControl: Set<PeerField> = [ static let peerFieldsWithControl: Set<PeerField> = [
@ -103,15 +128,15 @@ class TunnelViewModel {
fieldsWithError.removeAll() fieldsWithError.removeAll()
guard let name = scratchpad[.name]?.trimmingCharacters(in: .whitespacesAndNewlines), (!name.isEmpty) else { guard let name = scratchpad[.name]?.trimmingCharacters(in: .whitespacesAndNewlines), (!name.isEmpty) else {
fieldsWithError.insert(.name) fieldsWithError.insert(.name)
return .error("Interface name is required") return .error(tr("alertInvalidInterfaceMessageNameRequired"))
} }
guard let privateKeyString = scratchpad[.privateKey] else { guard let privateKeyString = scratchpad[.privateKey] else {
fieldsWithError.insert(.privateKey) fieldsWithError.insert(.privateKey)
return .error("Interface's private key is required") return .error(tr("alertInvalidInterfaceMessagePrivateKeyRequired"))
} }
guard let privateKey = Data(base64Encoded: privateKeyString), privateKey.count == TunnelConfiguration.keyLength else { guard let privateKey = Data(base64Encoded: privateKeyString), privateKey.count == TunnelConfiguration.keyLength else {
fieldsWithError.insert(.privateKey) fieldsWithError.insert(.privateKey)
return .error("Interface's private key must be a 32-byte key in base64 encoding") return .error(tr("alertInvalidInterfaceMessagePrivateKeyInvalid"))
} }
var config = InterfaceConfiguration(name: name, privateKey: privateKey) var config = InterfaceConfiguration(name: name, privateKey: privateKey)
var errorMessages = [String]() var errorMessages = [String]()
@ -122,7 +147,7 @@ class TunnelViewModel {
addresses.append(address) addresses.append(address)
} else { } else {
fieldsWithError.insert(.addresses) fieldsWithError.insert(.addresses)
errorMessages.append("Interface addresses must be a list of comma-separated IP addresses, optionally in CIDR notation") errorMessages.append(tr("alertInvalidInterfaceMessageAddressInvalid"))
} }
} }
config.addresses = addresses config.addresses = addresses
@ -132,7 +157,7 @@ class TunnelViewModel {
config.listenPort = listenPort config.listenPort = listenPort
} else { } else {
fieldsWithError.insert(.listenPort) fieldsWithError.insert(.listenPort)
errorMessages.append("Interface's listen port must be between 0 and 65535, or unspecified") errorMessages.append(tr("alertInvalidInterfaceMessageListenPortInvalid"))
} }
} }
if let mtuString = scratchpad[.mtu] { if let mtuString = scratchpad[.mtu] {
@ -140,7 +165,7 @@ class TunnelViewModel {
config.mtu = mtu config.mtu = mtu
} else { } else {
fieldsWithError.insert(.mtu) fieldsWithError.insert(.mtu)
errorMessages.append("Interface's MTU must be between 576 and 65535, or unspecified") errorMessages.append(tr("alertInvalidInterfaceMessageMTUInvalid"))
} }
} }
if let dnsString = scratchpad[.dns] { if let dnsString = scratchpad[.dns] {
@ -150,7 +175,7 @@ class TunnelViewModel {
dnsServers.append(dnsServer) dnsServers.append(dnsServer)
} else { } else {
fieldsWithError.insert(.dns) fieldsWithError.insert(.dns)
errorMessages.append("Interface's DNS servers must be a list of comma-separated IP addresses") errorMessages.append(tr("alertInvalidInterfaceMessageDNSInvalid"))
} }
} }
config.dns = dnsServers config.dns = dnsServers
@ -243,11 +268,11 @@ class TunnelViewModel {
fieldsWithError.removeAll() fieldsWithError.removeAll()
guard let publicKeyString = scratchpad[.publicKey] else { guard let publicKeyString = scratchpad[.publicKey] else {
fieldsWithError.insert(.publicKey) fieldsWithError.insert(.publicKey)
return .error("Peer's public key is required") return .error(tr("alertInvalidPeerMessagePublicKeyRequired"))
} }
guard let publicKey = Data(base64Encoded: publicKeyString), publicKey.count == TunnelConfiguration.keyLength else { guard let publicKey = Data(base64Encoded: publicKeyString), publicKey.count == TunnelConfiguration.keyLength else {
fieldsWithError.insert(.publicKey) fieldsWithError.insert(.publicKey)
return .error("Peer's public key must be a 32-byte key in base64 encoding") return .error(tr("alertInvalidPeerMessagePublicKeyInvalid"))
} }
var config = PeerConfiguration(publicKey: publicKey) var config = PeerConfiguration(publicKey: publicKey)
var errorMessages = [String]() var errorMessages = [String]()
@ -256,7 +281,7 @@ class TunnelViewModel {
config.preSharedKey = preSharedKey config.preSharedKey = preSharedKey
} else { } else {
fieldsWithError.insert(.preSharedKey) fieldsWithError.insert(.preSharedKey)
errorMessages.append("Peer's preshared key must be a 32-byte key in base64 encoding") errorMessages.append(tr("alertInvalidPeerMessagePreSharedKeyInvalid"))
} }
} }
if let allowedIPsString = scratchpad[.allowedIPs] { if let allowedIPsString = scratchpad[.allowedIPs] {
@ -266,7 +291,7 @@ class TunnelViewModel {
allowedIPs.append(allowedIP) allowedIPs.append(allowedIP)
} else { } else {
fieldsWithError.insert(.allowedIPs) fieldsWithError.insert(.allowedIPs)
errorMessages.append("Peer's allowed IPs must be a list of comma-separated IP addresses, optionally in CIDR notation") errorMessages.append(tr("alertInvalidPeerMessageAllowedIPsInvalid"))
} }
} }
config.allowedIPs = allowedIPs config.allowedIPs = allowedIPs
@ -276,7 +301,7 @@ class TunnelViewModel {
config.endpoint = endpoint config.endpoint = endpoint
} else { } else {
fieldsWithError.insert(.endpoint) fieldsWithError.insert(.endpoint)
errorMessages.append("Peer's endpoint must be of the form 'host:port' or '[host]:port'") errorMessages.append(tr("alertInvalidPeerMessageEndpointInvalid"))
} }
} }
if let persistentKeepAliveString = scratchpad[.persistentKeepAlive] { if let persistentKeepAliveString = scratchpad[.persistentKeepAlive] {
@ -284,7 +309,7 @@ class TunnelViewModel {
config.persistentKeepAlive = persistentKeepAlive config.persistentKeepAlive = persistentKeepAlive
} else { } else {
fieldsWithError.insert(.persistentKeepAlive) fieldsWithError.insert(.persistentKeepAlive)
errorMessages.append("Peer's persistent keepalive must be between 0 to 65535, or unspecified") errorMessages.append(tr("alertInvalidPeerMessagePersistentKeepaliveInvalid"))
} }
} }
@ -354,7 +379,7 @@ class TunnelViewModel {
enum SaveResult<Configuration> { enum SaveResult<Configuration> {
case saved(Configuration) case saved(Configuration)
case error(String) // TODO: Localize error messages case error(String)
} }
var interfaceData: InterfaceData var interfaceData: InterfaceData
@ -425,7 +450,7 @@ class TunnelViewModel {
let peerPublicKeysArray = peerConfigurations.map { $0.publicKey } let peerPublicKeysArray = peerConfigurations.map { $0.publicKey }
let peerPublicKeysSet = Set<Data>(peerPublicKeysArray) let peerPublicKeysSet = Set<Data>(peerPublicKeysArray)
if peerPublicKeysArray.count != peerPublicKeysSet.count { if peerPublicKeysArray.count != peerPublicKeysSet.count {
return .error("Two or more peers cannot have the same public key") return .error(tr("alertInvalidPeerMessagePublicKeyDuplicated"))
} }
let tunnelConfiguration = TunnelConfiguration(interface: interfaceConfiguration, peers: peerConfigurations) let tunnelConfiguration = TunnelConfiguration(interface: interfaceConfiguration, peers: peerConfigurations)
@ -440,13 +465,13 @@ extension TunnelViewModel {
static func activateOnDemandOptionText(for activateOnDemandOption: ActivateOnDemandOption) -> String { static func activateOnDemandOptionText(for activateOnDemandOption: ActivateOnDemandOption) -> String {
switch activateOnDemandOption { switch activateOnDemandOption {
case .none: case .none:
return "Off" return tr("tunnelOnDemandOptionOff")
case .useOnDemandOverWiFiOrCellular: case .useOnDemandOverWiFiOrCellular:
return "Wi-Fi or cellular" return tr("tunnelOnDemandOptionWiFiOrCellular")
case .useOnDemandOverWiFiOnly: case .useOnDemandOverWiFiOnly:
return "Wi-Fi only" return tr("tunnelOnDemandOptionWiFiOnly")
case .useOnDemandOverCellularOnly: case .useOnDemandOverCellularOnly:
return "Cellular only" return tr("tunnelOnDemandOptionCellularOnly")
} }
} }

View File

@ -17,11 +17,11 @@ class QRScanViewController: UIViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
title = "Scan QR code" title = tr("scanQRCodeViewTitle")
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelTapped)) navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelTapped))
let tipLabel = UILabel() let tipLabel = UILabel()
tipLabel.text = "Tip: Generate with `qrencode -t ansiutf8 < tunnel.conf`" tipLabel.text = tr("scanQRCodeTipText")
tipLabel.adjustsFontSizeToFitWidth = true tipLabel.adjustsFontSizeToFitWidth = true
tipLabel.textColor = .lightGray tipLabel.textColor = .lightGray
tipLabel.textAlignment = .center tipLabel.textAlignment = .center
@ -39,7 +39,7 @@ class QRScanViewController: UIViewController {
let captureSession = captureSession, let captureSession = captureSession,
captureSession.canAddInput(videoInput), captureSession.canAddInput(videoInput),
captureSession.canAddOutput(metadataOutput) else { captureSession.canAddOutput(metadataOutput) else {
scanDidEncounterError(title: "Camera Unsupported", message: "This device is not able to scan QR codes") scanDidEncounterError(title: tr("alertScanQRCodeCameraUnsupportedTitle"), message: tr("alertScanQRCodeCameraUnsupportedMessage"))
return return
} }
@ -103,16 +103,16 @@ class QRScanViewController: UIViewController {
func scanDidComplete(withCode code: String) { func scanDidComplete(withCode code: String) {
let scannedTunnelConfiguration = try? WgQuickConfigFileParser.parse(code, name: "Scanned") let scannedTunnelConfiguration = try? WgQuickConfigFileParser.parse(code, name: "Scanned")
guard let tunnelConfiguration = scannedTunnelConfiguration else { guard let tunnelConfiguration = scannedTunnelConfiguration else {
scanDidEncounterError(title: "Invalid QR Code", message: "The scanned QR code is not a valid WireGuard configuration") scanDidEncounterError(title: tr("alertScanQRCodeInvalidQRCodeTitle"), message: tr("alertScanQRCodeInvalidQRCodeMessage"))
return return
} }
let alert = UIAlertController(title: NSLocalizedString("Please name the scanned tunnel", comment: ""), message: nil, preferredStyle: .alert) let alert = UIAlertController(title: tr("alertScanQRCodeNamePromptTitle"), message: nil, preferredStyle: .alert)
alert.addTextField(configurationHandler: nil) alert.addTextField(configurationHandler: nil)
alert.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel) { [weak self] _ in alert.addAction(UIAlertAction(title: tr("actionCancel"), style: .cancel) { [weak self] _ in
self?.dismiss(animated: true, completion: nil) self?.dismiss(animated: true, completion: nil)
}) })
alert.addAction(UIAlertAction(title: NSLocalizedString("Save", comment: ""), style: .default) { [weak self] _ in alert.addAction(UIAlertAction(title: tr("actionSave"), style: .default) { [weak self] _ in
guard let title = alert.textFields?[0].text?.trimmingCharacters(in: .whitespacesAndNewlines), !title.isEmpty else { return } guard let title = alert.textFields?[0].text?.trimmingCharacters(in: .whitespacesAndNewlines), !title.isEmpty else { return }
tunnelConfiguration.interface.name = title tunnelConfiguration.interface.name = title
if let self = self { if let self = self {
@ -126,7 +126,7 @@ class QRScanViewController: UIViewController {
func scanDidEncounterError(title: String, message: String) { func scanDidEncounterError(title: String, message: String) {
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "OK", style: .default) { [weak self] _ in alertController.addAction(UIAlertAction(title: tr("actionOK"), style: .default) { [weak self] _ in
self?.dismiss(animated: true, completion: nil) self?.dismiss(animated: true, completion: nil)
}) })
present(alertController, animated: true) present(alertController, animated: true)
@ -145,7 +145,7 @@ extension QRScanViewController: AVCaptureMetadataOutputObjectsDelegate {
guard let metadataObject = metadataObjects.first, guard let metadataObject = metadataObjects.first,
let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject, let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject,
let stringValue = readableObject.stringValue else { let stringValue = readableObject.stringValue else {
scanDidEncounterError(title: "Invalid Code", message: "The scanned code could not be read") scanDidEncounterError(title: tr("alertScanQRCodeUnreadableQRCodeTitle"), message: tr("alertScanQRCodeUnreadableQRCodeMessage"))
return return
} }

View File

@ -6,11 +6,20 @@ import os.log
class SettingsTableViewController: UITableViewController { class SettingsTableViewController: UITableViewController {
enum SettingsFields: String { enum SettingsFields {
case iosAppVersion = "WireGuard for iOS" case iosAppVersion
case goBackendVersion = "WireGuard Go Backend" case goBackendVersion
case exportZipArchive = "Export zip archive" case exportZipArchive
case exportLogFile = "Export log file" case exportLogFile
var localizedUIString: String {
switch self {
case .iosAppVersion: return tr("settingsVersionKeyWireGuardForIOS")
case .goBackendVersion: return tr("settingsVersionKeyWireGuardGoBackend")
case .exportZipArchive: return tr("settingsExportZipButtonTitle")
case .exportLogFile: return tr("settingsExportLogFileButtonTitle")
}
}
} }
let settingsFieldsBySection: [[SettingsFields]] = [ let settingsFieldsBySection: [[SettingsFields]] = [
@ -33,7 +42,7 @@ class SettingsTableViewController: UITableViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
title = "Settings" title = tr("settingsViewTitle")
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneTapped)) navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneTapped))
tableView.estimatedRowHeight = 44 tableView.estimatedRowHeight = 44
@ -109,19 +118,19 @@ class SettingsTableViewController: UITableViewController {
if FileManager.default.fileExists(atPath: destinationURL.path) { if FileManager.default.fileExists(atPath: destinationURL.path) {
let isDeleted = FileManager.deleteFile(at: destinationURL) let isDeleted = FileManager.deleteFile(at: destinationURL)
if !isDeleted { if !isDeleted {
ErrorPresenter.showErrorAlert(title: "Log export failed", message: "The pre-existing log could not be cleared", from: self) ErrorPresenter.showErrorAlert(title: tr("alertUnableToRemovePreviousLogTitle"), message: tr("alertUnableToRemovePreviousLogMessage"), from: self)
return return
} }
} }
guard let networkExtensionLogFilePath = FileManager.networkExtensionLogFileURL?.path else { guard let networkExtensionLogFilePath = FileManager.networkExtensionLogFileURL?.path else {
ErrorPresenter.showErrorAlert(title: "Log export failed", message: "Unable to determine extension log path", from: self) ErrorPresenter.showErrorAlert(title: tr("alertUnableToFindExtensionLogPathTitle"), message: tr("alertUnableToFindExtensionLogPathMessage"), from: self)
return return
} }
let isWritten = Logger.global?.writeLog(called: "APP", mergedWith: networkExtensionLogFilePath, called: "NET", to: destinationURL.path) ?? false let isWritten = Logger.global?.writeLog(called: "APP", mergedWith: networkExtensionLogFilePath, called: "NET", to: destinationURL.path) ?? false
guard isWritten else { guard isWritten else {
ErrorPresenter.showErrorAlert(title: "Log export failed", message: "Unable to write logs to file", from: self) ErrorPresenter.showErrorAlert(title: tr("alertUnableToWriteLogTitle"), message: tr("alertUnableToWriteLogMessage"), from: self)
return return
} }
@ -153,11 +162,11 @@ extension SettingsTableViewController {
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
switch section { switch section {
case 0: case 0:
return "About" return tr("settingsSectionTitleAbout")
case 1: case 1:
return "Export configurations" return tr("settingsSectionTitleExportConfigurations")
case 2: case 2:
return "Tunnel log" return tr("settingsSectionTitleTunnelLog")
default: default:
return nil return nil
} }
@ -168,7 +177,7 @@ extension SettingsTableViewController {
if field == .iosAppVersion || field == .goBackendVersion { if field == .iosAppVersion || field == .goBackendVersion {
let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath) let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.copyableGesture = false cell.copyableGesture = false
cell.key = field.rawValue cell.key = field.localizedUIString
if field == .iosAppVersion { if field == .iosAppVersion {
var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown version" var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown version"
if let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String { if let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String {
@ -181,7 +190,7 @@ extension SettingsTableViewController {
return cell return cell
} else if field == .exportZipArchive { } else if field == .exportZipArchive {
let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath) let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
cell.buttonText = field.rawValue cell.buttonText = field.localizedUIString
cell.onTapped = { [weak self] in cell.onTapped = { [weak self] in
self?.exportConfigurationsAsZipFile(sourceView: cell.button) self?.exportConfigurationsAsZipFile(sourceView: cell.button)
} }
@ -189,7 +198,7 @@ extension SettingsTableViewController {
} else { } else {
assert(field == .exportLogFile) assert(field == .exportLogFile)
let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath) let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
cell.buttonText = field.rawValue cell.buttonText = field.localizedUIString
cell.onTapped = { [weak self] in cell.onTapped = { [weak self] in
self?.exportLogForLastActivatedTunnel(sourceView: cell.button) self?.exportLogForLastActivatedTunnel(sourceView: cell.button)
} }

View File

@ -85,7 +85,7 @@ class TunnelDetailTableViewController: UITableViewController {
let destroyAction = UIAlertAction(title: buttonTitle, style: .destructive) { _ in let destroyAction = UIAlertAction(title: buttonTitle, style: .destructive) { _ in
onConfirmed() onConfirmed()
} }
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) let cancelAction = UIAlertAction(title: tr("actionCancel"), style: .cancel)
let alert = UIAlertController(title: "", message: message, preferredStyle: .actionSheet) let alert = UIAlertController(title: "", message: message, preferredStyle: .actionSheet)
alert.addAction(destroyAction) alert.addAction(destroyAction)
alert.addAction(cancelAction) alert.addAction(cancelAction)
@ -137,13 +137,13 @@ extension TunnelDetailTableViewController {
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
switch sections[section] { switch sections[section] {
case .status: case .status:
return "Status" return tr("tunnelSectionTitleStatus")
case .interface: case .interface:
return "Interface" return tr("tunnelSectionTitleInterface")
case .peer: case .peer:
return "Peer" return tr("tunnelSectionTitlePeer")
case .onDemand: case .onDemand:
return "On-Demand Activation" return tr("tunnelSectionTitleOnDemand")
case .delete: case .delete:
return nil return nil
} }
@ -171,19 +171,19 @@ extension TunnelDetailTableViewController {
let text: String let text: String
switch status { switch status {
case .inactive: case .inactive:
text = "Inactive" text = tr("tunnelStatusInactive")
case .activating: case .activating:
text = "Activating" text = tr("tunnelStatusActivating")
case .active: case .active:
text = "Active" text = tr("tunnelStatusActive")
case .deactivating: case .deactivating:
text = "Deactivating" text = tr("tunnelStatusDeactivating")
case .reasserting: case .reasserting:
text = "Reactivating" text = tr("tunnelStatusReasserting")
case .restarting: case .restarting:
text = "Restarting" text = tr("tunnelStatusRestarting")
case .waiting: case .waiting:
text = "Waiting" text = tr("tunnelStatusWaiting")
} }
cell.textLabel?.text = text cell.textLabel?.text = text
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) { [weak cell] in DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) { [weak cell] in
@ -213,7 +213,7 @@ extension TunnelDetailTableViewController {
private func interfaceCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell { private func interfaceCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
let field = tunnelViewModel.interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFields)[indexPath.row] let field = tunnelViewModel.interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFields)[indexPath.row]
let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath) let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.key = field.rawValue cell.key = field.localizedUIString
cell.value = tunnelViewModel.interfaceData[field] cell.value = tunnelViewModel.interfaceData[field]
return cell return cell
} }
@ -221,14 +221,14 @@ extension TunnelDetailTableViewController {
private func peerCell(for tableView: UITableView, at indexPath: IndexPath, with peerData: TunnelViewModel.PeerData) -> UITableViewCell { private func peerCell(for tableView: UITableView, at indexPath: IndexPath, with peerData: TunnelViewModel.PeerData) -> UITableViewCell {
let field = peerData.filterFieldsWithValueOrControl(peerFields: peerFields)[indexPath.row] let field = peerData.filterFieldsWithValueOrControl(peerFields: peerFields)[indexPath.row]
let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath) let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.key = field.rawValue cell.key = field.localizedUIString
cell.value = peerData[field] cell.value = peerData[field]
return cell return cell
} }
private func onDemandCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell { private func onDemandCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath) let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.key = "Activate on demand" cell.key = tr("tunnelOnDemandKey")
cell.value = TunnelViewModel.activateOnDemandDetailText(for: tunnel.activateOnDemandSetting()) cell.value = TunnelViewModel.activateOnDemandDetailText(for: tunnel.activateOnDemandSetting())
onDemandStatusObservationToken = tunnel.observe(\.isActivateOnDemandEnabled) { [weak cell] tunnel, _ in onDemandStatusObservationToken = tunnel.observe(\.isActivateOnDemandEnabled) { [weak cell] tunnel, _ in
cell?.value = TunnelViewModel.activateOnDemandDetailText(for: tunnel.activateOnDemandSetting()) cell?.value = TunnelViewModel.activateOnDemandDetailText(for: tunnel.activateOnDemandSetting())
@ -238,11 +238,11 @@ extension TunnelDetailTableViewController {
private func deleteConfigurationCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell { private func deleteConfigurationCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath) let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
cell.buttonText = "Delete tunnel" cell.buttonText = tr("deleteTunnelButtonTitle")
cell.hasDestructiveAction = true cell.hasDestructiveAction = true
cell.onTapped = { [weak self] in cell.onTapped = { [weak self] in
guard let self = self else { return } guard let self = self else { return }
self.showConfirmationAlert(message: "Delete this tunnel?", buttonTitle: "Delete", from: cell) { [weak self] in self.showConfirmationAlert(message: tr("deleteTunnelConfirmationAlertMessage"), buttonTitle: tr("deleteTunnelConfirmationAlertButtonTitle"), from: cell) { [weak self] in
guard let self = self else { return } guard let self = self else { return }
self.tunnelsManager.remove(tunnel: self.tunnel) { error in self.tunnelsManager.remove(tunnel: self.tunnel) { error in
if error != nil { if error != nil {

View File

@ -71,7 +71,7 @@ class TunnelEditTableViewController: UITableViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
title = tunnel == nil ? "New configuration" : "Edit configuration" title = tunnel == nil ? tr("newTunnelViewTitle") : tr("editTunnelViewTitle")
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .save, target: self, action: #selector(saveTapped)) navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .save, target: self, action: #selector(saveTapped))
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelTapped)) navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelTapped))
@ -98,8 +98,9 @@ class TunnelEditTableViewController: UITableViewController {
let tunnelSaveResult = tunnelViewModel.save() let tunnelSaveResult = tunnelViewModel.save()
switch tunnelSaveResult { switch tunnelSaveResult {
case .error(let errorMessage): case .error(let errorMessage):
let erroringConfiguration = (tunnelViewModel.interfaceData.validatedConfiguration == nil) ? "Interface" : "Peer" let alertTitle = (tunnelViewModel.interfaceData.validatedConfiguration == nil) ?
ErrorPresenter.showErrorAlert(title: "Invalid \(erroringConfiguration)", message: errorMessage, from: self) tr("alertInvalidInterfaceTitle") : tr("alertInvalidPeerTitle")
ErrorPresenter.showErrorAlert(title: alertTitle, message: errorMessage, from: self)
tableView.reloadData() // Highlight erroring fields tableView.reloadData() // Highlight erroring fields
case .saved(let tunnelConfiguration): case .saved(let tunnelConfiguration):
if let tunnel = tunnel { if let tunnel = tunnel {
@ -164,13 +165,13 @@ extension TunnelEditTableViewController {
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
switch sections[section] { switch sections[section] {
case .interface: case .interface:
return section == 0 ? "Interface" : nil return section == 0 ? tr("tunnelSectionTitleInterface") : nil
case .peer: case .peer:
return "Peer" return tr("tunnelSectionTitlePeer")
case .addPeer: case .addPeer:
return nil return nil
case .onDemand: case .onDemand:
return "On-Demand Activation" return tr("tunnelSectionTitleOnDemand")
} }
} }
@ -201,7 +202,7 @@ extension TunnelEditTableViewController {
private func generateKeyPairCell(for tableView: UITableView, at indexPath: IndexPath, with field: TunnelViewModel.InterfaceField) -> UITableViewCell { private func generateKeyPairCell(for tableView: UITableView, at indexPath: IndexPath, with field: TunnelViewModel.InterfaceField) -> UITableViewCell {
let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath) let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
cell.buttonText = field.rawValue cell.buttonText = field.localizedUIString
cell.onTapped = { [weak self] in cell.onTapped = { [weak self] in
guard let self = self else { return } guard let self = self else { return }
@ -218,24 +219,24 @@ extension TunnelEditTableViewController {
private func publicKeyCell(for tableView: UITableView, at indexPath: IndexPath, with field: TunnelViewModel.InterfaceField) -> UITableViewCell { private func publicKeyCell(for tableView: UITableView, at indexPath: IndexPath, with field: TunnelViewModel.InterfaceField) -> UITableViewCell {
let cell: TunnelEditKeyValueCell = tableView.dequeueReusableCell(for: indexPath) let cell: TunnelEditKeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.key = field.rawValue cell.key = field.localizedUIString
cell.value = tunnelViewModel.interfaceData[field] cell.value = tunnelViewModel.interfaceData[field]
return cell return cell
} }
private func interfaceFieldKeyValueCell(for tableView: UITableView, at indexPath: IndexPath, with field: TunnelViewModel.InterfaceField) -> UITableViewCell { private func interfaceFieldKeyValueCell(for tableView: UITableView, at indexPath: IndexPath, with field: TunnelViewModel.InterfaceField) -> UITableViewCell {
let cell: TunnelEditEditableKeyValueCell = tableView.dequeueReusableCell(for: indexPath) let cell: TunnelEditEditableKeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.key = field.rawValue cell.key = field.localizedUIString
switch field { switch field {
case .name, .privateKey: case .name, .privateKey:
cell.placeholderText = "Required" cell.placeholderText = tr("tunnelEditPlaceholderTextRequired")
cell.keyboardType = .default cell.keyboardType = .default
case .addresses, .dns: case .addresses, .dns:
cell.placeholderText = "Optional" cell.placeholderText = tr("tunnelEditPlaceholderTextOptional")
cell.keyboardType = .numbersAndPunctuation cell.keyboardType = .numbersAndPunctuation
case .listenPort, .mtu: case .listenPort, .mtu:
cell.placeholderText = "Automatic" cell.placeholderText = tr("tunnelEditPlaceholderTextAutomatic")
cell.keyboardType = .numberPad cell.keyboardType = .numberPad
case .publicKey, .generateKeyPair: case .publicKey, .generateKeyPair:
cell.keyboardType = .default cell.keyboardType = .default
@ -283,12 +284,12 @@ extension TunnelEditTableViewController {
private func deletePeerCell(for tableView: UITableView, at indexPath: IndexPath, peerData: TunnelViewModel.PeerData, field: TunnelViewModel.PeerField) -> UITableViewCell { private func deletePeerCell(for tableView: UITableView, at indexPath: IndexPath, peerData: TunnelViewModel.PeerData, field: TunnelViewModel.PeerField) -> UITableViewCell {
let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath) let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
cell.buttonText = field.rawValue cell.buttonText = field.localizedUIString
cell.hasDestructiveAction = true cell.hasDestructiveAction = true
cell.onTapped = { [weak self, weak peerData] in cell.onTapped = { [weak self, weak peerData] in
guard let peerData = peerData else { return } guard let peerData = peerData else { return }
guard let self = self else { return } guard let self = self else { return }
self.showConfirmationAlert(message: "Delete this peer?", buttonTitle: "Delete", from: cell) { [weak self] in self.showConfirmationAlert(message: tr("deletePeerConfirmationAlertMessage"), buttonTitle: tr("deletePeerConfirmationAlertButtonTitle"), from: cell) { [weak self] in
guard let self = self else { return } guard let self = self else { return }
let removedSectionIndices = self.deletePeer(peer: peerData) let removedSectionIndices = self.deletePeer(peer: peerData)
let shouldShowExcludePrivateIPs = (self.tunnelViewModel.peersData.count == 1 && self.tunnelViewModel.peersData[0].shouldAllowExcludePrivateIPsControl) let shouldShowExcludePrivateIPs = (self.tunnelViewModel.peersData.count == 1 && self.tunnelViewModel.peersData[0].shouldAllowExcludePrivateIPsControl)
@ -309,7 +310,7 @@ extension TunnelEditTableViewController {
private func excludePrivateIPsCell(for tableView: UITableView, at indexPath: IndexPath, peerData: TunnelViewModel.PeerData, field: TunnelViewModel.PeerField) -> UITableViewCell { private func excludePrivateIPsCell(for tableView: UITableView, at indexPath: IndexPath, peerData: TunnelViewModel.PeerData, field: TunnelViewModel.PeerField) -> UITableViewCell {
let cell: SwitchCell = tableView.dequeueReusableCell(for: indexPath) let cell: SwitchCell = tableView.dequeueReusableCell(for: indexPath)
cell.message = field.rawValue cell.message = field.localizedUIString
cell.isEnabled = peerData.shouldAllowExcludePrivateIPsControl cell.isEnabled = peerData.shouldAllowExcludePrivateIPsControl
cell.isOn = peerData.excludePrivateIPsValue cell.isOn = peerData.excludePrivateIPsValue
cell.onSwitchToggled = { [weak self] isOn in cell.onSwitchToggled = { [weak self] isOn in
@ -324,20 +325,20 @@ extension TunnelEditTableViewController {
private func peerFieldKeyValueCell(for tableView: UITableView, at indexPath: IndexPath, peerData: TunnelViewModel.PeerData, field: TunnelViewModel.PeerField) -> UITableViewCell { private func peerFieldKeyValueCell(for tableView: UITableView, at indexPath: IndexPath, peerData: TunnelViewModel.PeerData, field: TunnelViewModel.PeerField) -> UITableViewCell {
let cell: TunnelEditEditableKeyValueCell = tableView.dequeueReusableCell(for: indexPath) let cell: TunnelEditEditableKeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.key = field.rawValue cell.key = field.localizedUIString
switch field { switch field {
case .publicKey: case .publicKey:
cell.placeholderText = "Required" cell.placeholderText = tr("tunnelEditPlaceholderTextRequired")
cell.keyboardType = .default cell.keyboardType = .default
case .preSharedKey, .endpoint: case .preSharedKey, .endpoint:
cell.placeholderText = "Optional" cell.placeholderText = tr("tunnelEditPlaceholderTextOptional")
cell.keyboardType = .default cell.keyboardType = .default
case .allowedIPs: case .allowedIPs:
cell.placeholderText = "Optional" cell.placeholderText = tr("tunnelEditPlaceholderTextOptional")
cell.keyboardType = .numbersAndPunctuation cell.keyboardType = .numbersAndPunctuation
case .persistentKeepAlive: case .persistentKeepAlive:
cell.placeholderText = "Off" cell.placeholderText = tr("tunnelEditPlaceholderTextOff")
cell.keyboardType = .numberPad cell.keyboardType = .numberPad
case .excludePrivateIPs, .deletePeer: case .excludePrivateIPs, .deletePeer:
cell.keyboardType = .default cell.keyboardType = .default
@ -373,7 +374,7 @@ extension TunnelEditTableViewController {
private func addPeerCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell { private func addPeerCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath) let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
cell.buttonText = "Add peer" cell.buttonText = tr("addPeerButtonTitle")
cell.onTapped = { [weak self] in cell.onTapped = { [weak self] in
guard let self = self else { return } guard let self = self else { return }
let shouldHideExcludePrivateIPs = (self.tunnelViewModel.peersData.count == 1 && self.tunnelViewModel.peersData[0].shouldAllowExcludePrivateIPsControl) let shouldHideExcludePrivateIPs = (self.tunnelViewModel.peersData.count == 1 && self.tunnelViewModel.peersData[0].shouldAllowExcludePrivateIPsControl)
@ -394,7 +395,7 @@ extension TunnelEditTableViewController {
private func onDemandCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell { private func onDemandCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == 0 { if indexPath.row == 0 {
let cell: SwitchCell = tableView.dequeueReusableCell(for: indexPath) let cell: SwitchCell = tableView.dequeueReusableCell(for: indexPath)
cell.message = "Activate on demand" cell.message = tr("tunnelOnDemandKey")
cell.isOn = activateOnDemandSetting.isActivateOnDemandEnabled cell.isOn = activateOnDemandSetting.isActivateOnDemandEnabled
cell.onSwitchToggled = { [weak self] isOn in cell.onSwitchToggled = { [weak self] isOn in
guard let self = self else { return } guard let self = self else { return }
@ -443,7 +444,7 @@ extension TunnelEditTableViewController {
let destroyAction = UIAlertAction(title: buttonTitle, style: .destructive) { _ in let destroyAction = UIAlertAction(title: buttonTitle, style: .destructive) { _ in
onConfirmed() onConfirmed()
} }
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) let cancelAction = UIAlertAction(title: tr("actionCancel"), style: .cancel)
let alert = UIAlertController(title: "", message: message, preferredStyle: .actionSheet) let alert = UIAlertController(title: "", message: message, preferredStyle: .actionSheet)
alert.addAction(destroyAction) alert.addAction(destroyAction)
alert.addAction(cancelAction) alert.addAction(cancelAction)

View File

@ -20,7 +20,7 @@ class TunnelsListTableViewController: UIViewController {
let centeredAddButton: BorderedTextButton = { let centeredAddButton: BorderedTextButton = {
let button = BorderedTextButton() let button = BorderedTextButton()
button.title = "Add a tunnel" button.title = tr("tunnelsListCenteredAddTunnelButtonTitle")
button.isHidden = true button.isHidden = true
return button return button
}() }()
@ -72,9 +72,9 @@ class TunnelsListTableViewController: UIViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
title = "WireGuard" title = tr("tunnelsListTitle")
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addButtonTapped(sender:))) navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addButtonTapped(sender:)))
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Settings", style: .plain, target: self, action: #selector(settingsButtonTapped(sender:))) navigationItem.leftBarButtonItem = UIBarButtonItem(title: tr("tunnelsListSettingsButtonTitle"), style: .plain, target: self, action: #selector(settingsButtonTapped(sender:)))
restorationIdentifier = "TunnelsListVC" restorationIdentifier = "TunnelsListVC"
} }
@ -97,25 +97,25 @@ class TunnelsListTableViewController: UIViewController {
@objc func addButtonTapped(sender: AnyObject) { @objc func addButtonTapped(sender: AnyObject) {
guard tunnelsManager != nil else { return } guard tunnelsManager != nil else { return }
let alert = UIAlertController(title: "", message: "Add a new WireGuard tunnel", preferredStyle: .actionSheet) let alert = UIAlertController(title: "", message: tr("addTunnelMenuHeader"), preferredStyle: .actionSheet)
let importFileAction = UIAlertAction(title: "Create from file or archive", style: .default) { [weak self] _ in let importFileAction = UIAlertAction(title: tr("addTunnelMenuImportFile"), style: .default) { [weak self] _ in
self?.presentViewControllerForFileImport() self?.presentViewControllerForFileImport()
} }
alert.addAction(importFileAction) alert.addAction(importFileAction)
let scanQRCodeAction = UIAlertAction(title: "Create from QR code", style: .default) { [weak self] _ in let scanQRCodeAction = UIAlertAction(title: tr("addTunnelMenuQRCode"), style: .default) { [weak self] _ in
self?.presentViewControllerForScanningQRCode() self?.presentViewControllerForScanningQRCode()
} }
alert.addAction(scanQRCodeAction) alert.addAction(scanQRCodeAction)
let createFromScratchAction = UIAlertAction(title: "Create from scratch", style: .default) { [weak self] _ in let createFromScratchAction = UIAlertAction(title: tr("addTunnelMenuFromScratch"), style: .default) { [weak self] _ in
if let self = self, let tunnelsManager = self.tunnelsManager { if let self = self, let tunnelsManager = self.tunnelsManager {
self.presentViewControllerForTunnelCreation(tunnelsManager: tunnelsManager) self.presentViewControllerForTunnelCreation(tunnelsManager: tunnelsManager)
} }
} }
alert.addAction(createFromScratchAction) alert.addAction(createFromScratchAction)
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) let cancelAction = UIAlertAction(title: tr("actionCancel"), style: .cancel)
alert.addAction(cancelAction) alert.addAction(cancelAction)
if let sender = sender as? UIBarButtonItem { if let sender = sender as? UIBarButtonItem {
@ -172,9 +172,9 @@ class TunnelsListTableViewController: UIViewController {
completionHandler?() completionHandler?()
return return
} }
ErrorPresenter.showErrorAlert(title: "Created \(numberSuccessful) tunnels", let title = tr(format: "alertImportedFromZipTitle (%d)", numberSuccessful)
message: "Created \(numberSuccessful) of \(configs.count) tunnels from zip archive", let message = tr(format: "alertImportedFromZipMessage (%1$d of %2$d)", numberSuccessful, configs.count)
from: self, onPresented: completionHandler) ErrorPresenter.showErrorAlert(title: title, message: message, from: self, onPresented: completionHandler)
} }
} }
} else /* if (url.pathExtension == "conf") -- we assume everything else is a conf */ { } else /* if (url.pathExtension == "conf") -- we assume everything else is a conf */ {
@ -189,8 +189,7 @@ class TunnelsListTableViewController: UIViewController {
} }
} }
} else { } else {
ErrorPresenter.showErrorAlert(title: "Unable to import tunnel", ErrorPresenter.showErrorAlert(title: tr("alertUnableToImportTitle"), message: tr("alertUnableToImportMessage"),
message: "An error occured when importing the tunnel configuration.",
from: self, onPresented: completionHandler) from: self, onPresented: completionHandler)
} }
} }
@ -266,7 +265,7 @@ extension TunnelsListTableViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, func tableView(_ tableView: UITableView,
trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let deleteAction = UIContextualAction(style: .destructive, title: "Delete") { [weak self] _, _, completionHandler in let deleteAction = UIContextualAction(style: .destructive, title: tr("tunnelsListSwipeDeleteButtonTitle")) { [weak self] _, _, completionHandler in
guard let tunnelsManager = self?.tunnelsManager else { return } guard let tunnelsManager = self?.tunnelsManager else { return }
let tunnel = tunnelsManager.tunnel(at: indexPath.row) let tunnel = tunnelsManager.tunnel(at: indexPath.row)
tunnelsManager.remove(tunnel: tunnel) { error in tunnelsManager.remove(tunnel: tunnel) { error in

View File

@ -11,11 +11,11 @@ enum ZipArchiveError: WireGuardAppError {
var alertText: AlertText { var alertText: AlertText {
switch self { switch self {
case .cantOpenInputZipFile: case .cantOpenInputZipFile:
return ("Unable to read zip archive", "The zip archive could not be read.") return (tr("alertCantOpenInputZipFileTitle"), tr("alertCantOpenInputZipFileMessage"))
case .cantOpenOutputZipFileForWriting: case .cantOpenOutputZipFileForWriting:
return ("Unable to create zip archive", "Could not open zip file for writing.") return (tr("alertCantOpenOutputZipFileForWritingTitle"), tr("alertCantOpenOutputZipFileForWritingMessage"))
case .badArchive: case .badArchive:
return ("Unable to read zip archive", "Bad or corrupt zip archive.") return (tr("alertBadArchiveTitle"), tr("alertBadArchiveMessage"))
} }
} }
} }

View File

@ -7,7 +7,7 @@ enum ZipExporterError: WireGuardAppError {
case noTunnelsToExport case noTunnelsToExport
var alertText: AlertText { var alertText: AlertText {
return ("Nothing to export", "There are no tunnels to export") return (tr("alertNoTunnelsToExportTitle"), tr("alertNoTunnelsToExportMessage"))
} }
} }

View File

@ -7,7 +7,7 @@ enum ZipImporterError: WireGuardAppError {
case noTunnelsInZipArchive case noTunnelsInZipArchive
var alertText: AlertText { var alertText: AlertText {
return ("No tunnels in zip archive", "No .conf tunnel files were found inside the zip archive.") return (tr("alertNoTunnelsInImportedZipArchiveTitle"), tr("alertNoTunnelsInImportedZipArchiveMessage"))
} }
} }