Share common protocols across localized strings (#324)

This commit is contained in:
Davide De Rosa 2023-07-05 16:18:33 +01:00 committed by GitHub
parent bf70c7c59a
commit 98e5e4cdde
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 487 additions and 215 deletions

View File

@ -29,7 +29,6 @@
0E0F4C5C29C76B790022E884 /* SceneDelegate+Shortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0F4C5B29C76B790022E884 /* SceneDelegate+Shortcuts.swift */; }; 0E0F4C5C29C76B790022E884 /* SceneDelegate+Shortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0F4C5B29C76B790022E884 /* SceneDelegate+Shortcuts.swift */; };
0E0F4C6429C84B5A0022E884 /* LockableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0F4C6329C84B5A0022E884 /* LockableView.swift */; }; 0E0F4C6429C84B5A0022E884 /* LockableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0F4C6329C84B5A0022E884 /* LockableView.swift */; };
0E0F4C6629C84CF60022E884 /* LogoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0F4C6529C84CF60022E884 /* LogoView.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 */; }; 0E1AD5CC2A2682DA002AE6E6 /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E1AD5CB2A2682DA002AE6E6 /* AppError.swift */; };
0E1AD5CE2A268645002AE6E6 /* Errors+L10n.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E1AD5CD2A268645002AE6E6 /* Errors+L10n.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 */; }; 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 = "<group>"; }; 0E0F4C5B29C76B790022E884 /* SceneDelegate+Shortcuts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SceneDelegate+Shortcuts.swift"; sourceTree = "<group>"; };
0E0F4C6329C84B5A0022E884 /* LockableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockableView.swift; sourceTree = "<group>"; }; 0E0F4C6329C84B5A0022E884 /* LockableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockableView.swift; sourceTree = "<group>"; };
0E0F4C6529C84CF60022E884 /* LogoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogoView.swift; sourceTree = "<group>"; }; 0E0F4C6529C84CF60022E884 /* LogoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogoView.swift; sourceTree = "<group>"; };
0E12BC8E27F62C8500B2F912 /* Validators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Validators.swift; sourceTree = "<group>"; };
0E1AD5CB2A2682DA002AE6E6 /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = "<group>"; }; 0E1AD5CB2A2682DA002AE6E6 /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = "<group>"; };
0E1AD5CD2A268645002AE6E6 /* Errors+L10n.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Errors+L10n.swift"; sourceTree = "<group>"; }; 0E1AD5CD2A268645002AE6E6 /* Errors+L10n.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Errors+L10n.swift"; sourceTree = "<group>"; };
0E1B5F5B29C506AC00FE7D18 /* ProfileView+Diagnostics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileView+Diagnostics.swift"; sourceTree = "<group>"; }; 0E1B5F5B29C506AC00FE7D18 /* ProfileView+Diagnostics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileView+Diagnostics.swift"; sourceTree = "<group>"; };
@ -612,7 +610,6 @@
0E2C172A27CB63F9007E8488 /* Reviewer.swift */, 0E2C172A27CB63F9007E8488 /* Reviewer.swift */,
0ED89C1427DE0A0C008B36D6 /* Shortcut.swift */, 0ED89C1427DE0A0C008B36D6 /* Shortcut.swift */,
0E5349BD27C16A4500C71BB3 /* StyledPicker.swift */, 0E5349BD27C16A4500C71BB3 /* StyledPicker.swift */,
0E12BC8E27F62C8500B2F912 /* Validators.swift */,
); );
path = Reusable; path = Reusable;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1509,7 +1506,6 @@
0E2A8D4927ADF87F00207D04 /* PassepartoutApp.swift in Sources */, 0E2A8D4927ADF87F00207D04 /* PassepartoutApp.swift in Sources */,
0EBC075527EBC83800208AD9 /* MailComposerView.swift in Sources */, 0EBC075527EBC83800208AD9 /* MailComposerView.swift in Sources */,
0EF0FAF727DD159C007EB181 /* IntentDispatcher.swift in Sources */, 0EF0FAF727DD159C007EB181 /* IntentDispatcher.swift in Sources */,
0E12BC8F27F62C8600B2F912 /* Validators.swift in Sources */,
0E0838FD2877334300A34EC0 /* DefaultLightProviderManager.swift in Sources */, 0E0838FD2877334300A34EC0 /* DefaultLightProviderManager.swift in Sources */,
0E0F4C6629C84CF60022E884 /* LogoView.swift in Sources */, 0E0F4C6629C84CF60022E884 /* LogoView.swift in Sources */,
0E039279281890B100827C10 /* AddHostView.swift in Sources */, 0E039279281890B100827C10 /* AddHostView.swift in Sources */,

