From 98e5e4cddeea9d7271639d9c021193f71515dadc Mon Sep 17 00:00:00 2001 From: Davide De Rosa Date: Wed, 5 Jul 2023 16:18:33 +0100 Subject: [PATCH] Share common protocols across localized strings (#324) --- Passepartout.xcodeproj/project.pbxproj | 4 - .../App/Domain/IntentDispatcher.swift | 2 +- .../PassepartoutProviders+Extensions.swift | 2 +- Passepartout/App/L10n/Core+L10n.swift | 62 +++++- Passepartout/App/L10n/OpenVPN+L10n.swift | 191 ++++++++++++------ Passepartout/App/L10n/Providers+L10n.swift | 137 ++++++++++--- Passepartout/App/L10n/TunnelKit+L10n.swift | 72 ++++--- Passepartout/App/L10n/WireGuard+L10n.swift | 28 +++ .../DefaultLightProviderManager.swift | 6 +- Passepartout/App/Views/AccountView.swift | 17 +- .../Views/EndpointAdvancedView+OpenVPN.swift | 87 ++++---- .../EndpointAdvancedView+WireGuard.swift | 2 +- .../App/Views/EndpointView+WireGuard.swift | 4 +- .../App/Views/NetworkSettingsView.swift | 2 +- .../App/Views/ProfileView+Provider.swift | 8 +- .../App/Views/ProviderLocationView.swift | 14 +- Passepartout/App/Views/VPNStatusText.swift | 4 +- .../Reusable/LocalizableEntity.swift | 42 ++++ .../Reusable/Validators.swift | 18 +- 19 files changed, 487 insertions(+), 215 deletions(-) create mode 100644 PassepartoutLibrary/Sources/PassepartoutCore/Reusable/LocalizableEntity.swift rename {Passepartout/App => PassepartoutLibrary/Sources/PassepartoutCore}/Reusable/Validators.swift (83%) diff --git a/Passepartout.xcodeproj/project.pbxproj b/Passepartout.xcodeproj/project.pbxproj index 1b17101c..3e4035d3 100644 --- a/Passepartout.xcodeproj/project.pbxproj +++ b/Passepartout.xcodeproj/project.pbxproj @@ -29,7 +29,6 @@ 0E0F4C5C29C76B790022E884 /* SceneDelegate+Shortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0F4C5B29C76B790022E884 /* SceneDelegate+Shortcuts.swift */; }; 0E0F4C6429C84B5A0022E884 /* LockableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0F4C6329C84B5A0022E884 /* LockableView.swift */; }; 0E0F4C6629C84CF60022E884 /* LogoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0F4C6529C84CF60022E884 /* LogoView.swift */; }; - 0E12BC8F27F62C8600B2F912 /* Validators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E12BC8E27F62C8500B2F912 /* Validators.swift */; }; 0E1AD5CC2A2682DA002AE6E6 /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E1AD5CB2A2682DA002AE6E6 /* AppError.swift */; }; 0E1AD5CE2A268645002AE6E6 /* Errors+L10n.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E1AD5CD2A268645002AE6E6 /* Errors+L10n.swift */; }; 0E1B5F5C29C506AD00FE7D18 /* ProfileView+Diagnostics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E1B5F5B29C506AC00FE7D18 /* ProfileView+Diagnostics.swift */; }; @@ -317,7 +316,6 @@ 0E0F4C5B29C76B790022E884 /* SceneDelegate+Shortcuts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SceneDelegate+Shortcuts.swift"; sourceTree = ""; }; 0E0F4C6329C84B5A0022E884 /* LockableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockableView.swift; sourceTree = ""; }; 0E0F4C6529C84CF60022E884 /* LogoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogoView.swift; sourceTree = ""; }; - 0E12BC8E27F62C8500B2F912 /* Validators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Validators.swift; sourceTree = ""; }; 0E1AD5CB2A2682DA002AE6E6 /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = ""; }; 0E1AD5CD2A268645002AE6E6 /* Errors+L10n.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Errors+L10n.swift"; sourceTree = ""; }; 0E1B5F5B29C506AC00FE7D18 /* ProfileView+Diagnostics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileView+Diagnostics.swift"; sourceTree = ""; }; @@ -612,7 +610,6 @@ 0E2C172A27CB63F9007E8488 /* Reviewer.swift */, 0ED89C1427DE0A0C008B36D6 /* Shortcut.swift */, 0E5349BD27C16A4500C71BB3 /* StyledPicker.swift */, - 0E12BC8E27F62C8500B2F912 /* Validators.swift */, ); path = Reusable; sourceTree = ""; @@ -1509,7 +1506,6 @@ 0E2A8D4927ADF87F00207D04 /* PassepartoutApp.swift in Sources */, 0EBC075527EBC83800208AD9 /* MailComposerView.swift in Sources */, 0EF0FAF727DD159C007EB181 /* IntentDispatcher.swift in Sources */, - 0E12BC8F27F62C8600B2F912 /* Validators.swift in Sources */, 0E0838FD2877334300A34EC0 /* DefaultLightProviderManager.swift in Sources */, 0E0F4C6629C84CF60022E884 /* LogoView.swift in Sources */, 0E039279281890B100827C10 /* AddHostView.swift in Sources */, diff --git a/Passepartout/App/Domain/IntentDispatcher.swift b/Passepartout/App/Domain/IntentDispatcher.swift index cfce6bc2..7a819cd6 100644 --- a/Passepartout/App/Domain/IntentDispatcher.swift +++ b/Passepartout/App/Domain/IntentDispatcher.swift @@ -48,7 +48,7 @@ final class IntentDispatcher { intent.profileId = header.id.uuidString intent.providerFullName = providerFullName intent.serverId = server.id - intent.serverName = server.localizedLongDescription(withCategory: false) + intent.serverName = server.localizedDescription(style: .longWithCategory(withCategory: false)) return intent } diff --git a/Passepartout/App/Extensions/PassepartoutProviders+Extensions.swift b/Passepartout/App/Extensions/PassepartoutProviders+Extensions.swift index bede367e..6cd86c89 100644 --- a/Passepartout/App/Extensions/PassepartoutProviders+Extensions.swift +++ b/Passepartout/App/Extensions/PassepartoutProviders+Extensions.swift @@ -60,7 +60,7 @@ extension ProviderLocation: Comparable { } public static func < (lhs: Self, rhs: Self) -> Bool { - lhs.localizedCountry < rhs.localizedCountry + lhs.localizedDescription(style: .country) < rhs.localizedDescription(style: .country) } } diff --git a/Passepartout/App/L10n/Core+L10n.swift b/Passepartout/App/L10n/Core+L10n.swift index b6aca679..9e8c29da 100644 --- a/Passepartout/App/L10n/Core+L10n.swift +++ b/Passepartout/App/L10n/Core+L10n.swift @@ -26,8 +26,19 @@ import Foundation import PassepartoutLibrary -extension ObservableVPNState { - func localizedStatusDescription(isActiveProfile: Bool, withErrors: Bool, dataCountIfAvailable: Bool) -> String { +extension ObservableVPNState: StyledLocalizableEntity { + public enum Style { + case status(isActiveProfile: Bool, withErrors: Bool, dataCountIfAvailable: Bool) + } + + public func localizedDescription(style: Style) -> String { + switch style { + case .status(let isActiveProfile, let withErrors, let dataCountIfAvailable): + return statusDescription(isActiveProfile: isActiveProfile, withErrors: withErrors, dataCountIfAvailable: dataCountIfAvailable) + } + } + + private func statusDescription(isActiveProfile: Bool, withErrors: Bool, dataCountIfAvailable: Bool) -> String { guard isActiveProfile && isEnabled else { return L10n.Tunnelkit.Vpn.disabled } @@ -65,8 +76,8 @@ extension Profile.WireGuardSettings { } } -extension Network.Choice { - var localizedDescription: String { +extension Network.Choice: LocalizableEntity { + public var localizedDescription: String { switch self { case .automatic: return L10n.Global.Strings.automatic @@ -77,8 +88,8 @@ extension Network.Choice { } } -extension Network.DNSSettings.ConfigurationType { - var localizedDescription: String { +extension Network.DNSSettings.ConfigurationType: LocalizableEntity { + public var localizedDescription: String { switch self { case .plain: return Unlocalized.DNS.plain @@ -95,8 +106,8 @@ extension Network.DNSSettings.ConfigurationType { } } -extension Network.ProxySettings.ConfigurationType { - var localizedDescription: String { +extension Network.ProxySettings.ConfigurationType: LocalizableEntity { + public var localizedDescription: String { switch self { case .manual: return L10n.Global.Strings.manual @@ -109,3 +120,38 @@ extension Network.ProxySettings.ConfigurationType { } } } + +extension Profile.Account.AuthenticationMethod: LocalizableEntity { + public var localizedDescription: String { + switch self { + case .persistent: + return L10n.Account.Items.AuthenticationMethod.persistent + + case .interactive: + return L10n.Account.Items.AuthenticationMethod.interactive + + case .totp: + return Unlocalized.Other.totp + } + } +} + +extension Int: StyledLocalizableEntity { + public enum Style { + case mtu + } + + public func localizedDescription(style: Style) -> String { + switch style { + case .mtu: + return mtuDescription + } + } + + private var mtuDescription: String { + guard self != 0 else { + return L10n.Global.Strings.default + } + return description + } +} diff --git a/Passepartout/App/L10n/OpenVPN+L10n.swift b/Passepartout/App/L10n/OpenVPN+L10n.swift index a48e407f..51e1e992 100644 --- a/Passepartout/App/L10n/OpenVPN+L10n.swift +++ b/Passepartout/App/L10n/OpenVPN+L10n.swift @@ -27,20 +27,20 @@ import Foundation import PassepartoutLibrary import TunnelKitOpenVPN -extension OpenVPN.Cipher { - var localizedDescription: String { +extension OpenVPN.Cipher: LocalizableEntity { + public var localizedDescription: String { description } } -extension OpenVPN.Digest { - var localizedDescription: String { +extension OpenVPN.Digest: LocalizableEntity { + public var localizedDescription: String { description } } -extension OpenVPN.CompressionFraming { - var localizedDescription: String { +extension OpenVPN.CompressionFraming: LocalizableEntity { + public var localizedDescription: String { switch self { case .disabled: return L10n.Global.Strings.disabled @@ -54,8 +54,8 @@ extension OpenVPN.CompressionFraming { } } -extension OpenVPN.CompressionAlgorithm { - var localizedDescription: String { +extension OpenVPN.CompressionAlgorithm: LocalizableEntity { + public var localizedDescription: String { let V = L10n.Endpoint.Advanced.Openvpn.Items.self switch self { case .disabled: @@ -70,24 +70,24 @@ extension OpenVPN.CompressionAlgorithm { } } -extension Optional where Wrapped == OpenVPN.TLSWrap { - var localizedDescription: String { - guard let strategy = self?.strategy else { - return L10n.Global.Strings.disabled - } - let V = L10n.Endpoint.Advanced.Openvpn.Items.self - switch strategy { - case .auth: - return V.TlsWrapping.Value.auth +extension OpenVPN.XORMethod: StyledLocalizableEntity { + public enum Style { + case short - case .crypt: - return V.TlsWrapping.Value.crypt + case long + } + + public func localizedDescription(style: Style) -> String { + switch style { + case .short: + return shortDescription + + case .long: + return longDescription } } -} -extension OpenVPN.XORMethod { - var localizedDescription: String { + private var shortDescription: String { switch self { case .xormask: return Unlocalized.OpenVPN.XOR.xormask.rawValue @@ -103,52 +103,22 @@ extension OpenVPN.XORMethod { } } - var localizedLongDescription: String { + private var longDescription: String { switch self { case .xormask(let mask): - return "\(localizedDescription) \(mask.toHex())" + return "\(shortDescription) \(mask.toHex())" case .obfuscate(let mask): - return "\(localizedDescription) \(mask.toHex())" + return "\(shortDescription) \(mask.toHex())" default: - return localizedDescription + return shortDescription } } } -extension Optional where Wrapped == Bool { - var localizedDescriptionAsEKU: String { - let V = L10n.Global.Strings.self - return (self ?? false) ? V.enabled : V.disabled - } -} - -extension TimeInterval { - var localizedDescriptionAsRenegotiatesAfter: String { - let V = L10n.Endpoint.Advanced.Openvpn.Items.self - if self > 0 { - return V.RenegotiationSeconds.Value.after(TimeInterval(self).localizedDescription) - } else { - return L10n.Global.Strings.disabled - } - } -} - -extension Bool { - var localizedDescriptionAsRandomizeEndpoint: String { - let V = L10n.Global.Strings.self - return self ? V.enabled : V.disabled - } - - var localizedDescriptionAsRandomizeHostnames: String { - let V = L10n.Global.Strings.self - return self ? V.enabled : V.disabled - } -} - -extension OpenVPN.PullMask { - var localizedDescription: String { +extension OpenVPN.PullMask: LocalizableEntity { + public var localizedDescription: String { switch self { case .routes: return L10n.Endpoint.Advanced.Openvpn.Items.Route.caption @@ -162,6 +132,111 @@ extension OpenVPN.PullMask { } } +extension OpenVPN.ConfigurationBuilder: StyledLocalizableEntity { + public enum Style { + case tlsWrap + + case eku + } + + public func localizedDescription(style: Style) -> String { + switch style { + case .tlsWrap: + return tlsWrap.tlsWrapDescription + + case .eku: + return checksEKU.ekuDescription + } + } +} + +extension OpenVPN.Configuration: StyledOptionalLocalizableEntity { + public enum OptionalStyle { + case keepAlive + + case renegotiatesAfter + + case randomizeEndpoint + + case randomizeHostnames + } + + public func localizedDescription(optionalStyle: OptionalStyle) -> String? { + switch optionalStyle { + case .keepAlive: + return keepAliveInterval?.keepAliveDescription + + case .renegotiatesAfter: + return renegotiatesAfter?.renegotiatesAfterDescription + + case .randomizeEndpoint: + return randomizeEndpoint?.randomizeEndpointDescription + + case .randomizeHostnames: + return randomizeHostnames?.randomizeHostnamesDescription + } + } +} + +private extension Optional where Wrapped == OpenVPN.TLSWrap { + var tlsWrapDescription: String { + guard let strategy = self?.strategy else { + return L10n.Global.Strings.disabled + } + let V = L10n.Endpoint.Advanced.Openvpn.Items.self + switch strategy { + case .auth: + return V.TlsWrapping.Value.auth + + case .crypt: + return V.TlsWrapping.Value.crypt + } + } +} + +private extension TimeInterval { + var keepAliveDescription: String { + let V = L10n.Endpoint.Advanced.Openvpn.Items.self + if self > 0 { + return V.KeepAlive.Value.seconds(Int(self)) + } else { + return L10n.Global.Strings.disabled + } + } +} + +private extension Optional where Wrapped == Bool { + var ekuDescription: String { + let V = L10n.Global.Strings.self + return (self ?? false) ? V.enabled : V.disabled + } +} + +private extension TimeInterval { + var renegotiatesAfterDescription: String { + let V = L10n.Endpoint.Advanced.Openvpn.Items.self + if self > 0 { + return V.RenegotiationSeconds.Value.after(TimeInterval(self).localizedDescription) + } else { + return L10n.Global.Strings.disabled + } + } +} + +private extension Bool { + var randomizeEndpointDescription: String { + let V = L10n.Global.Strings.self + return self ? V.enabled : V.disabled + } + + var randomizeHostnamesDescription: String { + let V = L10n.Global.Strings.self + return self ? V.enabled : V.disabled + } +} + +// MARK: - Errors + extension TunnelKitOpenVPNError: LocalizedError { public var errorDescription: String? { let V = L10n.Tunnelkit.Errors.Vpn.self diff --git a/Passepartout/App/L10n/Providers+L10n.swift b/Passepartout/App/L10n/Providers+L10n.swift index 06e975f0..06d86d4e 100644 --- a/Passepartout/App/L10n/Providers+L10n.swift +++ b/Passepartout/App/L10n/Providers+L10n.swift @@ -26,15 +26,31 @@ import Foundation import PassepartoutLibrary -extension ProviderManager { - func localizedPreset(forProfile profile: Profile) -> String? { +extension ProviderManager: StyledOptionalLocalizableEntity { + public enum OptionalStyle { + case preset(profile: Profile) + + case infrastructureUpdate(profile: Profile) + } + + public func localizedDescription(optionalStyle: OptionalStyle) -> String? { + switch optionalStyle { + case .preset(let profile): + return presetDescription(forProfile: profile) + + case .infrastructureUpdate(let profile): + return infrastructureUpdateDescription(forProfile: profile) + } + } + + private func presetDescription(forProfile profile: Profile) -> String? { guard let server = profile.providerServer(self) else { return nil } return profile.providerPreset(server)?.localizedDescription } - func localizedInfrastructureUpdate(forProfile profile: Profile) -> String? { + private func infrastructureUpdateDescription(forProfile profile: Profile) -> String? { guard let providerName = profile.header.providerName else { return nil } @@ -42,8 +58,19 @@ extension ProviderManager { } } -extension ProviderMetadata { - var localizedGuidanceString: String? { +extension ProviderMetadata: StyledOptionalLocalizableEntity { + public enum OptionalStyle { + case guidance + } + + public func localizedDescription(optionalStyle: OptionalStyle) -> String? { + switch optionalStyle { + case .guidance: + return guidanceString + } + } + + private var guidanceString: String? { let prefix = "account.sections.guidance.footer.infrastructure" let key = "\(prefix).\(name)" var format = NSLocalizedString(key, bundle: .main, comment: "") @@ -59,26 +86,92 @@ extension ProviderMetadata { } } -extension ProviderLocation { - var localizedCountry: String { +extension ProviderLocation: StyledLocalizableEntity { + public enum Style { + case country + } + + public func localizedDescription(style: Style) -> String { + switch style { + case .country: + return countryDescription + } + } + + private var countryDescription: String { countryCode.localizedAsCountryCode } } -extension ProviderServer { - var localizedCountry: String { +extension ProviderServer: StyledLocalizableEntity { + public enum Style { + case country + + case countryWithCategory(withCategory: Bool) + + case shortWithDefault + + case longWithCategory(withCategory: Bool) + } + + public func localizedDescription(style: Style) -> String { + switch style { + case .country: + return countryDescription + + case .countryWithCategory(let withCategory): + return countryDescription(withCategory: withCategory) + + case .shortWithDefault: + return shortDescriptionWithDefault + + case .longWithCategory(let withCategory): + return longDescription(withCategory: withCategory) + } + } + + private var countryDescription: String { countryCode.localizedAsCountryCode } - func localizedCountry(withCategory: Bool) -> String { - let desc = localizedCountry + private func countryDescription(withCategory: Bool) -> String { + let desc = countryDescription if withCategory, !categoryName.isEmpty { return "\(categoryName.uppercased()): \(desc)" } return desc } - var localizedShortDescription: String? { + private var shortDescriptionWithDefault: String { + shortDescription ?? "\(L10n.Global.Strings.default) [\(apiId)]" + } + + private func longDescription(withCategory: Bool) -> String { + var comps: [String] = [countryDescription] + shortDescription.map { + comps.append($0) + } + let desc = comps.joined(separator: ", ") + if withCategory, !categoryName.isEmpty { + return "\(categoryName.uppercased()): \(desc)" + } + return desc + } +} + +extension ProviderServer: StyledOptionalLocalizableEntity { + public enum OptionalStyle { + case short + } + + public func localizedDescription(optionalStyle: OptionalStyle) -> String? { + switch optionalStyle { + case .short: + return shortDescription + } + } + + private var shortDescription: String? { var comps = localizedName.map { [$0] } ?? [] if let serverIndex = serverIndex { comps.append("#\(serverIndex)") @@ -96,26 +189,10 @@ extension ProviderServer { } return str } - - var localizedShortDescriptionWithDefault: String { - localizedShortDescription ?? "\(L10n.Global.Strings.default) [\(apiId)]" - } - - func localizedLongDescription(withCategory: Bool) -> String { - var comps: [String] = [localizedCountry] - localizedShortDescription.map { - comps.append($0) - } - let desc = comps.joined(separator: ", ") - if withCategory, !categoryName.isEmpty { - return "\(categoryName.uppercased()): \(desc)" - } - return desc - } } -extension ProviderServer.Preset { - var localizedDescription: String { +extension ProviderServer.Preset: LocalizableEntity { + public var localizedDescription: String { name } } diff --git a/Passepartout/App/L10n/TunnelKit+L10n.swift b/Passepartout/App/L10n/TunnelKit+L10n.swift index 70d98860..5715c24d 100644 --- a/Passepartout/App/L10n/TunnelKit+L10n.swift +++ b/Passepartout/App/L10n/TunnelKit+L10n.swift @@ -30,8 +30,8 @@ import TunnelKitManager import TunnelKitOpenVPN import TunnelKitWireGuard -extension VPNStatus { - var localizedDescription: String { +extension VPNStatus: LocalizableEntity { + public var localizedDescription: String { switch self { case .connecting: return L10n.Tunnelkit.Vpn.connecting @@ -48,62 +48,74 @@ extension VPNStatus { } } -extension DataCount { - var localizedDescription: String { +extension DataCount: LocalizableEntity { + public var localizedDescription: String { let down = received.descriptionAsDataUnit let up = sent.descriptionAsDataUnit return "↓\(down) ↑\(up)" } } -extension Int { - var localizedDescriptionAsMTU: String { - guard self != 0 else { - return L10n.Global.Strings.default - } - return description - } -} +extension IPv4Settings: StyledLocalizableEntity { + public enum Style { + case address -extension TimeInterval { - var localizedDescriptionAsKeepAlive: String { - let V = L10n.Endpoint.Advanced.Openvpn.Items.self - if self > 0 { - return V.KeepAlive.Value.seconds(Int(self)) - } else { - return L10n.Global.Strings.disabled + case defaultGateway + } + + public func localizedDescription(style: Style) -> String { + switch style { + case .address: + return addressDescription + + case .defaultGateway: + return defaultGatewayDescription } } -} -extension IPv4Settings { - var localizedAddress: String { + private var addressDescription: String { "\(address)/\(addressMask)" } - var localizedDefaultGateway: String { + private var defaultGatewayDescription: String { defaultGateway } } -extension IPv6Settings { - var localizedAddress: String { +extension IPv6Settings: StyledLocalizableEntity { + public enum Style { + case address + + case defaultGateway + } + + public func localizedDescription(style: Style) -> String { + switch style { + case .address: + return addressDescription + + case .defaultGateway: + return defaultGatewayDescription + } + } + + private var addressDescription: String { "\(address)/\(addressPrefixLength)" } - var localizedDefaultGateway: String { + private var defaultGatewayDescription: String { defaultGateway } } -extension IPv4Settings.Route { - var localizedDescription: String { +extension IPv4Settings.Route: LocalizableEntity { + public var localizedDescription: String { "\(destination)/\(mask) -> \(gateway ?? "*")" } } -extension IPv6Settings.Route { - var localizedDescription: String { +extension IPv6Settings.Route: LocalizableEntity { + public var localizedDescription: String { "\(destination)/\(prefixLength) -> \(gateway ?? "*")" } } diff --git a/Passepartout/App/L10n/WireGuard+L10n.swift b/Passepartout/App/L10n/WireGuard+L10n.swift index 69156633..e5977b35 100644 --- a/Passepartout/App/L10n/WireGuard+L10n.swift +++ b/Passepartout/App/L10n/WireGuard+L10n.swift @@ -24,8 +24,36 @@ // import Foundation +import PassepartoutLibrary import TunnelKitWireGuard +extension WireGuard.ConfigurationBuilder: StyledOptionalLocalizableEntity { + public enum OptionalStyle { + case keepAlive(peerIndex: Int) + } + + public func localizedDescription(optionalStyle: OptionalStyle) -> String? { + switch optionalStyle { + case .keepAlive(let peerIndex): + return keepAlive(ofPeer: peerIndex)?.keepAliveDescription + } + } +} + +private extension UInt16 { + var keepAliveDescription: String { + // FIXME: l10n, move from OpenVPN to shared + let V = L10n.Endpoint.Advanced.Openvpn.Items.self + if self > 0 { + return V.KeepAlive.Value.seconds(Int(self)) + } else { + return L10n.Global.Strings.disabled + } + } +} + +// MARK: - Errors + extension TunnelKitWireGuardError: LocalizedError { public var errorDescription: String? { let V = L10n.Tunnelkit.Errors.Vpn.self diff --git a/Passepartout/App/Mac/Managers/DefaultLightProviderManager.swift b/Passepartout/App/Mac/Managers/DefaultLightProviderManager.swift index 419ccc25..7241ff47 100644 --- a/Passepartout/App/Mac/Managers/DefaultLightProviderManager.swift +++ b/Passepartout/App/Mac/Managers/DefaultLightProviderManager.swift @@ -50,7 +50,7 @@ final class DefaultLightProviderLocation: LightProviderLocation { let servers: [LightProviderServer] init(_ location: ProviderLocation) { - description = location.localizedCountry + description = location.localizedDescription(style: .country) id = location.id countryCode = location.countryCode servers = location.servers? @@ -71,8 +71,8 @@ final class DefaultLightProviderServer: LightProviderServer { let serverId: String init(_ server: ProviderServer) { - description = server.localizedShortDescriptionWithDefault - longDescription = server.localizedLongDescription(withCategory: false) + description = server.localizedDescription(style: .shortWithDefault) + longDescription = server.localizedDescription(style: .longWithCategory(withCategory: false)) categoryName = server.categoryName locationId = server.locationId serverId = server.id diff --git a/Passepartout/App/Views/AccountView.swift b/Passepartout/App/Views/AccountView.swift index 483575f5..03f15827 100644 --- a/Passepartout/App/Views/AccountView.swift +++ b/Passepartout/App/Views/AccountView.swift @@ -89,7 +89,7 @@ struct AccountView: View { .withLeadingText(L10n.Account.Items.Seed.caption) } } footer: { - metadata?.localizedGuidanceString.map { + metadata?.localizedDescription(optionalStyle: .guidance).map { Text($0) } } @@ -133,21 +133,6 @@ private extension AccountView { } } -private extension Profile.Account.AuthenticationMethod { - var localizedDescription: String { - switch self { - case .persistent: - return L10n.Account.Items.AuthenticationMethod.persistent - - case .interactive: - return L10n.Account.Items.AuthenticationMethod.interactive - - case .totp: - return Unlocalized.Other.totp - } - } -} - // MARK: - private extension AccountView { diff --git a/Passepartout/App/Views/EndpointAdvancedView+OpenVPN.swift b/Passepartout/App/Views/EndpointAdvancedView+OpenVPN.swift index bbb77c30..97315fea 100644 --- a/Passepartout/App/Views/EndpointAdvancedView+OpenVPN.swift +++ b/Passepartout/App/Views/EndpointAdvancedView+OpenVPN.swift @@ -87,11 +87,11 @@ private extension EndpointAdvancedView.OpenVPNView { if let settings = builder.ipv4 { themeLongContentLinkDefault( L10n.Global.Strings.address, - content: .constant(settings.localizedAddress) + content: .constant(settings.localizedDescription(style: .address)) ) themeLongContentLinkDefault( L10n.NetworkSettings.Gateway.title, - content: .constant(settings.localizedDefaultGateway) + content: .constant(settings.localizedDescription(style: .defaultGateway)) ) } builder.routes4.map { routes in @@ -112,11 +112,11 @@ private extension EndpointAdvancedView.OpenVPNView { if let settings = builder.ipv6 { themeLongContentLinkDefault( L10n.Global.Strings.address, - content: .constant(settings.localizedAddress) + content: .constant(settings.localizedDescription(style: .address)) ) themeLongContentLinkDefault( L10n.NetworkSettings.Gateway.title, - content: .constant(settings.localizedDefaultGateway) + content: .constant(settings.localizedDescription(style: .defaultGateway)) ) } builder.routes6.map { routes in @@ -137,17 +137,17 @@ private extension EndpointAdvancedView.OpenVPNView { Section { settings.cipher.map { Text(L10n.Endpoint.Advanced.Openvpn.Items.Cipher.caption) - .withTrailingText($0.localizedDescription) + .withTrailingText($0) } settings.digest.map { Text(L10n.Endpoint.Advanced.Openvpn.Items.Digest.caption) - .withTrailingText($0.localizedDescription) + .withTrailingText($0) } if let xor = settings.xor { themeLongContentLink( Unlocalized.VPN.xor, - content: .constant(xor.localizedLongDescription), - withPreview: xor.localizedDescription + content: .constant(xor.longDescription), + withPreview: xor.shortDescription ) } else { Text(Unlocalized.VPN.xor) @@ -176,8 +176,8 @@ private extension EndpointAdvancedView.OpenVPNView { if let xor = builder.xorMethod { themeLongContentLink( Unlocalized.VPN.xor, - content: .constant(xor.localizedLongDescription), - withPreview: xor.localizedDescription + content: .constant(xor.localizedDescription(style: .long)), + withPreview: xor.localizedDescription(style: .short) ) } else { Text(Unlocalized.VPN.xor) @@ -193,11 +193,11 @@ private extension EndpointAdvancedView.OpenVPNView { Section { settings.framing.map { Text(L10n.Endpoint.Advanced.Openvpn.Items.CompressionFraming.caption) - .withTrailingText($0.localizedDescription) + .withTrailingText($0) } settings.algorithm.map { Text(L10n.Endpoint.Advanced.Openvpn.Items.CompressionAlgorithm.caption) - .withTrailingText($0.localizedDescription) + .withTrailingText($0) } } header: { Text(L10n.Endpoint.Advanced.Openvpn.Sections.Compression.header) @@ -246,11 +246,11 @@ private extension EndpointAdvancedView.OpenVPNView { Section { settings.proxy.map { Text(L10n.Global.Strings.address) - .withTrailingText($0.rawValue, copyOnTap: true) + .withTrailingText($0, copyOnTap: true) } settings.pac.map { Text(Unlocalized.Network.proxyAutoConfiguration) - .withTrailingText($0.absoluteString, copyOnTap: true) + .withTrailingText($0, copyOnTap: true) } ForEach(settings.bypass, id: \.self) { Text(L10n.NetworkSettings.Items.ProxyBypass.caption) @@ -286,11 +286,11 @@ private extension EndpointAdvancedView.OpenVPNView { themeLongContentLink( L10n.Endpoint.Advanced.Openvpn.Items.TlsWrapping.caption, content: .constant(wrap.key.hexString), - withPreview: builder.tlsWrap.localizedDescription + withPreview: builder.localizedDescription(style: .tlsWrap) ) } Text(L10n.Endpoint.Advanced.Openvpn.Items.Eku.caption) - .withTrailingText(builder.checksEKU.localizedDescriptionAsEKU) + .withTrailingText(builder.localizedDescription(style: .eku)) } header: { Text(Unlocalized.Network.tls) } @@ -301,19 +301,19 @@ private extension EndpointAdvancedView.OpenVPNView { Section { settings.keepAlive.map { Text(L10n.Global.Strings.keepalive) - .withTrailingText($0.localizedDescriptionAsKeepAlive) + .withTrailingText($0) } settings.reneg.map { Text(L10n.Endpoint.Advanced.Openvpn.Items.RenegotiationSeconds.caption) - .withTrailingText($0.localizedDescriptionAsRenegotiatesAfter) + .withTrailingText($0) } settings.randomizeEndpoint.map { Text(L10n.Endpoint.Advanced.Openvpn.Items.RandomEndpoint.caption) - .withTrailingText($0.localizedDescriptionAsRandomizeEndpoint) + .withTrailingText($0) } settings.randomizeHostnames.map { Text(L10n.Endpoint.Advanced.Openvpn.Items.RandomHostname.caption) - .withTrailingText($0.localizedDescriptionAsRandomizeHostnames) + .withTrailingText($0) } } header: { Text(L10n.Endpoint.Advanced.Openvpn.Sections.Other.header) @@ -324,17 +324,17 @@ private extension EndpointAdvancedView.OpenVPNView { private extension OpenVPN.Configuration { struct CommunicationOptions { - let cipher: OpenVPN.Cipher? + let cipher: String? - let digest: OpenVPN.Digest? + let digest: String? - let xor: OpenVPN.XORMethod? + let xor: (shortDescription: String, longDescription: String)? } struct CompressionOptions { - let framing: OpenVPN.CompressionFraming? + let framing: String? - let algorithm: OpenVPN.CompressionAlgorithm? + let algorithm: String? } struct DNSOptions { @@ -344,35 +344,44 @@ private extension OpenVPN.Configuration { } struct ProxyOptions { - let proxy: Proxy? + let proxy: String? - let pac: URL? + let pac: String? let bypass: [String] } struct OtherOptions { - let keepAlive: TimeInterval? + let keepAlive: String? - let reneg: TimeInterval? + let reneg: String? - let randomizeEndpoint: Bool? + let randomizeEndpoint: String? - let randomizeHostnames: Bool? + let randomizeHostnames: String? } var communicationSettings: CommunicationOptions? { guard cipher != nil || digest != nil || xorMethod != nil else { return nil } - return .init(cipher: cipher, digest: digest, xor: xorMethod) + return .init( + cipher: cipher?.localizedDescription, + digest: digest?.localizedDescription, + xor: xorMethod.map { + ($0.localizedDescription(style: .short), $0.localizedDescription(style: .long)) + } + ) } var compressionSettings: CompressionOptions? { guard compressionFraming != nil || compressionAlgorithm != nil else { return nil } - return .init(framing: compressionFraming, algorithm: compressionAlgorithm) + return .init( + framing: compressionFraming?.localizedDescription, + algorithm: compressionAlgorithm?.localizedDescription + ) } var dnsSettings: DNSOptions? { @@ -388,8 +397,8 @@ private extension OpenVPN.Configuration { return nil } return .init( - proxy: httpsProxy ?? httpProxy, - pac: proxyAutoConfigurationURL, + proxy: (httpsProxy ?? httpProxy)?.rawValue, + pac: proxyAutoConfigurationURL?.absoluteString, bypass: proxyBypassDomains ?? [] ) } @@ -400,10 +409,10 @@ private extension OpenVPN.Configuration { return nil } return .init( - keepAlive: keepAliveInterval, - reneg: renegotiatesAfter, - randomizeEndpoint: randomizeEndpoint, - randomizeHostnames: randomizeHostnames + keepAlive: localizedDescription(optionalStyle: .keepAlive), + reneg: localizedDescription(optionalStyle: .renegotiatesAfter), + randomizeEndpoint: localizedDescription(optionalStyle: .randomizeEndpoint), + randomizeHostnames: localizedDescription(optionalStyle: .randomizeHostnames) ) } } diff --git a/Passepartout/App/Views/EndpointAdvancedView+WireGuard.swift b/Passepartout/App/Views/EndpointAdvancedView+WireGuard.swift index 300ba735..d266105e 100644 --- a/Passepartout/App/Views/EndpointAdvancedView+WireGuard.swift +++ b/Passepartout/App/Views/EndpointAdvancedView+WireGuard.swift @@ -85,7 +85,7 @@ private extension EndpointAdvancedView.WireGuardView { builder.mtu.map { mtu in Section { Text(Unlocalized.Network.mtu) - .withTrailingText(Int(mtu).localizedDescriptionAsMTU) + .withTrailingText(Int(mtu).localizedDescription(style: .mtu)) } } } diff --git a/Passepartout/App/Views/EndpointView+WireGuard.swift b/Passepartout/App/Views/EndpointView+WireGuard.swift index 7693addc..185bfd34 100644 --- a/Passepartout/App/Views/EndpointView+WireGuard.swift +++ b/Passepartout/App/Views/EndpointView+WireGuard.swift @@ -125,9 +125,9 @@ private extension EndpointView.WireGuardView { Text(L10n.Endpoint.Wireguard.Items.AllowedIp.caption) .withTrailingText($0) } - builder.keepAlive(ofPeer: peerIndex).map { + builder.localizedDescription(optionalStyle: .keepAlive(peerIndex: peerIndex)).map { Text(L10n.Global.Strings.keepalive) - .withTrailingText(TimeInterval($0).localizedDescriptionAsKeepAlive) + .withTrailingText($0) } } header: { Text(L10n.Endpoint.Wireguard.Items.Peer.caption) diff --git a/Passepartout/App/Views/NetworkSettingsView.swift b/Passepartout/App/Views/NetworkSettingsView.swift index 2541abf2..c4d296eb 100644 --- a/Passepartout/App/Views/NetworkSettingsView.swift +++ b/Passepartout/App/Views/NetworkSettingsView.swift @@ -257,7 +257,7 @@ private extension NetworkSettingsView { L10n.Global.Strings.bytes, selection: $settings.mtu.mtuBytes, values: Network.MTUSettings.availableBytes, - description: \.localizedDescriptionAsMTU + description: { $0.localizedDescription(style: .mtu) } ) } } header: { diff --git a/Passepartout/App/Views/ProfileView+Provider.swift b/Passepartout/App/Views/ProfileView+Provider.swift index fa79b91e..1ad39173 100644 --- a/Passepartout/App/Views/ProfileView+Provider.swift +++ b/Passepartout/App/Views/ProfileView+Provider.swift @@ -130,9 +130,9 @@ private extension ProfileView.ProviderSection { return nil } if currentProfile.value.providerRandomizesServer ?? false { - return server.localizedCountry(withCategory: true) + return server.localizedDescription(style: .countryWithCategory(withCategory: true)) } else { - return server.localizedLongDescription(withCategory: true) + return server.localizedDescription(style: .longWithCategory(withCategory: true)) } } @@ -144,11 +144,11 @@ private extension ProfileView.ProviderSection { } var currentProviderPreset: String? { - providerManager.localizedPreset(forProfile: profile) + providerManager.localizedDescription(optionalStyle: .preset(profile: profile)) } var lastInfrastructureUpdate: String? { - providerManager.localizedInfrastructureUpdate(forProfile: profile) + providerManager.localizedDescription(optionalStyle: .infrastructureUpdate(profile: profile)) } } diff --git a/Passepartout/App/Views/ProviderLocationView.swift b/Passepartout/App/Views/ProviderLocationView.swift index 7c6efc15..dfce3369 100644 --- a/Passepartout/App/Views/ProviderLocationView.swift +++ b/Passepartout/App/Views/ProviderLocationView.swift @@ -101,13 +101,15 @@ extension ProviderLocationView { HStack { themeAssetsCountryImage(location.countryCode).asAssetImage VStack { - if let singleServer = location.onlyServer, singleServer.localizedShortDescription != nil { - Text(location.localizedCountry) + if let singleServer = location.onlyServer, + let shortServerDescription = singleServer.localizedDescription(optionalStyle: .short) { + + Text(location.localizedDescription(style: .country)) .frame(maxWidth: .infinity, alignment: .leading) - Text(singleServer.localizedShortDescription ?? "") + Text(shortServerDescription) .frame(maxWidth: .infinity, alignment: .leading) } else { - Text(location.localizedCountry) + Text(location.localizedDescription(style: .country)) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) } }.withTrailingCheckmark(when: location.id == selectedLocationId) @@ -132,7 +134,7 @@ extension ProviderLocationView { ScrollViewReader { scrollProxy in List { ForEach(servers) { server in - Button(server.localizedShortDescriptionWithDefault) { + Button(server.localizedDescription(style: .shortWithDefault)) { selectedServer = server }.withTrailingCheckmark(when: server.id == selectedServer?.id) } @@ -211,7 +213,7 @@ private extension ProviderLocationView { ServerListView( location: location, selectedServer: $selectedServer - ).navigationTitle(location.localizedCountry) + ).navigationTitle(location.localizedDescription(style: .country)) }, label: { LocationRow( location: location, diff --git a/Passepartout/App/Views/VPNStatusText.swift b/Passepartout/App/Views/VPNStatusText.swift index 70b023b1..439a9022 100644 --- a/Passepartout/App/Views/VPNStatusText.swift +++ b/Passepartout/App/Views/VPNStatusText.swift @@ -45,10 +45,10 @@ struct VPNStatusText: View { private extension VPNStatusText { var statusText: String { - currentVPNState.localizedStatusDescription( + currentVPNState.localizedDescription(style: .status( isActiveProfile: isActiveProfile, withErrors: true, dataCountIfAvailable: true - ) + )) } } diff --git a/PassepartoutLibrary/Sources/PassepartoutCore/Reusable/LocalizableEntity.swift b/PassepartoutLibrary/Sources/PassepartoutCore/Reusable/LocalizableEntity.swift new file mode 100644 index 00000000..ab107046 --- /dev/null +++ b/PassepartoutLibrary/Sources/PassepartoutCore/Reusable/LocalizableEntity.swift @@ -0,0 +1,42 @@ +// +// LocalizableEntity.swift +// Passepartout +// +// Created by Davide De Rosa on 7/4/23. +// Copyright (c) 2023 Davide De Rosa. All rights reserved. +// +// https://github.com/passepartoutvpn +// +// This file is part of Passepartout. +// +// Passepartout is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Passepartout is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Passepartout. If not, see . +// + +import Foundation + +public protocol LocalizableEntity { + var localizedDescription: String { get } +} + +public protocol StyledLocalizableEntity { + associatedtype Style + + func localizedDescription(style: Style) -> String +} + +public protocol StyledOptionalLocalizableEntity { + associatedtype OptionalStyle + + func localizedDescription(optionalStyle: OptionalStyle) -> String? +} diff --git a/Passepartout/App/Reusable/Validators.swift b/PassepartoutLibrary/Sources/PassepartoutCore/Reusable/Validators.swift similarity index 83% rename from Passepartout/App/Reusable/Validators.swift rename to PassepartoutLibrary/Sources/PassepartoutCore/Reusable/Validators.swift index c820349d..e9c551c7 100644 --- a/Passepartout/App/Reusable/Validators.swift +++ b/PassepartoutLibrary/Sources/PassepartoutCore/Reusable/Validators.swift @@ -25,8 +25,8 @@ import Foundation -struct Validators { - enum ValidationError: Error { +public struct Validators { + public enum ValidationError: Error { case notSet case empty @@ -44,20 +44,20 @@ struct Validators { private static let rxWildcardDomainName = NSRegularExpression("^((?!-)[A-Za-z0-9-]{1,63}(? 0 else { throw ValidationError.domainName } } - static func wildcardDomainName(_ string: String) throws { + public static func wildcardDomainName(_ string: String) throws { guard rxWildcardDomainName.numberOfMatches(in: string, options: [], range: .init(location: 0, length: string.count)) > 0 else { throw ValidationError.wildcardDomainName } } - static func url(_ string: String) throws { + public static func url(_ string: String) throws { guard URL(string: string) != nil else { throw ValidationError.url } } - static func dnsOverTLSServerName(_ string: String) throws { + public static func dnsOverTLSServerName(_ string: String) throws { do { try domainName(string) } catch {