From 0552d75aa1d9e4e496bd066973fdf59726a4f235 Mon Sep 17 00:00:00 2001 From: Roopesh Chander Date: Tue, 18 Dec 2018 16:30:16 +0530 Subject: [PATCH] Localize all the things Signed-off-by: Roopesh Chander --- WireGuard/WireGuard.xcodeproj/project.pbxproj | 16 ++ .../WireGuard/Base.lproj/Localizable.strings | 210 ++++++++++++++++++ WireGuard/WireGuard/LocalizationHelper.swift | 12 + WireGuard/WireGuard/Tunnel/TunnelErrors.swift | 33 +-- WireGuard/WireGuard/UI/TunnelViewModel.swift | 97 +++++--- .../ViewController/QRScanViewController.swift | 18 +- .../SettingsTableViewController.swift | 39 ++-- .../TunnelDetailTableViewController.swift | 34 +-- .../TunnelEditTableViewController.swift | 47 ++-- .../TunnelsListTableViewController.swift | 27 ++- .../WireGuard/ZipArchive/ZipArchive.swift | 6 +- .../WireGuard/ZipArchive/ZipExporter.swift | 2 +- .../WireGuard/ZipArchive/ZipImporter.swift | 2 +- 13 files changed, 408 insertions(+), 135 deletions(-) create mode 100644 WireGuard/WireGuard/Base.lproj/Localizable.strings create mode 100644 WireGuard/WireGuard/LocalizationHelper.swift diff --git a/WireGuard/WireGuard.xcodeproj/project.pbxproj b/WireGuard/WireGuard.xcodeproj/project.pbxproj index e96b34b..24c59c5 100644 --- a/WireGuard/WireGuard.xcodeproj/project.pbxproj +++ b/WireGuard/WireGuard.xcodeproj/project.pbxproj @@ -53,6 +53,8 @@ 6FDEF802218646BA00D8FBF6 /* ZipArchive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF801218646B900D8FBF6 /* ZipArchive.swift */; }; 6FDEF806218725D200D8FBF6 /* SettingsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF805218725D200D8FBF6 /* SettingsTableViewController.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 */; }; 6FE254FF219C60290028284D /* ZipExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE254FE219C60290028284D /* ZipExporter.swift */; }; 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 = ""; }; 6FDEF805218725D200D8FBF6 /* SettingsTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsTableViewController.swift; sourceTree = ""; }; 6FDEF8072187442100D8FBF6 /* WgQuickConfigFileWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WgQuickConfigFileWriter.swift; sourceTree = ""; }; + 6FE1765521C90BBE002690EA /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = WireGuard/Base.lproj/Localizable.strings; sourceTree = ""; }; + 6FE1765921C90E87002690EA /* LocalizationHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationHelper.swift; sourceTree = ""; }; 6FE254FA219C10800028284D /* ZipImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZipImporter.swift; sourceTree = ""; }; 6FE254FE219C60290028284D /* ZipExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZipExporter.swift; sourceTree = ""; }; 6FF3526B21C23F960008484E /* ringlogger.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ringlogger.h; sourceTree = ""; }; @@ -377,6 +381,7 @@ 6FF4AC0B211EC46F002C96EB = { isa = PBXGroup; children = ( + 6FE1765421C90BBE002690EA /* Localizable.strings */, 6F5D0C432183B4A4000F85AD /* Shared */, 6FF4AC16211EC46F002C96EB /* WireGuard */, 6F5D0C1B218352EF000F85AD /* WireGuardNetworkExtension */, @@ -405,6 +410,7 @@ 6FDEF7E72186320E00D8FBF6 /* ZipArchive */, 6F61F1E821B932F700483816 /* WireGuardAppError.swift */, 6F61F1EA21B937EF00483816 /* WireGuardResult.swift */, + 6FE1765921C90E87002690EA /* LocalizationHelper.swift */, 6FF4AC482120B9E0002C96EB /* WireGuard.entitlements */, 6FF4AC1E211EC472002C96EB /* Assets.xcassets */, 6FF4AC20211EC472002C96EB /* LaunchScreen.storyboard */, @@ -549,6 +555,7 @@ buildActionMask = 2147483647; files = ( 6F919ED9218C65C50023B400 /* wireguard_doc_logo_22x29.png in Resources */, + 6FE1765621C90BBE002690EA /* Localizable.strings in Resources */, 6FF4AC22211EC472002C96EB /* LaunchScreen.storyboard in Resources */, 6F919EDA218C65C50023B400 /* wireguard_doc_logo_44x58.png in Resources */, 6FF4AC1F211EC472002C96EB /* Assets.xcassets in Resources */, @@ -660,6 +667,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 6FE1765A21C90E87002690EA /* LocalizationHelper.swift in Sources */, 6FF3527221C2616C0008484E /* ringlogger.c in Sources */, 6FF3527321C2616C0008484E /* Logger.swift in Sources */, 6F6899AC218099F00012E523 /* WgQuickConfigFileParser.swift in Sources */, @@ -723,6 +731,14 @@ /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ + 6FE1765421C90BBE002690EA /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + 6FE1765521C90BBE002690EA /* Base */, + ); + name = Localizable.strings; + sourceTree = ""; + }; 6FF4AC20211EC472002C96EB /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( diff --git a/WireGuard/WireGuard/Base.lproj/Localizable.strings b/WireGuard/WireGuard/Base.lproj/Localizable.strings new file mode 100644 index 0000000..524cfdc --- /dev/null +++ b/WireGuard/WireGuard/Base.lproj/Localizable.strings @@ -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."; diff --git a/WireGuard/WireGuard/LocalizationHelper.swift b/WireGuard/WireGuard/LocalizationHelper.swift new file mode 100644 index 0000000..ea4cc64 --- /dev/null +++ b/WireGuard/WireGuard/LocalizationHelper.swift @@ -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) +} diff --git a/WireGuard/WireGuard/Tunnel/TunnelErrors.swift b/WireGuard/WireGuard/Tunnel/TunnelErrors.swift index 45c20f6..c3d15b2 100644 --- a/WireGuard/WireGuard/Tunnel/TunnelErrors.swift +++ b/WireGuard/WireGuard/Tunnel/TunnelErrors.swift @@ -14,17 +14,17 @@ enum TunnelsManagerError: WireGuardAppError { var alertText: AlertText { switch self { case .tunnelNameEmpty: - return ("No name provided", "Cannot create tunnel with an empty name") + return (tr("alertTunnelNameEmptyTitle"), tr("alertTunnelNameEmptyMessage")) case .tunnelAlreadyExistsWithThatName: - return ("Name already exists", "A tunnel with that name already exists") + return (tr("alertTunnelAlreadyExistsWithThatNameTitle"), tr("alertTunnelAlreadyExistsWithThatNameMessage")) case .systemErrorOnListingTunnels(let systemError): - return ("Unable to list tunnels", systemError.UIString) + return (tr("alertSystemErrorOnListingTunnelsTitle"), systemError.localizedUIString) case .systemErrorOnAddTunnel(let systemError): - return ("Unable to create tunnel", systemError.UIString) + return (tr("alertSystemErrorOnAddTunnelTitle"), systemError.localizedUIString) case .systemErrorOnModifyTunnel(let systemError): - return ("Unable to modify tunnel", systemError.UIString) + return (tr("alertSystemErrorOnModifyTunnelTitle"), systemError.localizedUIString) 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 { switch self { 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), .failedWhileSaving(let systemError), .failedWhileLoading(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 { switch self { 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): return (title, message) } @@ -64,21 +65,21 @@ enum TunnelsManagerActivationError: WireGuardAppError { } extension Error { - var UIString: String { + var localizedUIString: String { if let systemError = self as? NEVPNError { switch systemError { case NEVPNError.configurationInvalid: - return "The configuration is invalid." + return tr("alertSystemErrorMessageTunnelConfigurationInvalid") case NEVPNError.configurationDisabled: - return "The configuration is disabled." + return tr("alertSystemErrorMessageTunnelConfigurationDisabled") case NEVPNError.connectionFailed: - return "The connection failed." + return tr("alertSystemErrorMessageTunnelConnectionFailed") case NEVPNError.configurationStale: - return "The configuration is stale." + return tr("alertSystemErrorMessageTunnelConfigurationStale") case NEVPNError.configurationReadWriteFailed: - return "Reading or writing the configuration failed." + return tr("alertSystemErrorMessageTunnelConfigurationReadWriteFailed") case NEVPNError.configurationUnknown: - return "Unknown system error." + return tr("alertSystemErrorMessageTunnelConfigurationUnknown") default: return "" } diff --git a/WireGuard/WireGuard/UI/TunnelViewModel.swift b/WireGuard/WireGuard/UI/TunnelViewModel.swift index 71cb18e..f7ebb68 100644 --- a/WireGuard/WireGuard/UI/TunnelViewModel.swift +++ b/WireGuard/WireGuard/UI/TunnelViewModel.swift @@ -6,29 +6,54 @@ import UIKit //swiftlint:disable:next type_body_length class TunnelViewModel { - enum InterfaceField: String { - case name = "Name" - case privateKey = "Private key" - case publicKey = "Public key" - case generateKeyPair = "Generate keypair" - case addresses = "Addresses" - case listenPort = "Listen port" - case mtu = "MTU" - case dns = "DNS servers" + enum InterfaceField { + case name + case privateKey + case publicKey + case generateKeyPair + case addresses + case listenPort + case mtu + 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 = [ .generateKeyPair ] - enum PeerField: String { - case publicKey = "Public key" - case preSharedKey = "Preshared key" - case endpoint = "Endpoint" - case persistentKeepAlive = "Persistent keepalive" - case allowedIPs = "Allowed IPs" - case excludePrivateIPs = "Exclude private IPs" - case deletePeer = "Delete peer" + enum PeerField { + case publicKey + case preSharedKey + case endpoint + case persistentKeepAlive + case allowedIPs + case excludePrivateIPs + 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 = [ @@ -103,15 +128,15 @@ class TunnelViewModel { fieldsWithError.removeAll() guard let name = scratchpad[.name]?.trimmingCharacters(in: .whitespacesAndNewlines), (!name.isEmpty) else { fieldsWithError.insert(.name) - return .error("Interface name is required") + return .error(tr("alertInvalidInterfaceMessageNameRequired")) } guard let privateKeyString = scratchpad[.privateKey] else { 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 { 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 errorMessages = [String]() @@ -122,7 +147,7 @@ class TunnelViewModel { addresses.append(address) } else { 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 @@ -132,7 +157,7 @@ class TunnelViewModel { config.listenPort = listenPort } else { 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] { @@ -140,7 +165,7 @@ class TunnelViewModel { config.mtu = mtu } else { 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] { @@ -150,7 +175,7 @@ class TunnelViewModel { dnsServers.append(dnsServer) } else { 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 @@ -243,11 +268,11 @@ class TunnelViewModel { fieldsWithError.removeAll() guard let publicKeyString = scratchpad[.publicKey] else { 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 { 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 errorMessages = [String]() @@ -256,7 +281,7 @@ class TunnelViewModel { config.preSharedKey = preSharedKey } else { 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] { @@ -266,7 +291,7 @@ class TunnelViewModel { allowedIPs.append(allowedIP) } else { 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 @@ -276,7 +301,7 @@ class TunnelViewModel { config.endpoint = endpoint } else { 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] { @@ -284,7 +309,7 @@ class TunnelViewModel { config.persistentKeepAlive = persistentKeepAlive } else { 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 { case saved(Configuration) - case error(String) // TODO: Localize error messages + case error(String) } var interfaceData: InterfaceData @@ -425,7 +450,7 @@ class TunnelViewModel { let peerPublicKeysArray = peerConfigurations.map { $0.publicKey } let peerPublicKeysSet = Set(peerPublicKeysArray) 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) @@ -440,13 +465,13 @@ extension TunnelViewModel { static func activateOnDemandOptionText(for activateOnDemandOption: ActivateOnDemandOption) -> String { switch activateOnDemandOption { case .none: - return "Off" + return tr("tunnelOnDemandOptionOff") case .useOnDemandOverWiFiOrCellular: - return "Wi-Fi or cellular" + return tr("tunnelOnDemandOptionWiFiOrCellular") case .useOnDemandOverWiFiOnly: - return "Wi-Fi only" + return tr("tunnelOnDemandOptionWiFiOnly") case .useOnDemandOverCellularOnly: - return "Cellular only" + return tr("tunnelOnDemandOptionCellularOnly") } } diff --git a/WireGuard/WireGuard/UI/iOS/ViewController/QRScanViewController.swift b/WireGuard/WireGuard/UI/iOS/ViewController/QRScanViewController.swift index e4b6287..1fd6905 100644 --- a/WireGuard/WireGuard/UI/iOS/ViewController/QRScanViewController.swift +++ b/WireGuard/WireGuard/UI/iOS/ViewController/QRScanViewController.swift @@ -17,11 +17,11 @@ class QRScanViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - title = "Scan QR code" + title = tr("scanQRCodeViewTitle") navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelTapped)) let tipLabel = UILabel() - tipLabel.text = "Tip: Generate with `qrencode -t ansiutf8 < tunnel.conf`" + tipLabel.text = tr("scanQRCodeTipText") tipLabel.adjustsFontSizeToFitWidth = true tipLabel.textColor = .lightGray tipLabel.textAlignment = .center @@ -39,7 +39,7 @@ class QRScanViewController: UIViewController { let captureSession = captureSession, captureSession.canAddInput(videoInput), 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 } @@ -103,16 +103,16 @@ class QRScanViewController: UIViewController { func scanDidComplete(withCode code: String) { let scannedTunnelConfiguration = try? WgQuickConfigFileParser.parse(code, name: "Scanned") 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 } - 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.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) }) - 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 } tunnelConfiguration.interface.name = title if let self = self { @@ -126,7 +126,7 @@ class QRScanViewController: UIViewController { func scanDidEncounterError(title: String, message: String) { 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) }) present(alertController, animated: true) @@ -145,7 +145,7 @@ extension QRScanViewController: AVCaptureMetadataOutputObjectsDelegate { guard let metadataObject = metadataObjects.first, let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject, 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 } diff --git a/WireGuard/WireGuard/UI/iOS/ViewController/SettingsTableViewController.swift b/WireGuard/WireGuard/UI/iOS/ViewController/SettingsTableViewController.swift index 65ad2fe..22edcbc 100644 --- a/WireGuard/WireGuard/UI/iOS/ViewController/SettingsTableViewController.swift +++ b/WireGuard/WireGuard/UI/iOS/ViewController/SettingsTableViewController.swift @@ -6,11 +6,20 @@ import os.log class SettingsTableViewController: UITableViewController { - enum SettingsFields: String { - case iosAppVersion = "WireGuard for iOS" - case goBackendVersion = "WireGuard Go Backend" - case exportZipArchive = "Export zip archive" - case exportLogFile = "Export log file" + enum SettingsFields { + case iosAppVersion + case goBackendVersion + case exportZipArchive + 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]] = [ @@ -33,7 +42,7 @@ class SettingsTableViewController: UITableViewController { override func viewDidLoad() { super.viewDidLoad() - title = "Settings" + title = tr("settingsViewTitle") navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneTapped)) tableView.estimatedRowHeight = 44 @@ -109,19 +118,19 @@ class SettingsTableViewController: UITableViewController { if FileManager.default.fileExists(atPath: destinationURL.path) { let isDeleted = FileManager.deleteFile(at: destinationURL) 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 } } 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 } let isWritten = Logger.global?.writeLog(called: "APP", mergedWith: networkExtensionLogFilePath, called: "NET", to: destinationURL.path) ?? false 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 } @@ -153,11 +162,11 @@ extension SettingsTableViewController { override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { switch section { case 0: - return "About" + return tr("settingsSectionTitleAbout") case 1: - return "Export configurations" + return tr("settingsSectionTitleExportConfigurations") case 2: - return "Tunnel log" + return tr("settingsSectionTitleTunnelLog") default: return nil } @@ -168,7 +177,7 @@ extension SettingsTableViewController { if field == .iosAppVersion || field == .goBackendVersion { let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath) cell.copyableGesture = false - cell.key = field.rawValue + cell.key = field.localizedUIString if field == .iosAppVersion { var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown version" if let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String { @@ -181,7 +190,7 @@ extension SettingsTableViewController { return cell } else if field == .exportZipArchive { let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath) - cell.buttonText = field.rawValue + cell.buttonText = field.localizedUIString cell.onTapped = { [weak self] in self?.exportConfigurationsAsZipFile(sourceView: cell.button) } @@ -189,7 +198,7 @@ extension SettingsTableViewController { } else { assert(field == .exportLogFile) let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath) - cell.buttonText = field.rawValue + cell.buttonText = field.localizedUIString cell.onTapped = { [weak self] in self?.exportLogForLastActivatedTunnel(sourceView: cell.button) } diff --git a/WireGuard/WireGuard/UI/iOS/ViewController/TunnelDetailTableViewController.swift b/WireGuard/WireGuard/UI/iOS/ViewController/TunnelDetailTableViewController.swift index d3f9c84..50c0e33 100644 --- a/WireGuard/WireGuard/UI/iOS/ViewController/TunnelDetailTableViewController.swift +++ b/WireGuard/WireGuard/UI/iOS/ViewController/TunnelDetailTableViewController.swift @@ -85,7 +85,7 @@ class TunnelDetailTableViewController: UITableViewController { let destroyAction = UIAlertAction(title: buttonTitle, style: .destructive) { _ in onConfirmed() } - let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) + let cancelAction = UIAlertAction(title: tr("actionCancel"), style: .cancel) let alert = UIAlertController(title: "", message: message, preferredStyle: .actionSheet) alert.addAction(destroyAction) alert.addAction(cancelAction) @@ -137,13 +137,13 @@ extension TunnelDetailTableViewController { override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { switch sections[section] { case .status: - return "Status" + return tr("tunnelSectionTitleStatus") case .interface: - return "Interface" + return tr("tunnelSectionTitleInterface") case .peer: - return "Peer" + return tr("tunnelSectionTitlePeer") case .onDemand: - return "On-Demand Activation" + return tr("tunnelSectionTitleOnDemand") case .delete: return nil } @@ -171,19 +171,19 @@ extension TunnelDetailTableViewController { let text: String switch status { case .inactive: - text = "Inactive" + text = tr("tunnelStatusInactive") case .activating: - text = "Activating" + text = tr("tunnelStatusActivating") case .active: - text = "Active" + text = tr("tunnelStatusActive") case .deactivating: - text = "Deactivating" + text = tr("tunnelStatusDeactivating") case .reasserting: - text = "Reactivating" + text = tr("tunnelStatusReasserting") case .restarting: - text = "Restarting" + text = tr("tunnelStatusRestarting") case .waiting: - text = "Waiting" + text = tr("tunnelStatusWaiting") } cell.textLabel?.text = text 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 { let field = tunnelViewModel.interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFields)[indexPath.row] let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath) - cell.key = field.rawValue + cell.key = field.localizedUIString cell.value = tunnelViewModel.interfaceData[field] return cell } @@ -221,14 +221,14 @@ extension TunnelDetailTableViewController { private func peerCell(for tableView: UITableView, at indexPath: IndexPath, with peerData: TunnelViewModel.PeerData) -> UITableViewCell { let field = peerData.filterFieldsWithValueOrControl(peerFields: peerFields)[indexPath.row] let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath) - cell.key = field.rawValue + cell.key = field.localizedUIString cell.value = peerData[field] return cell } private func onDemandCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell { let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath) - cell.key = "Activate on demand" + cell.key = tr("tunnelOnDemandKey") cell.value = TunnelViewModel.activateOnDemandDetailText(for: tunnel.activateOnDemandSetting()) onDemandStatusObservationToken = tunnel.observe(\.isActivateOnDemandEnabled) { [weak cell] tunnel, _ in cell?.value = TunnelViewModel.activateOnDemandDetailText(for: tunnel.activateOnDemandSetting()) @@ -238,11 +238,11 @@ extension TunnelDetailTableViewController { private func deleteConfigurationCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell { let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath) - cell.buttonText = "Delete tunnel" + cell.buttonText = tr("deleteTunnelButtonTitle") cell.hasDestructiveAction = true cell.onTapped = { [weak self] in 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 } self.tunnelsManager.remove(tunnel: self.tunnel) { error in if error != nil { diff --git a/WireGuard/WireGuard/UI/iOS/ViewController/TunnelEditTableViewController.swift b/WireGuard/WireGuard/UI/iOS/ViewController/TunnelEditTableViewController.swift index 4aa1180..3d9724c 100644 --- a/WireGuard/WireGuard/UI/iOS/ViewController/TunnelEditTableViewController.swift +++ b/WireGuard/WireGuard/UI/iOS/ViewController/TunnelEditTableViewController.swift @@ -71,7 +71,7 @@ class TunnelEditTableViewController: UITableViewController { override func 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.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelTapped)) @@ -98,8 +98,9 @@ class TunnelEditTableViewController: UITableViewController { let tunnelSaveResult = tunnelViewModel.save() switch tunnelSaveResult { case .error(let errorMessage): - let erroringConfiguration = (tunnelViewModel.interfaceData.validatedConfiguration == nil) ? "Interface" : "Peer" - ErrorPresenter.showErrorAlert(title: "Invalid \(erroringConfiguration)", message: errorMessage, from: self) + let alertTitle = (tunnelViewModel.interfaceData.validatedConfiguration == nil) ? + tr("alertInvalidInterfaceTitle") : tr("alertInvalidPeerTitle") + ErrorPresenter.showErrorAlert(title: alertTitle, message: errorMessage, from: self) tableView.reloadData() // Highlight erroring fields case .saved(let tunnelConfiguration): if let tunnel = tunnel { @@ -164,13 +165,13 @@ extension TunnelEditTableViewController { override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { switch sections[section] { case .interface: - return section == 0 ? "Interface" : nil + return section == 0 ? tr("tunnelSectionTitleInterface") : nil case .peer: - return "Peer" + return tr("tunnelSectionTitlePeer") case .addPeer: return nil 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 { let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath) - cell.buttonText = field.rawValue + cell.buttonText = field.localizedUIString cell.onTapped = { [weak self] in 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 { let cell: TunnelEditKeyValueCell = tableView.dequeueReusableCell(for: indexPath) - cell.key = field.rawValue + cell.key = field.localizedUIString cell.value = tunnelViewModel.interfaceData[field] return cell } private func interfaceFieldKeyValueCell(for tableView: UITableView, at indexPath: IndexPath, with field: TunnelViewModel.InterfaceField) -> UITableViewCell { let cell: TunnelEditEditableKeyValueCell = tableView.dequeueReusableCell(for: indexPath) - cell.key = field.rawValue + cell.key = field.localizedUIString switch field { case .name, .privateKey: - cell.placeholderText = "Required" + cell.placeholderText = tr("tunnelEditPlaceholderTextRequired") cell.keyboardType = .default case .addresses, .dns: - cell.placeholderText = "Optional" + cell.placeholderText = tr("tunnelEditPlaceholderTextOptional") cell.keyboardType = .numbersAndPunctuation case .listenPort, .mtu: - cell.placeholderText = "Automatic" + cell.placeholderText = tr("tunnelEditPlaceholderTextAutomatic") cell.keyboardType = .numberPad case .publicKey, .generateKeyPair: 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 { let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath) - cell.buttonText = field.rawValue + cell.buttonText = field.localizedUIString cell.hasDestructiveAction = true cell.onTapped = { [weak self, weak peerData] in guard let peerData = peerData 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 } let removedSectionIndices = self.deletePeer(peer: peerData) 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 { let cell: SwitchCell = tableView.dequeueReusableCell(for: indexPath) - cell.message = field.rawValue + cell.message = field.localizedUIString cell.isEnabled = peerData.shouldAllowExcludePrivateIPsControl cell.isOn = peerData.excludePrivateIPsValue 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 { let cell: TunnelEditEditableKeyValueCell = tableView.dequeueReusableCell(for: indexPath) - cell.key = field.rawValue + cell.key = field.localizedUIString switch field { case .publicKey: - cell.placeholderText = "Required" + cell.placeholderText = tr("tunnelEditPlaceholderTextRequired") cell.keyboardType = .default case .preSharedKey, .endpoint: - cell.placeholderText = "Optional" + cell.placeholderText = tr("tunnelEditPlaceholderTextOptional") cell.keyboardType = .default case .allowedIPs: - cell.placeholderText = "Optional" + cell.placeholderText = tr("tunnelEditPlaceholderTextOptional") cell.keyboardType = .numbersAndPunctuation case .persistentKeepAlive: - cell.placeholderText = "Off" + cell.placeholderText = tr("tunnelEditPlaceholderTextOff") cell.keyboardType = .numberPad case .excludePrivateIPs, .deletePeer: cell.keyboardType = .default @@ -373,7 +374,7 @@ extension TunnelEditTableViewController { private func addPeerCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell { let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath) - cell.buttonText = "Add peer" + cell.buttonText = tr("addPeerButtonTitle") cell.onTapped = { [weak self] in guard let self = self else { return } 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 { if indexPath.row == 0 { let cell: SwitchCell = tableView.dequeueReusableCell(for: indexPath) - cell.message = "Activate on demand" + cell.message = tr("tunnelOnDemandKey") cell.isOn = activateOnDemandSetting.isActivateOnDemandEnabled cell.onSwitchToggled = { [weak self] isOn in guard let self = self else { return } @@ -443,7 +444,7 @@ extension TunnelEditTableViewController { let destroyAction = UIAlertAction(title: buttonTitle, style: .destructive) { _ in onConfirmed() } - let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) + let cancelAction = UIAlertAction(title: tr("actionCancel"), style: .cancel) let alert = UIAlertController(title: "", message: message, preferredStyle: .actionSheet) alert.addAction(destroyAction) alert.addAction(cancelAction) diff --git a/WireGuard/WireGuard/UI/iOS/ViewController/TunnelsListTableViewController.swift b/WireGuard/WireGuard/UI/iOS/ViewController/TunnelsListTableViewController.swift index 9dea8b0..fff976f 100644 --- a/WireGuard/WireGuard/UI/iOS/ViewController/TunnelsListTableViewController.swift +++ b/WireGuard/WireGuard/UI/iOS/ViewController/TunnelsListTableViewController.swift @@ -20,7 +20,7 @@ class TunnelsListTableViewController: UIViewController { let centeredAddButton: BorderedTextButton = { let button = BorderedTextButton() - button.title = "Add a tunnel" + button.title = tr("tunnelsListCenteredAddTunnelButtonTitle") button.isHidden = true return button }() @@ -72,9 +72,9 @@ class TunnelsListTableViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - title = "WireGuard" + title = tr("tunnelsListTitle") 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" } @@ -97,25 +97,25 @@ class TunnelsListTableViewController: UIViewController { @objc func addButtonTapped(sender: AnyObject) { guard tunnelsManager != nil else { return } - let alert = UIAlertController(title: "", message: "Add a new WireGuard tunnel", preferredStyle: .actionSheet) - let importFileAction = UIAlertAction(title: "Create from file or archive", style: .default) { [weak self] _ in + let alert = UIAlertController(title: "", message: tr("addTunnelMenuHeader"), preferredStyle: .actionSheet) + let importFileAction = UIAlertAction(title: tr("addTunnelMenuImportFile"), style: .default) { [weak self] _ in self?.presentViewControllerForFileImport() } 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() } 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 { self.presentViewControllerForTunnelCreation(tunnelsManager: tunnelsManager) } } alert.addAction(createFromScratchAction) - let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) + let cancelAction = UIAlertAction(title: tr("actionCancel"), style: .cancel) alert.addAction(cancelAction) if let sender = sender as? UIBarButtonItem { @@ -172,9 +172,9 @@ class TunnelsListTableViewController: UIViewController { completionHandler?() return } - ErrorPresenter.showErrorAlert(title: "Created \(numberSuccessful) tunnels", - message: "Created \(numberSuccessful) of \(configs.count) tunnels from zip archive", - from: self, onPresented: completionHandler) + let title = tr(format: "alertImportedFromZipTitle (%d)", numberSuccessful) + let message = tr(format: "alertImportedFromZipMessage (%1$d of %2$d)", numberSuccessful, configs.count) + ErrorPresenter.showErrorAlert(title: title, message: message, from: self, onPresented: completionHandler) } } } else /* if (url.pathExtension == "conf") -- we assume everything else is a conf */ { @@ -189,8 +189,7 @@ class TunnelsListTableViewController: UIViewController { } } } else { - ErrorPresenter.showErrorAlert(title: "Unable to import tunnel", - message: "An error occured when importing the tunnel configuration.", + ErrorPresenter.showErrorAlert(title: tr("alertUnableToImportTitle"), message: tr("alertUnableToImportMessage"), from: self, onPresented: completionHandler) } } @@ -266,7 +265,7 @@ extension TunnelsListTableViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, 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 } let tunnel = tunnelsManager.tunnel(at: indexPath.row) tunnelsManager.remove(tunnel: tunnel) { error in diff --git a/WireGuard/WireGuard/ZipArchive/ZipArchive.swift b/WireGuard/WireGuard/ZipArchive/ZipArchive.swift index 66c99f5..df0ec73 100644 --- a/WireGuard/WireGuard/ZipArchive/ZipArchive.swift +++ b/WireGuard/WireGuard/ZipArchive/ZipArchive.swift @@ -11,11 +11,11 @@ enum ZipArchiveError: WireGuardAppError { var alertText: AlertText { switch self { case .cantOpenInputZipFile: - return ("Unable to read zip archive", "The zip archive could not be read.") + return (tr("alertCantOpenInputZipFileTitle"), tr("alertCantOpenInputZipFileMessage")) case .cantOpenOutputZipFileForWriting: - return ("Unable to create zip archive", "Could not open zip file for writing.") + return (tr("alertCantOpenOutputZipFileForWritingTitle"), tr("alertCantOpenOutputZipFileForWritingMessage")) case .badArchive: - return ("Unable to read zip archive", "Bad or corrupt zip archive.") + return (tr("alertBadArchiveTitle"), tr("alertBadArchiveMessage")) } } } diff --git a/WireGuard/WireGuard/ZipArchive/ZipExporter.swift b/WireGuard/WireGuard/ZipArchive/ZipExporter.swift index 4c5fde8..33d62fd 100644 --- a/WireGuard/WireGuard/ZipArchive/ZipExporter.swift +++ b/WireGuard/WireGuard/ZipArchive/ZipExporter.swift @@ -7,7 +7,7 @@ enum ZipExporterError: WireGuardAppError { case noTunnelsToExport var alertText: AlertText { - return ("Nothing to export", "There are no tunnels to export") + return (tr("alertNoTunnelsToExportTitle"), tr("alertNoTunnelsToExportMessage")) } } diff --git a/WireGuard/WireGuard/ZipArchive/ZipImporter.swift b/WireGuard/WireGuard/ZipArchive/ZipImporter.swift index e87633c..0178ca0 100644 --- a/WireGuard/WireGuard/ZipArchive/ZipImporter.swift +++ b/WireGuard/WireGuard/ZipArchive/ZipImporter.swift @@ -7,7 +7,7 @@ enum ZipImporterError: WireGuardAppError { case noTunnelsInZipArchive var alertText: AlertText { - return ("No tunnels in zip archive", "No .conf tunnel files were found inside the zip archive.") + return (tr("alertNoTunnelsInImportedZipArchiveTitle"), tr("alertNoTunnelsInImportedZipArchiveMessage")) } }