View File

@ -48,7 +48,7 @@ final class IntentDispatcher {
intent.profileId = header.id.uuidString intent.profileId = header.id.uuidString
intent.providerFullName = providerFullName intent.providerFullName = providerFullName
intent.serverId = server.id intent.serverId = server.id
intent.serverName = server.localizedLongDescription(withCategory: false) intent.serverName = server.localizedDescription(style: .longWithCategory(withCategory: false))
return intent return intent
} }

View File

@ -60,7 +60,7 @@ extension ProviderLocation: Comparable {
} }
public static func < (lhs: Self, rhs: Self) -> Bool { public static func < (lhs: Self, rhs: Self) -> Bool {
lhs.localizedCountry < rhs.localizedCountry lhs.localizedDescription(style: .country) < rhs.localizedDescription(style: .country)
} }
} }

View File

@ -26,8 +26,19 @@
import Foundation import Foundation
import PassepartoutLibrary import PassepartoutLibrary
extension ObservableVPNState { extension ObservableVPNState: StyledLocalizableEntity {
func localizedStatusDescription(isActiveProfile: Bool, withErrors: Bool, dataCountIfAvailable: Bool) -> String { 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 { guard isActiveProfile && isEnabled else {
return L10n.Tunnelkit.Vpn.disabled return L10n.Tunnelkit.Vpn.disabled
} }
@ -65,8 +76,8 @@ extension Profile.WireGuardSettings {
} }
} }
extension Network.Choice { extension Network.Choice: LocalizableEntity {
var localizedDescription: String { public var localizedDescription: String {
switch self { switch self {
case .automatic: case .automatic:
return L10n.Global.Strings.automatic return L10n.Global.Strings.automatic
@ -77,8 +88,8 @@ extension Network.Choice {
} }
} }
extension Network.DNSSettings.ConfigurationType { extension Network.DNSSettings.ConfigurationType: LocalizableEntity {
var localizedDescription: String { public var localizedDescription: String {
switch self { switch self {
case .plain: case .plain:
return Unlocalized.DNS.plain return Unlocalized.DNS.plain
@ -95,8 +106,8 @@ extension Network.DNSSettings.ConfigurationType {
} }
} }
extension Network.ProxySettings.ConfigurationType { extension Network.ProxySettings.ConfigurationType: LocalizableEntity {
var localizedDescription: String { public var localizedDescription: String {
switch self { switch self {
case .manual: case .manual:
return L10n.Global.Strings.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
}
}

View File

@ -27,20 +27,20 @@ import Foundation
import PassepartoutLibrary import PassepartoutLibrary
import TunnelKitOpenVPN import TunnelKitOpenVPN
extension OpenVPN.Cipher { extension OpenVPN.Cipher: LocalizableEntity {
var localizedDescription: String { public var localizedDescription: String {
description description
} }
} }
extension OpenVPN.Digest { extension OpenVPN.Digest: LocalizableEntity {
var localizedDescription: String { public var localizedDescription: String {
description description
} }
} }
extension OpenVPN.CompressionFraming { extension OpenVPN.CompressionFraming: LocalizableEntity {
var localizedDescription: String { public var localizedDescription: String {
switch self { switch self {
case .disabled: case .disabled:
return L10n.Global.Strings.disabled return L10n.Global.Strings.disabled
@ -54,8 +54,8 @@ extension OpenVPN.CompressionFraming {
} }
} }
extension OpenVPN.CompressionAlgorithm { extension OpenVPN.CompressionAlgorithm: LocalizableEntity {
var localizedDescription: String { public var localizedDescription: String {
let V = L10n.Endpoint.Advanced.Openvpn.Items.self let V = L10n.Endpoint.Advanced.Openvpn.Items.self
switch self { switch self {
case .disabled: case .disabled:
@ -70,24 +70,24 @@ extension OpenVPN.CompressionAlgorithm {
} }
} }
extension Optional where Wrapped == OpenVPN.TLSWrap { extension OpenVPN.XORMethod: StyledLocalizableEntity {
var localizedDescription: String { public enum Style {
guard let strategy = self?.strategy else { case short
return L10n.Global.Strings.disabled
}
let V = L10n.Endpoint.Advanced.Openvpn.Items.self
switch strategy {
case .auth:
return V.TlsWrapping.Value.auth
case .crypt: case long
return V.TlsWrapping.Value.crypt }
public func localizedDescription(style: Style) -> String {
switch style {
case .short:
return shortDescription
case .long:
return longDescription
} }
} }
}
extension OpenVPN.XORMethod { private var shortDescription: String {
var localizedDescription: String {
switch self { switch self {
case .xormask: case .xormask:
return Unlocalized.OpenVPN.XOR.xormask.rawValue return Unlocalized.OpenVPN.XOR.xormask.rawValue
@ -103,52 +103,22 @@ extension OpenVPN.XORMethod {
} }
} }
var localizedLongDescription: String { private var longDescription: String {
switch self { switch self {
case .xormask(let mask): case .xormask(let mask):
return "\(localizedDescription) \(mask.toHex())" return "\(shortDescription) \(mask.toHex())"
case .obfuscate(let mask): case .obfuscate(let mask):
return "\(localizedDescription) \(mask.toHex())" return "\(shortDescription) \(mask.toHex())"
default: default:
return localizedDescription return shortDescription
} }
} }
} }
extension Optional where Wrapped == Bool { extension OpenVPN.PullMask: LocalizableEntity {
var localizedDescriptionAsEKU: String { public var localizedDescription: 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 {
switch self { switch self {
case .routes: case .routes:
return L10n.Endpoint.Advanced.Openvpn.Items.Route.caption 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 { extension TunnelKitOpenVPNError: LocalizedError {
public var errorDescription: String? { public var errorDescription: String? {
let V = L10n.Tunnelkit.Errors.Vpn.self let V = L10n.Tunnelkit.Errors.Vpn.self

View File

@ -26,15 +26,31 @@
import Foundation import Foundation
import PassepartoutLibrary import PassepartoutLibrary
extension ProviderManager { extension ProviderManager: StyledOptionalLocalizableEntity {
func localizedPreset(forProfile profile: Profile) -> String? { 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 { guard let server = profile.providerServer(self) else {
return nil return nil
} }
return profile.providerPreset(server)?.localizedDescription 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 { guard let providerName = profile.header.providerName else {
return nil return nil
} }
@ -42,8 +58,19 @@ extension ProviderManager {
} }
} }
extension ProviderMetadata { extension ProviderMetadata: StyledOptionalLocalizableEntity {
var localizedGuidanceString: String? { 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 prefix = "account.sections.guidance.footer.infrastructure"
let key = "\(prefix).\(name)" let key = "\(prefix).\(name)"
var format = NSLocalizedString(key, bundle: .main, comment: "") var format = NSLocalizedString(key, bundle: .main, comment: "")
@ -59,26 +86,92 @@ extension ProviderMetadata {
} }
} }
extension ProviderLocation { extension ProviderLocation: StyledLocalizableEntity {
var localizedCountry: String { public enum Style {
case country
}
public func localizedDescription(style: Style) -> String {
switch style {
case .country:
return countryDescription
}
}
private var countryDescription: String {
countryCode.localizedAsCountryCode countryCode.localizedAsCountryCode
} }
} }
extension ProviderServer { extension ProviderServer: StyledLocalizableEntity {
var localizedCountry: String { 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 countryCode.localizedAsCountryCode
} }
func localizedCountry(withCategory: Bool) -> String { private func countryDescription(withCategory: Bool) -> String {
let desc = localizedCountry let desc = countryDescription
if withCategory, !categoryName.isEmpty { if withCategory, !categoryName.isEmpty {
return "\(categoryName.uppercased()): \(desc)" return "\(categoryName.uppercased()): \(desc)"
} }
return 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] } ?? [] var comps = localizedName.map { [$0] } ?? []
if let serverIndex = serverIndex { if let serverIndex = serverIndex {
comps.append("#\(serverIndex)") comps.append("#\(serverIndex)")
@ -96,26 +189,10 @@ extension ProviderServer {
} }
return str 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 { extension ProviderServer.Preset: LocalizableEntity {
var localizedDescription: String { public var localizedDescription: String {
name name
} }
} }

View File

@ -30,8 +30,8 @@ import TunnelKitManager
import TunnelKitOpenVPN import TunnelKitOpenVPN
import TunnelKitWireGuard import TunnelKitWireGuard
extension VPNStatus { extension VPNStatus: LocalizableEntity {
var localizedDescription: String { public var localizedDescription: String {
switch self { switch self {
case .connecting: case .connecting:
return L10n.Tunnelkit.Vpn.connecting return L10n.Tunnelkit.Vpn.connecting
@ -48,62 +48,74 @@ extension VPNStatus {
} }
} }
extension DataCount { extension DataCount: LocalizableEntity {
var localizedDescription: String { public var localizedDescription: String {
let down = received.descriptionAsDataUnit let down = received.descriptionAsDataUnit
let up = sent.descriptionAsDataUnit let up = sent.descriptionAsDataUnit
return "\(down)\(up)" return "\(down)\(up)"
} }
} }
extension Int { extension IPv4Settings: StyledLocalizableEntity {
var localizedDescriptionAsMTU: String { public enum Style {
guard self != 0 else { case address
return L10n.Global.Strings.default
}
return description
}
}
extension TimeInterval { case defaultGateway
var localizedDescriptionAsKeepAlive: String { }
let V = L10n.Endpoint.Advanced.Openvpn.Items.self
if self > 0 { public func localizedDescription(style: Style) -> String {
return V.KeepAlive.Value.seconds(Int(self)) switch style {
} else { case .address:
return L10n.Global.Strings.disabled return addressDescription
case .defaultGateway:
return defaultGatewayDescription
} }
} }
}
extension IPv4Settings { private var addressDescription: String {
var localizedAddress: String {
"\(address)/\(addressMask)" "\(address)/\(addressMask)"
} }
var localizedDefaultGateway: String { private var defaultGatewayDescription: String {
defaultGateway defaultGateway
} }
} }
extension IPv6Settings { extension IPv6Settings: StyledLocalizableEntity {
var localizedAddress: String { 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)" "\(address)/\(addressPrefixLength)"
} }
var localizedDefaultGateway: String { private var defaultGatewayDescription: String {
defaultGateway defaultGateway
} }
} }
extension IPv4Settings.Route { extension IPv4Settings.Route: LocalizableEntity {
var localizedDescription: String { public var localizedDescription: String {
"\(destination)/\(mask) -> \(gateway ?? "*")" "\(destination)/\(mask) -> \(gateway ?? "*")"
} }
} }
extension IPv6Settings.Route { extension IPv6Settings.Route: LocalizableEntity {
var localizedDescription: String { public var localizedDescription: String {
"\(destination)/\(prefixLength) -> \(gateway ?? "*")" "\(destination)/\(prefixLength) -> \(gateway ?? "*")"
} }
} }

View File

@ -24,8 +24,36 @@
// //
import Foundation import Foundation
import PassepartoutLibrary
import TunnelKitWireGuard 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 { extension TunnelKitWireGuardError: LocalizedError {
public var errorDescription: String? { public var errorDescription: String? {
let V = L10n.Tunnelkit.Errors.Vpn.self let V = L10n.Tunnelkit.Errors.Vpn.self

View File

@ -50,7 +50,7 @@ final class DefaultLightProviderLocation: LightProviderLocation {
let servers: [LightProviderServer] let servers: [LightProviderServer]
init(_ location: ProviderLocation) { init(_ location: ProviderLocation) {
description = location.localizedCountry description = location.localizedDescription(style: .country)
id = location.id id = location.id
countryCode = location.countryCode countryCode = location.countryCode
servers = location.servers? servers = location.servers?
@ -71,8 +71,8 @@ final class DefaultLightProviderServer: LightProviderServer {
let serverId: String let serverId: String
init(_ server: ProviderServer) { init(_ server: ProviderServer) {
description = server.localizedShortDescriptionWithDefault description = server.localizedDescription(style: .shortWithDefault)
longDescription = server.localizedLongDescription(withCategory: false) longDescription = server.localizedDescription(style: .longWithCategory(withCategory: false))
categoryName = server.categoryName categoryName = server.categoryName
locationId = server.locationId locationId = server.locationId
serverId = server.id serverId = server.id

View File

@ -89,7 +89,7 @@ struct AccountView: View {
.withLeadingText(L10n.Account.Items.Seed.caption) .withLeadingText(L10n.Account.Items.Seed.caption)
} }
} footer: { } footer: {
metadata?.localizedGuidanceString.map { metadata?.localizedDescription(optionalStyle: .guidance).map {
Text($0) 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: - // MARK: -
private extension AccountView { private extension AccountView {

View File

@ -87,11 +87,11 @@ private extension EndpointAdvancedView.OpenVPNView {
if let settings = builder.ipv4 { if let settings = builder.ipv4 {
themeLongContentLinkDefault( themeLongContentLinkDefault(
L10n.Global.Strings.address, L10n.Global.Strings.address,
content: .constant(settings.localizedAddress) content: .constant(settings.localizedDescription(style: .address))
) )
themeLongContentLinkDefault( themeLongContentLinkDefault(
L10n.NetworkSettings.Gateway.title, L10n.NetworkSettings.Gateway.title,
content: .constant(settings.localizedDefaultGateway) content: .constant(settings.localizedDescription(style: .defaultGateway))
) )
} }
builder.routes4.map { routes in builder.routes4.map { routes in
@ -112,11 +112,11 @@ private extension EndpointAdvancedView.OpenVPNView {
if let settings = builder.ipv6 { if let settings = builder.ipv6 {
themeLongContentLinkDefault( themeLongContentLinkDefault(
L10n.Global.Strings.address, L10n.Global.Strings.address,
content: .constant(settings.localizedAddress) content: .constant(settings.localizedDescription(style: .address))
) )
themeLongContentLinkDefault( themeLongContentLinkDefault(
L10n.NetworkSettings.Gateway.title, L10n.NetworkSettings.Gateway.title,
content: .constant(settings.localizedDefaultGateway) content: .constant(settings.localizedDescription(style: .defaultGateway))
) )
} }
builder.routes6.map { routes in builder.routes6.map { routes in
@ -137,17 +137,17 @@ private extension EndpointAdvancedView.OpenVPNView {
Section { Section {
settings.cipher.map { settings.cipher.map {
Text(L10n.Endpoint.Advanced.Openvpn.Items.Cipher.caption) Text(L10n.Endpoint.Advanced.Openvpn.Items.Cipher.caption)
.withTrailingText($0.localizedDescription) .withTrailingText($0)
} }
settings.digest.map { settings.digest.map {
Text(L10n.Endpoint.Advanced.Openvpn.Items.Digest.caption) Text(L10n.Endpoint.Advanced.Openvpn.Items.Digest.caption)
.withTrailingText($0.localizedDescription) .withTrailingText($0)
} }
if let xor = settings.xor { if let xor = settings.xor {
themeLongContentLink( themeLongContentLink(
Unlocalized.VPN.xor, Unlocalized.VPN.xor,
content: .constant(xor.localizedLongDescription), content: .constant(xor.longDescription),
withPreview: xor.localizedDescription withPreview: xor.shortDescription
) )
} else { } else {
Text(Unlocalized.VPN.xor) Text(Unlocalized.VPN.xor)
@ -176,8 +176,8 @@ private extension EndpointAdvancedView.OpenVPNView {
if let xor = builder.xorMethod { if let xor = builder.xorMethod {
themeLongContentLink( themeLongContentLink(
Unlocalized.VPN.xor, Unlocalized.VPN.xor,
content: .constant(xor.localizedLongDescription), content: .constant(xor.localizedDescription(style: .long)),
withPreview: xor.localizedDescription withPreview: xor.localizedDescription(style: .short)
) )
} else { } else {
Text(Unlocalized.VPN.xor) Text(Unlocalized.VPN.xor)
@ -193,11 +193,11 @@ private extension EndpointAdvancedView.OpenVPNView {
Section { Section {
settings.framing.map { settings.framing.map {
Text(L10n.Endpoint.Advanced.Openvpn.Items.CompressionFraming.caption) Text(L10n.Endpoint.Advanced.Openvpn.Items.CompressionFraming.caption)
.withTrailingText($0.localizedDescription) .withTrailingText($0)
} }
settings.algorithm.map { settings.algorithm.map {
Text(L10n.Endpoint.Advanced.Openvpn.Items.CompressionAlgorithm.caption) Text(L10n.Endpoint.Advanced.Openvpn.Items.CompressionAlgorithm.caption)
.withTrailingText($0.localizedDescription) .withTrailingText($0)
} }
} header: { } header: {
Text(L10n.Endpoint.Advanced.Openvpn.Sections.Compression.header) Text(L10n.Endpoint.Advanced.Openvpn.Sections.Compression.header)
@ -246,11 +246,11 @@ private extension EndpointAdvancedView.OpenVPNView {
Section { Section {
settings.proxy.map { settings.proxy.map {
Text(L10n.Global.Strings.address) Text(L10n.Global.Strings.address)
.withTrailingText($0.rawValue, copyOnTap: true) .withTrailingText($0, copyOnTap: true)
} }
settings.pac.map { settings.pac.map {
Text(Unlocalized.Network.proxyAutoConfiguration) Text(Unlocalized.Network.proxyAutoConfiguration)
.withTrailingText($0.absoluteString, copyOnTap: true) .withTrailingText($0, copyOnTap: true)
} }
ForEach(settings.bypass, id: \.self) { ForEach(settings.bypass, id: \.self) {
Text(L10n.NetworkSettings.Items.ProxyBypass.caption) Text(L10n.NetworkSettings.Items.ProxyBypass.caption)
@ -286,11 +286,11 @@ private extension EndpointAdvancedView.OpenVPNView {
themeLongContentLink( themeLongContentLink(
L10n.Endpoint.Advanced.Openvpn.Items.TlsWrapping.caption, L10n.Endpoint.Advanced.Openvpn.Items.TlsWrapping.caption,
content: .constant(wrap.key.hexString), content: .constant(wrap.key.hexString),
withPreview: builder.tlsWrap.localizedDescription withPreview: builder.localizedDescription(style: .tlsWrap)
) )
} }
Text(L10n.Endpoint.Advanced.Openvpn.Items.Eku.caption) Text(L10n.Endpoint.Advanced.Openvpn.Items.Eku.caption)
.withTrailingText(builder.checksEKU.localizedDescriptionAsEKU) .withTrailingText(builder.localizedDescription(style: .eku))
} header: { } header: {
Text(Unlocalized.Network.tls) Text(Unlocalized.Network.tls)
} }
@ -301,19 +301,19 @@ private extension EndpointAdvancedView.OpenVPNView {
Section { Section {
settings.keepAlive.map { settings.keepAlive.map {
Text(L10n.Global.Strings.keepalive) Text(L10n.Global.Strings.keepalive)
.withTrailingText($0.localizedDescriptionAsKeepAlive) .withTrailingText($0)
} }
settings.reneg.map { settings.reneg.map {
Text(L10n.Endpoint.Advanced.Openvpn.Items.RenegotiationSeconds.caption) Text(L10n.Endpoint.Advanced.Openvpn.Items.RenegotiationSeconds.caption)
.withTrailingText($0.localizedDescriptionAsRenegotiatesAfter) .withTrailingText($0)
} }
settings.randomizeEndpoint.map { settings.randomizeEndpoint.map {
Text(L10n.Endpoint.Advanced.Openvpn.Items.RandomEndpoint.caption) Text(L10n.Endpoint.Advanced.Openvpn.Items.RandomEndpoint.caption)
.withTrailingText($0.localizedDescriptionAsRandomizeEndpoint) .withTrailingText($0)
} }
settings.randomizeHostnames.map { settings.randomizeHostnames.map {
Text(L10n.Endpoint.Advanced.Openvpn.Items.RandomHostname.caption) Text(L10n.Endpoint.Advanced.Openvpn.Items.RandomHostname.caption)
.withTrailingText($0.localizedDescriptionAsRandomizeHostnames) .withTrailingText($0)
} }
} header: { } header: {
Text(L10n.Endpoint.Advanced.Openvpn.Sections.Other.header) Text(L10n.Endpoint.Advanced.Openvpn.Sections.Other.header)
@ -324,17 +324,17 @@ private extension EndpointAdvancedView.OpenVPNView {
private extension OpenVPN.Configuration { private extension OpenVPN.Configuration {
struct CommunicationOptions { 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 { struct CompressionOptions {
let framing: OpenVPN.CompressionFraming? let framing: String?
let algorithm: OpenVPN.CompressionAlgorithm? let algorithm: String?
} }
struct DNSOptions { struct DNSOptions {
@ -344,35 +344,44 @@ private extension OpenVPN.Configuration {
} }
struct ProxyOptions { struct ProxyOptions {
let proxy: Proxy? let proxy: String?
let pac: URL? let pac: String?
let bypass: [String] let bypass: [String]
} }
struct OtherOptions { 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? { var communicationSettings: CommunicationOptions? {
guard cipher != nil || digest != nil || xorMethod != nil else { guard cipher != nil || digest != nil || xorMethod != nil else {
return nil 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? { var compressionSettings: CompressionOptions? {
guard compressionFraming != nil || compressionAlgorithm != nil else { guard compressionFraming != nil || compressionAlgorithm != nil else {
return nil return nil
} }
return .init(framing: compressionFraming, algorithm: compressionAlgorithm) return .init(
framing: compressionFraming?.localizedDescription,
algorithm: compressionAlgorithm?.localizedDescription
)
} }
var dnsSettings: DNSOptions? { var dnsSettings: DNSOptions? {
@ -388,8 +397,8 @@ private extension OpenVPN.Configuration {
return nil return nil
} }
return .init( return .init(
proxy: httpsProxy ?? httpProxy, proxy: (httpsProxy ?? httpProxy)?.rawValue,
pac: proxyAutoConfigurationURL, pac: proxyAutoConfigurationURL?.absoluteString,
bypass: proxyBypassDomains ?? [] bypass: proxyBypassDomains ?? []
) )
} }
@ -400,10 +409,10 @@ private extension OpenVPN.Configuration {
return nil return nil
} }
return .init( return .init(
keepAlive: keepAliveInterval, keepAlive: localizedDescription(optionalStyle: .keepAlive),
reneg: renegotiatesAfter, reneg: localizedDescription(optionalStyle: .renegotiatesAfter),
randomizeEndpoint: randomizeEndpoint, randomizeEndpoint: localizedDescription(optionalStyle: .randomizeEndpoint),
randomizeHostnames: randomizeHostnames randomizeHostnames: localizedDescription(optionalStyle: .randomizeHostnames)
) )
} }
} }

View File

@ -85,7 +85,7 @@ private extension EndpointAdvancedView.WireGuardView {
builder.mtu.map { mtu in builder.mtu.map { mtu in
Section { Section {
Text(Unlocalized.Network.mtu) Text(Unlocalized.Network.mtu)
.withTrailingText(Int(mtu).localizedDescriptionAsMTU) .withTrailingText(Int(mtu).localizedDescription(style: .mtu))
} }
} }
} }

View File

@ -125,9 +125,9 @@ private extension EndpointView.WireGuardView {
Text(L10n.Endpoint.Wireguard.Items.AllowedIp.caption) Text(L10n.Endpoint.Wireguard.Items.AllowedIp.caption)
.withTrailingText($0) .withTrailingText($0)
} }
builder.keepAlive(ofPeer: peerIndex).map { builder.localizedDescription(optionalStyle: .keepAlive(peerIndex: peerIndex)).map {
Text(L10n.Global.Strings.keepalive) Text(L10n.Global.Strings.keepalive)
.withTrailingText(TimeInterval($0).localizedDescriptionAsKeepAlive) .withTrailingText($0)
} }
} header: { } header: {
Text(L10n.Endpoint.Wireguard.Items.Peer.caption) Text(L10n.Endpoint.Wireguard.Items.Peer.caption)

View File

@ -257,7 +257,7 @@ private extension NetworkSettingsView {
L10n.Global.Strings.bytes, L10n.Global.Strings.bytes,
selection: $settings.mtu.mtuBytes, selection: $settings.mtu.mtuBytes,
values: Network.MTUSettings.availableBytes, values: Network.MTUSettings.availableBytes,
description: \.localizedDescriptionAsMTU description: { $0.localizedDescription(style: .mtu) }
) )
} }
} header: { } header: {

View File

@ -130,9 +130,9 @@ private extension ProfileView.ProviderSection {
return nil return nil
} }
if currentProfile.value.providerRandomizesServer ?? false { if currentProfile.value.providerRandomizesServer ?? false {
return server.localizedCountry(withCategory: true) return server.localizedDescription(style: .countryWithCategory(withCategory: true))
} else { } else {
return server.localizedLongDescription(withCategory: true) return server.localizedDescription(style: .longWithCategory(withCategory: true))
} }
} }
@ -144,11 +144,11 @@ private extension ProfileView.ProviderSection {
} }
var currentProviderPreset: String? { var currentProviderPreset: String? {
providerManager.localizedPreset(forProfile: profile) providerManager.localizedDescription(optionalStyle: .preset(profile: profile))
} }
var lastInfrastructureUpdate: String? { var lastInfrastructureUpdate: String? {
providerManager.localizedInfrastructureUpdate(forProfile: profile) providerManager.localizedDescription(optionalStyle: .infrastructureUpdate(profile: profile))
} }
} }

View File

@ -101,13 +101,15 @@ extension ProviderLocationView {
HStack { HStack {
themeAssetsCountryImage(location.countryCode).asAssetImage themeAssetsCountryImage(location.countryCode).asAssetImage
VStack { VStack {
if let singleServer = location.onlyServer, singleServer.localizedShortDescription != nil { if let singleServer = location.onlyServer,
Text(location.localizedCountry) let shortServerDescription = singleServer.localizedDescription(optionalStyle: .short) {
Text(location.localizedDescription(style: .country))
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
Text(singleServer.localizedShortDescription ?? "") Text(shortServerDescription)
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
} else { } else {
Text(location.localizedCountry) Text(location.localizedDescription(style: .country))
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
} }
}.withTrailingCheckmark(when: location.id == selectedLocationId) }.withTrailingCheckmark(when: location.id == selectedLocationId)
@ -132,7 +134,7 @@ extension ProviderLocationView {
ScrollViewReader { scrollProxy in ScrollViewReader { scrollProxy in
List { List {
ForEach(servers) { server in ForEach(servers) { server in
Button(server.localizedShortDescriptionWithDefault) { Button(server.localizedDescription(style: .shortWithDefault)) {
selectedServer = server selectedServer = server
}.withTrailingCheckmark(when: server.id == selectedServer?.id) }.withTrailingCheckmark(when: server.id == selectedServer?.id)
} }
@ -211,7 +213,7 @@ private extension ProviderLocationView {
ServerListView( ServerListView(
location: location, location: location,
selectedServer: $selectedServer selectedServer: $selectedServer
).navigationTitle(location.localizedCountry) ).navigationTitle(location.localizedDescription(style: .country))
}, label: { }, label: {
LocationRow( LocationRow(
location: location, location: location,

View File

@ -45,10 +45,10 @@ struct VPNStatusText: View {
private extension VPNStatusText { private extension VPNStatusText {
var statusText: String { var statusText: String {
currentVPNState.localizedStatusDescription( currentVPNState.localizedDescription(style: .status(
isActiveProfile: isActiveProfile, isActiveProfile: isActiveProfile,
withErrors: true, withErrors: true,
dataCountIfAvailable: true dataCountIfAvailable: true
) ))
} }
} }

View File

@ -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 <http://www.gnu.org/licenses/>.
//
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?
}

View File

@ -25,8 +25,8 @@
import Foundation import Foundation
struct Validators { public struct Validators {
enum ValidationError: Error { public enum ValidationError: Error {
case notSet case notSet
case empty case empty
@ -44,20 +44,20 @@ struct Validators {
private static let rxWildcardDomainName = NSRegularExpression("^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.|\\*\\.)+([A-Za-z]{2,}|\\*)$") private static let rxWildcardDomainName = NSRegularExpression("^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.|\\*\\.)+([A-Za-z]{2,}|\\*)$")
static func notNil(_ string: String?) throws { public static func notNil(_ string: String?) throws {
guard string != nil else { guard string != nil else {
throw ValidationError.notSet throw ValidationError.notSet
} }
} }
static func notEmpty(_ string: String) throws { public static func notEmpty(_ string: String) throws {
let valid = string.trimmingCharacters(in: .whitespacesAndNewlines) let valid = string.trimmingCharacters(in: .whitespacesAndNewlines)
guard !valid.isEmpty else { guard !valid.isEmpty else {
throw ValidationError.empty throw ValidationError.empty
} }
} }
static func ipAddress(_ string: String) throws { public static func ipAddress(_ string: String) throws {
var sin = sockaddr_in() var sin = sockaddr_in()
var sin6 = sockaddr_in6() var sin6 = sockaddr_in6()
@ -68,25 +68,25 @@ struct Validators {
} }
} }
static func domainName(_ string: String) throws { public static func domainName(_ string: String) throws {
guard rxDomainName.numberOfMatches(in: string, options: [], range: .init(location: 0, length: string.count)) > 0 else { guard rxDomainName.numberOfMatches(in: string, options: [], range: .init(location: 0, length: string.count)) > 0 else {
throw ValidationError.domainName 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 { guard rxWildcardDomainName.numberOfMatches(in: string, options: [], range: .init(location: 0, length: string.count)) > 0 else {
throw ValidationError.wildcardDomainName throw ValidationError.wildcardDomainName
} }
} }
static func url(_ string: String) throws { public static func url(_ string: String) throws {
guard URL(string: string) != nil else { guard URL(string: string) != nil else {
throw ValidationError.url throw ValidationError.url
} }
} }
static func dnsOverTLSServerName(_ string: String) throws { public static func dnsOverTLSServerName(_ string: String) throws {
do { do {
try domainName(string) try domainName(string)
} catch { } catch {