From 0b2885461ac1f6a013dd7b23a39154c43347af4c Mon Sep 17 00:00:00 2001 From: Benjamin Erhart Date: Fri, 29 May 2026 15:41:27 +0200 Subject: [PATCH 1/9] Issue #6: Show UI to explain user's next steps to enable. Disable toggle as this cannot be used to activate DNS settings. --- dns/BlocklistOption.swift | 9 +++++++++ dns/HomeView.swift | 28 +++++++++++++++++++++++++++- dns/ViewModel.swift | 16 +++++++--------- 3 files changed, 43 insertions(+), 10 deletions(-) diff --git a/dns/BlocklistOption.swift b/dns/BlocklistOption.swift index 2afffe1..7935deb 100644 --- a/dns/BlocklistOption.swift +++ b/dns/BlocklistOption.swift @@ -32,6 +32,15 @@ enum BlocklistOption: String, CaseIterable, Identifiable { } } + var title: String { + switch self { + case .secure: + return String(format: NSLocalizedString( "%@: Malware and phishing protection", comment: ""), ViewModel.title) + case .securePlusAdblock: + return String(format: NSLocalizedString("%@: Security plus ad and tracker blocking", comment: ""), ViewModel.title) + } + } + var icon: String { switch self { case .secure: diff --git a/dns/HomeView.swift b/dns/HomeView.swift index f1df067..21dfa9b 100644 --- a/dns/HomeView.swift +++ b/dns/HomeView.swift @@ -31,11 +31,37 @@ struct HomeView: View { Toggle("", isOn: $viewModel.isDnsEnabled) .labelsHidden() + .disabled(true) .tint(.green) } .padding(.vertical, 4) } + if !viewModel.isDnsEnabled { + Section { + VStack(alignment: .leading, spacing: 4) { + Text(String(format: NSLocalizedString("To enable %@:", comment: ""), ViewModel.title)) + Text(String(format: NSLocalizedString("%1$@ Tap \"%2$@\"", comment: ""), "–", NSLocalizedString("Open Settings", comment: ""))) + Text(String(format: NSLocalizedString("%1$@ Go to General", comment: ""), "–")) + Text(String(format: NSLocalizedString("%1$@ VPN & Network", comment: ""), "–")) + Text(String(format: NSLocalizedString("%1$@ DNS", comment: ""), "–")) + Text(String(format: NSLocalizedString("%1$@ Select \"%2$@\"", comment: ""), "–", viewModel.blocklist.title)) + + Spacer() + + Button { + if let url = URL(string: UIApplication.openSettingsURLString) { + UIApplication.shared.open(url) + } + } label: { + Text(NSLocalizedString("Open Settings", comment: "")) + .frame(maxWidth: .infinity, minHeight: 32) + } + .buttonStyle(.borderedProminent) + } + } + } + // Blocklist selection Section { ForEach(BlocklistOption.allCases) { option in @@ -176,7 +202,7 @@ struct HomeView: View { }.foregroundStyle(.primary) } } - .navigationTitle("SR2® Cloud DNS") + .navigationTitle(ViewModel.title) .animation(.default, value: viewModel.isDnsEnabled) } } diff --git a/dns/ViewModel.swift b/dns/ViewModel.swift index 5b331d0..64e9782 100644 --- a/dns/ViewModel.swift +++ b/dns/ViewModel.swift @@ -12,6 +12,8 @@ import OSLog class ViewModel: NSObject, ObservableObject { + static let title = "SR2® Cloud DNS" + // MARK: Public Properties @Published @@ -29,11 +31,7 @@ class ViewModel: NSObject, ObservableObject { } @Published - var isDnsEnabled = false { - didSet { - toggleDns() - } - } + var isDnsEnabled = false @Published var summaryStatus: SummaryStatus = .pending @@ -51,6 +49,8 @@ class ViewModel: NSObject, ObservableObject { override init() { super.init() + isDnsEnabled = manager.isEnabled + isProgrammaticChange = true blocklist = Settings.blocklist @@ -60,11 +60,9 @@ class ViewModel: NSObject, ObservableObject { } catch { log.error("Error loading preferences: \(error)") - - return } - if manager.isEnabled, let settings = manager.dnsSettings { + if let settings = manager.dnsSettings { for dnsServer in BlocklistOption.allCases { if settings.servers.contains(dnsServer.ipv4) { await MainActor.run { @@ -99,7 +97,7 @@ class ViewModel: NSObject, ObservableObject { Task { if isDnsEnabled { manager.dnsSettings = blocklist.settings - manager.localizedDescription = blocklist.description + manager.localizedDescription = blocklist.title do { try await manager.saveToPreferences() From 587adf39b4900800b948f33c26fd1a19d2a83a55 Mon Sep 17 00:00:00 2001 From: Benjamin Erhart Date: Fri, 29 May 2026 15:48:16 +0200 Subject: [PATCH 2/9] Issues #7, #8: Removed UserDefaults again. NEDNSSettingsManager is our source of truth. Update automatically, when user changes blocklist. --- dns/Settings.swift | 24 -------------- dns/ViewModel.swift | 76 ++++++--------------------------------------- 2 files changed, 9 insertions(+), 91 deletions(-) delete mode 100644 dns/Settings.swift diff --git a/dns/Settings.swift b/dns/Settings.swift deleted file mode 100644 index 1d4a914..0000000 --- a/dns/Settings.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// Settings.swift -// dns -// -// Created by Benjamin Erhart on 15.04.26. -// - -import Foundation - -class Settings { - - private static let blocklistKey = "blocklist" - - private static let defaults = UserDefaults.standard - - class var blocklist: BlocklistOption { - get { - BlocklistOption(rawValue: defaults.string(forKey: blocklistKey) ?? BlocklistOption.secure.rawValue) ?? .secure - } - set { - defaults.set(newValue.rawValue, forKey: blocklistKey) - } - } -} diff --git a/dns/ViewModel.swift b/dns/ViewModel.swift index 64e9782..a79fc5f 100644 --- a/dns/ViewModel.swift +++ b/dns/ViewModel.swift @@ -19,13 +19,16 @@ class ViewModel: NSObject, ObservableObject { @Published var blocklist: BlocklistOption = .secure { didSet { - Settings.blocklist = blocklist + Task { + manager.dnsSettings = blocklist.settings + manager.localizedDescription = blocklist.title - if isDnsEnabled { - toggleDns() - } - else { - isProgrammaticChange = false + do { + try await manager.saveToPreferences() + } + catch { + log.error("Error storing preferences: \(error)") + } } } } @@ -39,8 +42,6 @@ class ViewModel: NSObject, ObservableObject { // MARK: Private Properties - private var isProgrammaticChange = false - private let manager = NEDNSSettingsManager.shared() private let log = Logger(subsystem: String(describing: ViewModel.self), category: String(describing: ViewModel.self)) @@ -51,9 +52,6 @@ class ViewModel: NSObject, ObservableObject { isDnsEnabled = manager.isEnabled - isProgrammaticChange = true - blocklist = Settings.blocklist - Task { do { try await manager.loadFromPreferences() @@ -66,10 +64,7 @@ class ViewModel: NSObject, ObservableObject { for dnsServer in BlocklistOption.allCases { if settings.servers.contains(dnsServer.ipv4) { await MainActor.run { - isProgrammaticChange = true blocklist = dnsServer - isProgrammaticChange = true - isDnsEnabled = true } break @@ -86,45 +81,6 @@ class ViewModel: NSObject, ObservableObject { // MARK: Public Methods - func toggleDns() { - guard !isProgrammaticChange else { - // Reset, so next one is recognized as coming from the user again. - isProgrammaticChange = false - - return - } - - Task { - if isDnsEnabled { - manager.dnsSettings = blocklist.settings - manager.localizedDescription = blocklist.title - - do { - try await manager.saveToPreferences() - } - catch { - log.error("Error storing preferences: \(error)") - - delayedToggle(false) - } - - if !manager.isEnabled { - delayedToggle(false) - } - } - else { - do { - try await manager.removeFromPreferences() - } - catch { - log.error("Error removing preferences: \(error)") - - delayedToggle(true) - } - } - } - } - func fetchServerStatus() async { do { let (data, _) = try await URLSession.shared.data(for: .init(url: .init(string: "https://status.sr2.uk/index.json")!)) @@ -137,18 +93,4 @@ class ViewModel: NSObject, ObservableObject { log.error("Error while checking status: \(error)") } } - - - // MARK: Private Methods - - private func delayedToggle(_ enabled: Bool) { - Task { - try? await Task.sleep(nanoseconds: 500_000_000) - - await MainActor.run { - isProgrammaticChange = true - isDnsEnabled = enabled - } - } - } } From 19b9b2a95de88bbddff86f219301f25e18028124 Mon Sep 17 00:00:00 2001 From: Benjamin Erhart Date: Fri, 29 May 2026 15:52:26 +0200 Subject: [PATCH 3/9] Issue #9: Store DNS profile, if none available, yet, so user has something to select. --- dns/ViewModel.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dns/ViewModel.swift b/dns/ViewModel.swift index a79fc5f..6595b91 100644 --- a/dns/ViewModel.swift +++ b/dns/ViewModel.swift @@ -71,6 +71,10 @@ class ViewModel: NSObject, ObservableObject { } } } + else { + // Trigger `blocklist.didSet` to store right away, so user has something to select. + blocklist = blocklist + } } Task { From a6252c2df3183a233298100261de461675fa3ae5 Mon Sep 17 00:00:00 2001 From: Benjamin Erhart Date: Fri, 29 May 2026 16:06:52 +0200 Subject: [PATCH 4/9] Issue #10: Improved DNS Settings title. --- dns/BlocklistOption.swift | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/dns/BlocklistOption.swift b/dns/BlocklistOption.swift index 7935deb..ebef560 100644 --- a/dns/BlocklistOption.swift +++ b/dns/BlocklistOption.swift @@ -33,12 +33,7 @@ enum BlocklistOption: String, CaseIterable, Identifiable { } var title: String { - switch self { - case .secure: - return String(format: NSLocalizedString( "%@: Malware and phishing protection", comment: ""), ViewModel.title) - case .securePlusAdblock: - return String(format: NSLocalizedString("%@: Security plus ad and tracker blocking", comment: ""), ViewModel.title) - } + return String(format: "%1$@: %2$@", ViewModel.title, self.rawValue) } var icon: String { From 178a625050f2dfbcbbdcceeaaa54b2fe15611c47 Mon Sep 17 00:00:00 2001 From: Benjamin Erhart Date: Fri, 29 May 2026 16:07:44 +0200 Subject: [PATCH 5/9] Issue #6: Fixed Read-out of current DNS usage state. --- dns/ViewModel.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dns/ViewModel.swift b/dns/ViewModel.swift index 6595b91..7806081 100644 --- a/dns/ViewModel.swift +++ b/dns/ViewModel.swift @@ -29,6 +29,8 @@ class ViewModel: NSObject, ObservableObject { catch { log.error("Error storing preferences: \(error)") } + + isDnsEnabled = manager.isEnabled } } } @@ -50,8 +52,6 @@ class ViewModel: NSObject, ObservableObject { override init() { super.init() - isDnsEnabled = manager.isEnabled - Task { do { try await manager.loadFromPreferences() @@ -60,6 +60,8 @@ class ViewModel: NSObject, ObservableObject { log.error("Error loading preferences: \(error)") } + isDnsEnabled = manager.isEnabled + if let settings = manager.dnsSettings { for dnsServer in BlocklistOption.allCases { if settings.servers.contains(dnsServer.ipv4) { From 3aee0db9e99d76ae75c74c7b5781e7e596a687ee Mon Sep 17 00:00:00 2001 From: Benjamin Erhart Date: Fri, 29 May 2026 16:16:20 +0200 Subject: [PATCH 6/9] Issue #6: Always reflect current state of NEDNSSettingsManager. --- dns/HomeView.swift | 10 +++++++++ dns/ViewModel.swift | 52 ++++++++++++++++++++++++--------------------- 2 files changed, 38 insertions(+), 24 deletions(-) diff --git a/dns/HomeView.swift b/dns/HomeView.swift index 21dfa9b..71542f1 100644 --- a/dns/HomeView.swift +++ b/dns/HomeView.swift @@ -3,6 +3,9 @@ import SwiftUI struct HomeView: View { + @Environment(\.scenePhase) + private var scenePhase + @EnvironmentObject private var viewModel: ViewModel @@ -204,6 +207,13 @@ struct HomeView: View { } .navigationTitle(ViewModel.title) .animation(.default, value: viewModel.isDnsEnabled) + .onChange(of: scenePhase) { _, newPhase in + if newPhase == .active { + Task { + await viewModel.refreshConfig() + } + } + } } } } diff --git a/dns/ViewModel.swift b/dns/ViewModel.swift index 7806081..b07ae51 100644 --- a/dns/ViewModel.swift +++ b/dns/ViewModel.swift @@ -53,30 +53,7 @@ class ViewModel: NSObject, ObservableObject { super.init() Task { - do { - try await manager.loadFromPreferences() - } - catch { - log.error("Error loading preferences: \(error)") - } - - isDnsEnabled = manager.isEnabled - - if let settings = manager.dnsSettings { - for dnsServer in BlocklistOption.allCases { - if settings.servers.contains(dnsServer.ipv4) { - await MainActor.run { - blocklist = dnsServer - } - - break - } - } - } - else { - // Trigger `blocklist.didSet` to store right away, so user has something to select. - blocklist = blocklist - } + await refreshConfig() } Task { @@ -87,6 +64,33 @@ class ViewModel: NSObject, ObservableObject { // MARK: Public Methods + func refreshConfig() async { + do { + try await manager.loadFromPreferences() + } + catch { + log.error("Error loading preferences: \(error)") + } + + isDnsEnabled = manager.isEnabled + + if let settings = manager.dnsSettings { + for dnsServer in BlocklistOption.allCases { + if settings.servers.contains(dnsServer.ipv4) { + await MainActor.run { + blocklist = dnsServer + } + + break + } + } + } + else { + // Trigger `blocklist.didSet` to store right away, so user has something to select. + blocklist = blocklist + } + } + func fetchServerStatus() async { do { let (data, _) = try await URLSession.shared.data(for: .init(url: .init(string: "https://status.sr2.uk/index.json")!)) From ddcf2070f7a8f03a796bc150849eb3a51fa1aad1 Mon Sep 17 00:00:00 2001 From: Benjamin Erhart Date: Fri, 29 May 2026 16:54:21 +0200 Subject: [PATCH 7/9] Issue #11: Switched to old-school localization so we can leverage Weblate. --- .bartycrouch.toml | 23 ++ dns.xcodeproj/project.pbxproj | 32 ++- dns/BlockedCount.swift | 4 +- dns/BlocklistOption.swift | 16 +- dns/BlocklistRow.swift | 2 +- dns/HomeView.swift | 30 +- dns/Localizable.xcstrings | 263 ------------------ .../ar.xcloc/Localized Contents/ar.xliff | 7 - .../en.xcloc/Localized Contents/en.xliff | 147 ---------- .../Source Contents/dns/Localizable.xcstrings | 263 ------------------ .../dns/dns-InfoPlist.xcstrings | 18 -- dns/Localizations/en.xcloc/contents.json | 12 - .../fa.xcloc/Localized Contents/fa.xliff | 7 - .../fr.xcloc/Localized Contents/fr.xliff | 7 - .../prs.xcloc/Localized Contents/prs.xliff | 7 - .../ps.xcloc/Localized Contents/ps.xliff | 7 - .../ro.xcloc/Localized Contents/ro.xliff | 7 - .../ru.xcloc/Localized Contents/ru.xliff | 7 - .../tok.xcloc/Localized Contents/tok.xliff | 7 - dns/SummaryStatus.swift | 10 +- dns/de.lproj/Localizable.strings | 98 +++++++ dns/en.lproj/Localizable.strings | 98 +++++++ 22 files changed, 285 insertions(+), 787 deletions(-) create mode 100644 .bartycrouch.toml delete mode 100644 dns/Localizable.xcstrings delete mode 100644 dns/Localizations/ar.xcloc/Localized Contents/ar.xliff delete mode 100644 dns/Localizations/en.xcloc/Localized Contents/en.xliff delete mode 100644 dns/Localizations/en.xcloc/Source Contents/dns/Localizable.xcstrings delete mode 100644 dns/Localizations/en.xcloc/Source Contents/dns/dns-InfoPlist.xcstrings delete mode 100644 dns/Localizations/en.xcloc/contents.json delete mode 100644 dns/Localizations/fa.xcloc/Localized Contents/fa.xliff delete mode 100644 dns/Localizations/fr.xcloc/Localized Contents/fr.xliff delete mode 100644 dns/Localizations/prs.xcloc/Localized Contents/prs.xliff delete mode 100644 dns/Localizations/ps.xcloc/Localized Contents/ps.xliff delete mode 100644 dns/Localizations/ro.xcloc/Localized Contents/ro.xliff delete mode 100644 dns/Localizations/ru.xcloc/Localized Contents/ru.xliff delete mode 100644 dns/Localizations/tok.xcloc/Localized Contents/tok.xliff create mode 100644 dns/de.lproj/Localizable.strings create mode 100644 dns/en.lproj/Localizable.strings diff --git a/.bartycrouch.toml b/.bartycrouch.toml new file mode 100644 index 0000000..7da2af3 --- /dev/null +++ b/.bartycrouch.toml @@ -0,0 +1,23 @@ +[update] +tasks = ["code", "normalize"] + +[update.code] +codePaths = ["."] +localizablePaths = ["dns"] +defaultToKeys = true +additive = false +unstripped = false +plistArguments = false +ignoreKeys = ["#bartycrouch-ignore!", "#bc-ignore!", "#i!"] + +[update.normalize] +paths = ["dns"] +subpathsToIgnore = ["InfoPlist.strings"] +sourceLocale = "en" +harmonizeWithSource = true +sortByKeys = true + +[lint] +paths = ["dns"] +duplicateKeys = true +emptyValues = true diff --git a/dns.xcodeproj/project.pbxproj b/dns.xcodeproj/project.pbxproj index bdf3b2a..651cb4e 100644 --- a/dns.xcodeproj/project.pbxproj +++ b/dns.xcodeproj/project.pbxproj @@ -36,6 +36,7 @@ /* Begin PBXFileReference section */ 069DCCFA2F8C0DCE00F1EB16 /* dns.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = dns.app; sourceTree = BUILT_PRODUCTS_DIR; }; + A051B50B2FC9DA9100EACDC0 /* .bartycrouch.toml */ = {isa = PBXFileReference; lastKnownFileType = text; path = .bartycrouch.toml; sourceTree = ""; }; A06A74772F8E95410093A9E4 /* .gitignore */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitignore; sourceTree = ""; }; A06A74782F8E95410093A9E4 /* LICENCE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENCE; sourceTree = ""; }; A06A74792F8E95410093A9E4 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; @@ -85,6 +86,7 @@ A06A74772F8E95410093A9E4 /* .gitignore */, A06A74782F8E95410093A9E4 /* LICENCE */, A06A74792F8E95410093A9E4 /* README.md */, + A051B50B2FC9DA9100EACDC0 /* .bartycrouch.toml */, 069DCCFC2F8C0DCE00F1EB16 /* dns */, A082640D2FC718790077B227 /* Stickers */, 069DCCFB2F8C0DCE00F1EB16 /* Products */, @@ -107,6 +109,7 @@ isa = PBXNativeTarget; buildConfigurationList = 069DCD052F8C0DCE00F1EB16 /* Build configuration list for PBXNativeTarget "dns" */; buildPhases = ( + A051B50A2FC9DA4300EACDC0 /* BartyCrouch */, 069DCCF62F8C0DCE00F1EB16 /* Sources */, 069DCCF72F8C0DCE00F1EB16 /* Frameworks */, 069DCCF82F8C0DCE00F1EB16 /* Resources */, @@ -155,7 +158,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 2640; - LastUpgradeCheck = 2640; + LastUpgradeCheck = 2650; TargetAttributes = { 069DCCF92F8C0DCE00F1EB16 = { CreatedOnToolsVersion = 26.4; @@ -171,6 +174,7 @@ knownRegions = ( en, Base, + de, ); mainGroup = 069DCCF12F8C0DCD00F1EB16; minimizedProjectReferenceProxies = 1; @@ -202,6 +206,28 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + A051B50A2FC9DA4300EACDC0 /* BartyCrouch */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = BartyCrouch; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if [ -f /opt/homebrew/bin/bartycrouch ]; then\n /opt/homebrew/bin/bartycrouch update -x\n /opt/homebrew/bin/bartycrouch lint -x\nelse\n echo \"warning: BartyCrouch not installed. Download it form https://github.com/Flinesoft/BartyCrouch\"\nfi\n"; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ 069DCCF62F8C0DCE00F1EB16 /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -226,6 +252,7 @@ buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; @@ -290,6 +317,7 @@ buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; @@ -353,6 +381,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleDisplayName = "SR2 Cloud DNS"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; @@ -392,6 +421,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleDisplayName = "SR2 Cloud DNS"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; diff --git a/dns/BlockedCount.swift b/dns/BlockedCount.swift index 24df33f..892191e 100644 --- a/dns/BlockedCount.swift +++ b/dns/BlockedCount.swift @@ -63,11 +63,11 @@ struct BlockedCount: View { txtRecord = count } else { - txtRecord = "Error" + txtRecord = NSLocalizedString("Error", comment: "") } } catch { - txtRecord = "Error" + txtRecord = NSLocalizedString("Error", comment: "") } } } diff --git a/dns/BlocklistOption.swift b/dns/BlocklistOption.swift index ebef560..c67c349 100644 --- a/dns/BlocklistOption.swift +++ b/dns/BlocklistOption.swift @@ -12,7 +12,15 @@ enum BlocklistOption: String, CaseIterable, Identifiable { case secure = "Secure" case securePlusAdblock = "Secure + Adblock" - var id: String { rawValue } + var id: String { + switch self { + case .secure: + return NSLocalizedString("Secure", comment: "") + + case .securePlusAdblock: + return NSLocalizedString("Secure + Adblock", comment: "") + } + } var enabled: Bool { switch self { @@ -26,14 +34,14 @@ enum BlocklistOption: String, CaseIterable, Identifiable { var description: String { switch self { case .secure: - return "Malware and phishing protection" + return NSLocalizedString("Malware and phishing protection", comment: "") case .securePlusAdblock: - return "Security plus ad and tracker blocking" + return NSLocalizedString("Security plus ad and tracker blocking", comment: "") } } var title: String { - return String(format: "%1$@: %2$@", ViewModel.title, self.rawValue) + return String(format: "%1$@: %2$@", ViewModel.title, id) } var icon: String { diff --git a/dns/BlocklistRow.swift b/dns/BlocklistRow.swift index 8f0b64d..9cdec60 100644 --- a/dns/BlocklistRow.swift +++ b/dns/BlocklistRow.swift @@ -17,7 +17,7 @@ struct BlocklistRow: View { } VStack(alignment: .leading, spacing: 4) { - Text(option.id + " " + (option.enabled ? "" : "(Coming Soon)")) + Text(option.enabled ? option.id : String(format: NSLocalizedString("%@ (Coming Soon)", comment: ""), option.id)) .font(.body) .fontWeight(isSelected ? .semibold : .regular) diff --git a/dns/HomeView.swift b/dns/HomeView.swift index 71542f1..f9295ea 100644 --- a/dns/HomeView.swift +++ b/dns/HomeView.swift @@ -23,9 +23,9 @@ struct HomeView: View { Section { HStack { VStack(alignment: .leading, spacing: 4) { - Text("DNS Protection") + Text(NSLocalizedString("DNS Protection", comment: "")) .font(.headline) - Text(viewModel.isDnsEnabled ? "Active" : "Inactive") + Text(viewModel.isDnsEnabled ? NSLocalizedString("Active", comment: "") : NSLocalizedString("Inactive", comment: "")) .font(.caption) .foregroundStyle(viewModel.isDnsEnabled ? .green : .secondary) } @@ -82,24 +82,24 @@ struct HomeView: View { .opacity(option.enabled ? 1 : 0.6) } } header: { - Text("Blocklist") + Text(NSLocalizedString("Blocklist", comment: "")) } footer: { - Text("Select the level of protection for your DNS queries") + Text(NSLocalizedString("Select the level of protection for your DNS queries", comment: "")) } // Status section if viewModel.isDnsEnabled { Section { HStack { - Label("Status", systemImage: "checkmark.circle.fill") + Label(NSLocalizedString("Status", comment: ""), systemImage: "checkmark.circle.fill") .foregroundStyle(.green) Spacer() - Text("Connected") + Text(NSLocalizedString("Connected", comment: "")) .foregroundStyle(.secondary) } HStack { - Label("Server", systemImage: "server.rack") + Label(NSLocalizedString("Server", comment: ""), systemImage: "server.rack") Spacer() Text(viewModel.blocklist.server) .foregroundStyle(.secondary) @@ -123,13 +123,13 @@ struct HomeView: View { } HStack { - Label("Domains in blocklist", systemImage: "xmark.shield.fill") + Label(NSLocalizedString("Domains in blocklist", comment: ""), systemImage: "xmark.shield.fill") Spacer() BlockedCount() .foregroundStyle(.secondary) } } header: { - Text("Connection Details") + Text(NSLocalizedString("Connection Details", comment: "")) } } @@ -138,7 +138,7 @@ struct HomeView: View { Link(destination: falsePositiveURL) { HStack { Label { - Text("Report False Positive") + Text(NSLocalizedString("Report False Positive", comment: "")) } icon: { Image(systemName: "exclamationmark.bubble") .foregroundStyle(.orange) @@ -152,7 +152,7 @@ struct HomeView: View { } }.foregroundStyle(.primary) } footer: { - Text("Submit incorrectly blocked domains for review") + Text(NSLocalizedString("Submit incorrectly blocked domains for review", comment: "")) } // Service status section @@ -164,7 +164,7 @@ struct HomeView: View { .frame(width: 12, height: 12) VStack(alignment: .leading, spacing: 4) { - Text("Service Status") + Text(NSLocalizedString("Service Status", comment: "")) .font(.headline) Text(viewModel.summaryStatus.description) .font(.caption) @@ -183,7 +183,7 @@ struct HomeView: View { Link(destination: tosURL) { HStack(spacing: 12) { Image(systemName: "doc.text") - Text("Terms of Service") + Text(NSLocalizedString("Terms of Service", comment: "")) Spacer() Image(systemName: "arrow.up.right.square") .font(.caption) @@ -195,7 +195,7 @@ struct HomeView: View { Link(destination: privacyPolicyURL) { HStack(spacing: 12) { Image(systemName: "doc.text") - Text("Privacy Policy") + Text(NSLocalizedString("Privacy Policy", comment: "")) Spacer() Image(systemName: "arrow.up.right.square") .font(.caption) @@ -207,7 +207,7 @@ struct HomeView: View { } .navigationTitle(ViewModel.title) .animation(.default, value: viewModel.isDnsEnabled) - .onChange(of: scenePhase) { _, newPhase in + .onChange(of: scenePhase) { newPhase in if newPhase == .active { Task { await viewModel.refreshConfig() diff --git a/dns/Localizable.xcstrings b/dns/Localizable.xcstrings deleted file mode 100644 index aeb6242..0000000 --- a/dns/Localizable.xcstrings +++ /dev/null @@ -1,263 +0,0 @@ -{ - "sourceLanguage" : "en", - "strings" : { - "" : { - - }, - "(Coming Soon)" : { - "comment" : "Indicates that this feature is not yet implemented but will be soon", - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "(Coming Soon)" - } - } - } - }, - "Active" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Active" - } - } - } - }, - "Blocklist" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Blocklist" - } - } - } - }, - "Connected" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Connected" - } - } - } - }, - "Connection Details" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Connection Details" - } - } - } - }, - "DNS Protection" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "DNS Protection" - } - } - } - }, - "Domains in blocklist" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Domains in blocklist" - } - } - } - }, - "Inactive" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Inactive" - } - } - } - }, - "IPv4" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "IPv4" - } - } - } - }, - "IPv6" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "IPv6" - } - } - } - }, - "Malware and phishing protection" : { - "comment" : "Description of the blocklist contents", - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Malware and phishing protection" - } - } - } - }, - "No issues detected" : { - "comment" : "No current issues detected with the service", - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No issues detected" - } - } - } - }, - "Privacy Policy" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Privacy Policy" - } - } - } - }, - "Report False Positive" : { - "comment" : "Link to report that a domain name has been incorrectly blocked", - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Report False Positive" - } - } - } - }, - "Secure" : { - "comment" : "Name of the blocklist that only includes malware and security threats", - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Secure" - } - } - } - }, - "Secure + Adblock" : { - "comment" : "Name of the blocklist that contains “Secure” plus ad blocking", - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Secure + Adblock" - } - } - } - }, - "Security plus ad and tracker blocking" : { - "comment" : "Description of the blocklist contents", - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Security plus ad and tracker blocking" - } - } - } - }, - "Select the level of protection for your DNS queries" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Select the level of protection for your DNS queries" - } - } - } - }, - "Server" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Server" - } - } - } - }, - "Service Status" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Service Status" - } - } - } - }, - "SR2® Cloud DNS" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "SR2® Cloud DNS" - } - } - } - }, - "Status" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Status" - } - } - } - }, - "Submit incorrectly blocked domains for review" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Submit incorrectly blocked domains for review" - } - } - } - }, - "Terms of Service" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Terms of Service" - } - } - } - } - }, - "version" : "1.2" -} \ No newline at end of file diff --git a/dns/Localizations/ar.xcloc/Localized Contents/ar.xliff b/dns/Localizations/ar.xcloc/Localized Contents/ar.xliff deleted file mode 100644 index 93abab9..0000000 --- a/dns/Localizations/ar.xcloc/Localized Contents/ar.xliff +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/dns/Localizations/en.xcloc/Localized Contents/en.xliff b/dns/Localizations/en.xcloc/Localized Contents/en.xliff deleted file mode 100644 index ad7a3b4..0000000 --- a/dns/Localizations/en.xcloc/Localized Contents/en.xliff +++ /dev/null @@ -1,147 +0,0 @@ - - - -
- -
- - - dns - dns - Bundle name - - -
- -
- -
- - - - - - - - (Coming Soon) - (Coming Soon) - Indicates that this feature is not yet implemented but will be soon - - - Active - Active - - - - Blocklist - Blocklist - - - - Connected - Connected - - - - Connection Details - Connection Details - - - - DNS Protection - DNS Protection - - - - Domains in blocklist - Domains in blocklist - - - - IPv4 - IPv4 - - - - IPv6 - IPv6 - - - - Inactive - Inactive - - - - Malware and phishing protection - Malware and phishing protection - Description of the blocklist contents - - - No issues detected - No issues detected - No current issues detected with the service - - - Privacy Policy - Privacy Policy - - - - Report False Positive - Report False Positive - Link to report that a domain name has been incorrectly blocked - - - SR2® Cloud DNS - SR2® Cloud DNS - - - - Secure - Secure - Name of the blocklist that only includes malware and security threats - - - Secure + Adblock - Secure + Adblock - Name of the blocklist that contains “Secure” plus ad blocking - - - Security plus ad and tracker blocking - Security plus ad and tracker blocking - Description of the blocklist contents - - - Select the level of protection for your DNS queries - Select the level of protection for your DNS queries - - - - Server - Server - - - - Service Status - Service Status - - - - Status - Status - - - - Submit incorrectly blocked domains for review - Submit incorrectly blocked domains for review - - - - Terms of Service - Terms of Service - - - -
-
diff --git a/dns/Localizations/en.xcloc/Source Contents/dns/Localizable.xcstrings b/dns/Localizations/en.xcloc/Source Contents/dns/Localizable.xcstrings deleted file mode 100644 index aeb6242..0000000 --- a/dns/Localizations/en.xcloc/Source Contents/dns/Localizable.xcstrings +++ /dev/null @@ -1,263 +0,0 @@ -{ - "sourceLanguage" : "en", - "strings" : { - "" : { - - }, - "(Coming Soon)" : { - "comment" : "Indicates that this feature is not yet implemented but will be soon", - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "(Coming Soon)" - } - } - } - }, - "Active" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Active" - } - } - } - }, - "Blocklist" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Blocklist" - } - } - } - }, - "Connected" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Connected" - } - } - } - }, - "Connection Details" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Connection Details" - } - } - } - }, - "DNS Protection" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "DNS Protection" - } - } - } - }, - "Domains in blocklist" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Domains in blocklist" - } - } - } - }, - "Inactive" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Inactive" - } - } - } - }, - "IPv4" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "IPv4" - } - } - } - }, - "IPv6" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "IPv6" - } - } - } - }, - "Malware and phishing protection" : { - "comment" : "Description of the blocklist contents", - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Malware and phishing protection" - } - } - } - }, - "No issues detected" : { - "comment" : "No current issues detected with the service", - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No issues detected" - } - } - } - }, - "Privacy Policy" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Privacy Policy" - } - } - } - }, - "Report False Positive" : { - "comment" : "Link to report that a domain name has been incorrectly blocked", - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Report False Positive" - } - } - } - }, - "Secure" : { - "comment" : "Name of the blocklist that only includes malware and security threats", - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Secure" - } - } - } - }, - "Secure + Adblock" : { - "comment" : "Name of the blocklist that contains “Secure” plus ad blocking", - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Secure + Adblock" - } - } - } - }, - "Security plus ad and tracker blocking" : { - "comment" : "Description of the blocklist contents", - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Security plus ad and tracker blocking" - } - } - } - }, - "Select the level of protection for your DNS queries" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Select the level of protection for your DNS queries" - } - } - } - }, - "Server" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Server" - } - } - } - }, - "Service Status" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Service Status" - } - } - } - }, - "SR2® Cloud DNS" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "SR2® Cloud DNS" - } - } - } - }, - "Status" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Status" - } - } - } - }, - "Submit incorrectly blocked domains for review" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Submit incorrectly blocked domains for review" - } - } - } - }, - "Terms of Service" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Terms of Service" - } - } - } - } - }, - "version" : "1.2" -} \ No newline at end of file diff --git a/dns/Localizations/en.xcloc/Source Contents/dns/dns-InfoPlist.xcstrings b/dns/Localizations/en.xcloc/Source Contents/dns/dns-InfoPlist.xcstrings deleted file mode 100644 index a3786f7..0000000 --- a/dns/Localizations/en.xcloc/Source Contents/dns/dns-InfoPlist.xcstrings +++ /dev/null @@ -1,18 +0,0 @@ -{ - "sourceLanguage" : "en", - "strings" : { - "CFBundleName" : { - "comment" : "Bundle name", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "dns" - } - } - } - } - }, - "version" : "1.2" -} \ No newline at end of file diff --git a/dns/Localizations/en.xcloc/contents.json b/dns/Localizations/en.xcloc/contents.json deleted file mode 100644 index 2423001..0000000 --- a/dns/Localizations/en.xcloc/contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "developmentRegion" : "en", - "project" : "dns.xcodeproj", - "targetLocale" : "en", - "toolInfo" : { - "toolBuildNumber" : "17E192", - "toolID" : "com.apple.dt.xcode", - "toolName" : "Xcode", - "toolVersion" : "26.4" - }, - "version" : "1.0" -} \ No newline at end of file diff --git a/dns/Localizations/fa.xcloc/Localized Contents/fa.xliff b/dns/Localizations/fa.xcloc/Localized Contents/fa.xliff deleted file mode 100644 index 93abab9..0000000 --- a/dns/Localizations/fa.xcloc/Localized Contents/fa.xliff +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/dns/Localizations/fr.xcloc/Localized Contents/fr.xliff b/dns/Localizations/fr.xcloc/Localized Contents/fr.xliff deleted file mode 100644 index 93abab9..0000000 --- a/dns/Localizations/fr.xcloc/Localized Contents/fr.xliff +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/dns/Localizations/prs.xcloc/Localized Contents/prs.xliff b/dns/Localizations/prs.xcloc/Localized Contents/prs.xliff deleted file mode 100644 index 93abab9..0000000 --- a/dns/Localizations/prs.xcloc/Localized Contents/prs.xliff +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/dns/Localizations/ps.xcloc/Localized Contents/ps.xliff b/dns/Localizations/ps.xcloc/Localized Contents/ps.xliff deleted file mode 100644 index 93abab9..0000000 --- a/dns/Localizations/ps.xcloc/Localized Contents/ps.xliff +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/dns/Localizations/ro.xcloc/Localized Contents/ro.xliff b/dns/Localizations/ro.xcloc/Localized Contents/ro.xliff deleted file mode 100644 index 93abab9..0000000 --- a/dns/Localizations/ro.xcloc/Localized Contents/ro.xliff +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/dns/Localizations/ru.xcloc/Localized Contents/ru.xliff b/dns/Localizations/ru.xcloc/Localized Contents/ru.xliff deleted file mode 100644 index 93abab9..0000000 --- a/dns/Localizations/ru.xcloc/Localized Contents/ru.xliff +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/dns/Localizations/tok.xcloc/Localized Contents/tok.xliff b/dns/Localizations/tok.xcloc/Localized Contents/tok.xliff deleted file mode 100644 index 93abab9..0000000 --- a/dns/Localizations/tok.xcloc/Localized Contents/tok.xliff +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/dns/SummaryStatus.swift b/dns/SummaryStatus.swift index 971ba0e..5cdc15f 100644 --- a/dns/SummaryStatus.swift +++ b/dns/SummaryStatus.swift @@ -23,19 +23,19 @@ enum SummaryStatus: String, Codable, CustomStringConvertible { var description: String { switch self { case .pending: - return "Fetching service status" + return NSLocalizedString("Fetching service status", comment: "") case .ok: - return "No issues detected" + return NSLocalizedString("No issues detected", comment: "") case .notice: - return "In maintenance" + return NSLocalizedString("In maintenance", comment: "") case .disrupted: - return "Service disruption" + return NSLocalizedString("Service disruption", comment: "") case .down: - return "Service down" + return NSLocalizedString("Service down", comment: "") } } diff --git a/dns/de.lproj/Localizable.strings b/dns/de.lproj/Localizable.strings new file mode 100644 index 0000000..01a3d6e --- /dev/null +++ b/dns/de.lproj/Localizable.strings @@ -0,0 +1,98 @@ +/* No comment provided by engineer. */ +"%1$@ DNS" = "%1$@ DNS"; + +/* No comment provided by engineer. */ +"%1$@ Go to General" = "%1$@ Gehe zu Allgemein"; + +/* No comment provided by engineer. */ +"%1$@ Select \"%2$@\"" = "%1$@ Wähle \"%2$@\""; + +/* No comment provided by engineer. */ +"%1$@ Tap \"%2$@\"" = "%1$@ Tippe auf \"%2$@\""; + +/* No comment provided by engineer. */ +"%1$@ VPN & Network" = "%1$@ VPN und Geräteverwaltung"; + +/* No comment provided by engineer. */ +"%@ (Coming Soon)" = "%@ (bald)"; + +/* No comment provided by engineer. */ +"Active" = "Aktiv"; + +/* No comment provided by engineer. */ +"Blocklist" = "Sperrliste"; + +/* No comment provided by engineer. */ +"Connected" = "Verbunden"; + +/* No comment provided by engineer. */ +"Connection Details" = "Verbindungsdetails"; + +/* No comment provided by engineer. */ +"DNS Protection" = "DNS–Schutz"; + +/* No comment provided by engineer. */ +"Domains in blocklist" = "Domains in Sperrliste"; + +/* No comment provided by engineer. */ +"Error" = "Fehler"; + +/* No comment provided by engineer. */ +"Fetching service status" = "Servicestatus ermitteln"; + +/* No comment provided by engineer. */ +"In maintenance" = "Wartung"; + +/* No comment provided by engineer. */ +"Inactive" = "Inaktiv"; + +/* No comment provided by engineer. */ +"Malware and phishing protection" = "Malware- und Phishingschutz"; + +/* No comment provided by engineer. */ +"No issues detected" = "Keine Probleme erkannt"; + +/* No comment provided by engineer. */ +"Open Settings" = "Öffne Einstellungen"; + +/* No comment provided by engineer. */ +"Privacy Policy" = "Datenschutzerklärung"; + +/* No comment provided by engineer. */ +"Report False Positive" = "Berichte über falsch-positive Sperre"; + +/* No comment provided by engineer. */ +"Secure" = "Sicherheit"; + +/* No comment provided by engineer. */ +"Secure + Adblock" = "Sicherheit + Werbeblocker"; + +/* No comment provided by engineer. */ +"Security plus ad and tracker blocking" = "Sicherheit und Werbe- und Trackerschutz"; + +/* No comment provided by engineer. */ +"Select the level of protection for your DNS queries" = "Wähle das Schutzniveau für Deine DNS-Anfragen"; + +/* No comment provided by engineer. */ +"Server" = "Server"; + +/* No comment provided by engineer. */ +"Service disruption" = "Serviceunterbrechung"; + +/* No comment provided by engineer. */ +"Service down" = "Service unerreichbar"; + +/* No comment provided by engineer. */ +"Service Status" = "Servicestatus"; + +/* No comment provided by engineer. */ +"Status" = "Status"; + +/* No comment provided by engineer. */ +"Submit incorrectly blocked domains for review" = "Falsch blockierte Domains zur Überprüfung einreichen"; + +/* No comment provided by engineer. */ +"Terms of Service" = "Geschäftsbedingungen"; + +/* No comment provided by engineer. */ +"To enable %@:" = "Um %@ zu aktivieren:"; diff --git a/dns/en.lproj/Localizable.strings b/dns/en.lproj/Localizable.strings new file mode 100644 index 0000000..69eebe5 --- /dev/null +++ b/dns/en.lproj/Localizable.strings @@ -0,0 +1,98 @@ +/* No comment provided by engineer. */ +"%1$@ DNS" = "%1$@ DNS"; + +/* No comment provided by engineer. */ +"%1$@ Go to General" = "%1$@ Go to General"; + +/* No comment provided by engineer. */ +"%1$@ Select \"%2$@\"" = "%1$@ Select \"%2$@\""; + +/* No comment provided by engineer. */ +"%1$@ Tap \"%2$@\"" = "%1$@ Tap \"%2$@\""; + +/* No comment provided by engineer. */ +"%1$@ VPN & Network" = "%1$@ VPN & Network"; + +/* No comment provided by engineer. */ +"%@ (Coming Soon)" = "%@ (Coming Soon)"; + +/* No comment provided by engineer. */ +"Active" = "Active"; + +/* No comment provided by engineer. */ +"Blocklist" = "Blocklist"; + +/* No comment provided by engineer. */ +"Connected" = "Connected"; + +/* No comment provided by engineer. */ +"Connection Details" = "Connection Details"; + +/* No comment provided by engineer. */ +"DNS Protection" = "DNS Protection"; + +/* No comment provided by engineer. */ +"Domains in blocklist" = "Domains in blocklist"; + +/* No comment provided by engineer. */ +"Error" = "Error"; + +/* No comment provided by engineer. */ +"Fetching service status" = "Fetching service status"; + +/* No comment provided by engineer. */ +"In maintenance" = "In maintenance"; + +/* No comment provided by engineer. */ +"Inactive" = "Inactive"; + +/* No comment provided by engineer. */ +"Malware and phishing protection" = "Malware and phishing protection"; + +/* No comment provided by engineer. */ +"No issues detected" = "No issues detected"; + +/* No comment provided by engineer. */ +"Open Settings" = "Open Settings"; + +/* No comment provided by engineer. */ +"Privacy Policy" = "Privacy Policy"; + +/* No comment provided by engineer. */ +"Report False Positive" = "Report False Positive"; + +/* No comment provided by engineer. */ +"Secure" = "Secure"; + +/* No comment provided by engineer. */ +"Secure + Adblock" = "Secure + Adblock"; + +/* No comment provided by engineer. */ +"Security plus ad and tracker blocking" = "Security plus ad and tracker blocking"; + +/* No comment provided by engineer. */ +"Select the level of protection for your DNS queries" = "Select the level of protection for your DNS queries"; + +/* No comment provided by engineer. */ +"Server" = "Server"; + +/* No comment provided by engineer. */ +"Service disruption" = "Service disruption"; + +/* No comment provided by engineer. */ +"Service down" = "Service down"; + +/* No comment provided by engineer. */ +"Service Status" = "Service Status"; + +/* No comment provided by engineer. */ +"Status" = "Status"; + +/* No comment provided by engineer. */ +"Submit incorrectly blocked domains for review" = "Submit incorrectly blocked domains for review"; + +/* No comment provided by engineer. */ +"Terms of Service" = "Terms of Service"; + +/* No comment provided by engineer. */ +"To enable %@:" = "To enable %@:"; From 23952582e6f0ffa491b7d791e7e63272b8265fa0 Mon Sep 17 00:00:00 2001 From: Benjamin Erhart Date: Fri, 29 May 2026 17:25:29 +0200 Subject: [PATCH 8/9] Issue #12: Use proper DNS library instead of handcrafted DNS query to fetch statistics. --- dns.xcodeproj/project.pbxproj | 25 +++++++++ .../xcshareddata/swiftpm/Package.resolved | 15 ++++++ dns/BlockedCount.swift | 54 ++++++++----------- 3 files changed, 62 insertions(+), 32 deletions(-) create mode 100644 dns.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/dns.xcodeproj/project.pbxproj b/dns.xcodeproj/project.pbxproj index 651cb4e..5eb3ebc 100644 --- a/dns.xcodeproj/project.pbxproj +++ b/dns.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + A051B5122FC9E0D700EACDC0 /* AsyncDNSResolver in Frameworks */ = {isa = PBXBuildFile; productRef = A051B5112FC9E0D700EACDC0 /* AsyncDNSResolver */; }; A08264132FC718790077B227 /* Stickers.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = A082640C2FC718790077B227 /* Stickers.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ @@ -74,6 +75,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + A051B5122FC9E0D700EACDC0 /* AsyncDNSResolver in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -125,6 +127,7 @@ ); name = dns; packageProductDependencies = ( + A051B5112FC9E0D700EACDC0 /* AsyncDNSResolver */, ); productName = dns; productReference = 069DCCFA2F8C0DCE00F1EB16 /* dns.app */; @@ -178,6 +181,9 @@ ); mainGroup = 069DCCF12F8C0DCD00F1EB16; minimizedProjectReferenceProxies = 1; + packageReferences = ( + A051B5102FC9E0D700EACDC0 /* XCRemoteSwiftPackageReference "swift-async-dns-resolver" */, + ); preferredProjectObjectVersion = 77; productRefGroup = 069DCCFB2F8C0DCE00F1EB16 /* Products */; projectDirPath = ""; @@ -529,6 +535,25 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + A051B5102FC9E0D700EACDC0 /* XCRemoteSwiftPackageReference "swift-async-dns-resolver" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/apple/swift-async-dns-resolver/"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.7.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + A051B5112FC9E0D700EACDC0 /* AsyncDNSResolver */ = { + isa = XCSwiftPackageProductDependency; + package = A051B5102FC9E0D700EACDC0 /* XCRemoteSwiftPackageReference "swift-async-dns-resolver" */; + productName = AsyncDNSResolver; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 069DCCF22F8C0DCD00F1EB16 /* Project object */; } diff --git a/dns.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/dns.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..6e3a053 --- /dev/null +++ b/dns.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "8d197ac71cfa33d9e3396085f37fbee66795a894a6d74e24ff28bd887e01b9f2", + "pins" : [ + { + "identity" : "swift-async-dns-resolver", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-async-dns-resolver/", + "state" : { + "revision" : "9900d9b4113427a170d5f26075c2128be94984df", + "version" : "0.7.0" + } + } + ], + "version" : 3 +} diff --git a/dns/BlockedCount.swift b/dns/BlockedCount.swift index 892191e..3957fdf 100644 --- a/dns/BlockedCount.swift +++ b/dns/BlockedCount.swift @@ -1,9 +1,13 @@ import SwiftUI import Foundation +import AsyncDNSResolver struct BlockedCount: View { + @Environment(\.scenePhase) + private var scenePhase + @EnvironmentObject private var viewModel: ViewModel @@ -21,46 +25,32 @@ struct BlockedCount: View { var body: some View { Text(txtRecord) .onAppear { - fetchTXTRecord() + fetchTxtRecord() } .onChange(of: viewModel.blocklist) { _ in - fetchTXTRecord() + fetchTxtRecord() + } + .onChange(of: scenePhase) { newPhase in + if newPhase == .active { + fetchTxtRecord() + } } } - func parseResponse(data: Data) -> String? { - // This is a DNS wire format response and we make a lot of assumptions - // It is not critical functionality so just let it fail if it fails - - guard data.count > Self.startIndex else { - return nil - } - - // Find the first space character (ASCII 32). - guard let endIndex = data.suffix(from: Self.startIndex).firstIndex(of: 32) else { - return nil - } - - guard let numberString = String(data: data[Self.startIndex ..< endIndex], encoding: .utf8) - else { - return nil - } - - return Self.formatter.string(for: Int(numberString)) - } - - func fetchTXTRecord() { - let dohURL = URL(string: "https://\(viewModel.blocklist.server)/dns-query?dns=DoQBAAABAAAAAAAABXN0YXRzB2ludmFsaWQAABAAAQ")! - let request = URLRequest(url: dohURL) - - txtRecord = "…" - + private func fetchTxtRecord() { Task { do { - let (data, _) = try await URLSession.shared.data(for: request) + let resolver = try AsyncDNSResolver() - if let count = parseResponse(data: data) { - txtRecord = count + let records = try await resolver.queryTXT(name: "stats.invalid") + + let text = records.map { $0.txt }.joined() + + if let number = text.split(separator: " ").first, + let intValue = Int(number), + let formatted = Self.formatter.string(for: intValue) + { + txtRecord = formatted } else { txtRecord = NSLocalizedString("Error", comment: "") From d3b94aff2cc88a533922e1b5ae452c682c5a0408 Mon Sep 17 00:00:00 2001 From: Benjamin Erhart Date: Fri, 29 May 2026 17:44:01 +0200 Subject: [PATCH 9/9] Switch to manual signing configuration via a Config.xcconfig file to reduce interference from different developers. --- Config.xcconfig | 17 +++++++++++++++ dns.xcodeproj/project.pbxproj | 40 ++++++++++++++++------------------- 2 files changed, 35 insertions(+), 22 deletions(-) create mode 100644 Config.xcconfig diff --git a/Config.xcconfig b/Config.xcconfig new file mode 100644 index 0000000..fb44ceb --- /dev/null +++ b/Config.xcconfig @@ -0,0 +1,17 @@ +// Avoid accidental checkins: +// git update-index --skip-worktree Config.xcconfig +// git update-index --no-skip-worktree Config.xcconfig + + +DEVELOPMENT_TEAM[config=Debug] = +APP_PRODUCT_BUNDLE_IDENTIFIER[config=Debug] = +APP_PROVISIONING_PROFILE_SPECIFIER[config=Debug] = +STICKERS_PRODUCT_BUNDLE_IDENTIFIER[config=Debug] = +STICKERS_PROVISIONING_PROFILE_SPECIFIER[config=Debug] = + + +DEVELOPMENT_TEAM[config=Release] = +APP_PRODUCT_BUNDLE_IDENTIFIER[config=Release] = uk.sr2.dns +APP_PROVISIONING_PROFILE_SPECIFIER[config=Release] = +STICKERS_PRODUCT_BUNDLE_IDENTIFIER[config=Release] = uk.sr2.dns.Stickers +STICKERS_PROVISIONING_PROFILE_SPECIFIER[config=Release] = diff --git a/dns.xcodeproj/project.pbxproj b/dns.xcodeproj/project.pbxproj index 5eb3ebc..604c92f 100644 --- a/dns.xcodeproj/project.pbxproj +++ b/dns.xcodeproj/project.pbxproj @@ -38,6 +38,7 @@ /* Begin PBXFileReference section */ 069DCCFA2F8C0DCE00F1EB16 /* dns.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = dns.app; sourceTree = BUILT_PRODUCTS_DIR; }; A051B50B2FC9DA9100EACDC0 /* .bartycrouch.toml */ = {isa = PBXFileReference; lastKnownFileType = text; path = .bartycrouch.toml; sourceTree = ""; }; + A051B5132FC9E7BC00EACDC0 /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; A06A74772F8E95410093A9E4 /* .gitignore */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitignore; sourceTree = ""; }; A06A74782F8E95410093A9E4 /* LICENCE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENCE; sourceTree = ""; }; A06A74792F8E95410093A9E4 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; @@ -89,6 +90,7 @@ A06A74782F8E95410093A9E4 /* LICENCE */, A06A74792F8E95410093A9E4 /* README.md */, A051B50B2FC9DA9100EACDC0 /* .bartycrouch.toml */, + A051B5132FC9E7BC00EACDC0 /* Config.xcconfig */, 069DCCFC2F8C0DCE00F1EB16 /* dns */, A082640D2FC718790077B227 /* Stickers */, 069DCCFB2F8C0DCE00F1EB16 /* Products */, @@ -255,6 +257,7 @@ /* Begin XCBuildConfiguration section */ 069DCD032F8C0DCE00F1EB16 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = A051B5132FC9E7BC00EACDC0 /* Config.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -320,6 +323,7 @@ }; 069DCD042F8C0DCE00F1EB16 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = A051B5132FC9E7BC00EACDC0 /* Config.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -382,10 +386,8 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = dns/dns.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; @@ -403,10 +405,10 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = uk.sr2.dns; + PRODUCT_BUNDLE_IDENTIFIER = "$(APP_PRODUCT_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - STRING_CATALOG_GENERATE_SYMBOLS = YES; + PROVISIONING_PROFILE_SPECIFIER = "$(APP_PROVISIONING_PROFILE_SPECIFIER)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_EMIT_LOC_STRINGS = YES; @@ -422,10 +424,8 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = dns/dns.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; @@ -443,10 +443,10 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = uk.sr2.dns; + PRODUCT_BUNDLE_IDENTIFIER = "$(APP_PRODUCT_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - STRING_CATALOG_GENERATE_SYMBOLS = YES; + PROVISIONING_PROFILE_SPECIFIER = "$(APP_PROVISIONING_PROFILE_SPECIFIER)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_EMIT_LOC_STRINGS = YES; @@ -460,19 +460,17 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = "iMessage App Icon"; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Stickers/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Stickers; INFOPLIST_KEY_NSStickerSharingLevel = OS; IPHONEOS_DEPLOYMENT_TARGET = 26.5; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = uk.sr2.dns.Stickers; + PRODUCT_BUNDLE_IDENTIFIER = "$(STICKERS_PRODUCT_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; + PROVISIONING_PROFILE_SPECIFIER = "$(STICKERS_PROVISIONING_PROFILE_SPECIFIER)"; SKIP_INSTALL = YES; STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_EMIT_LOC_STRINGS = YES; @@ -484,19 +482,17 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = "iMessage App Icon"; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Stickers/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Stickers; INFOPLIST_KEY_NSStickerSharingLevel = OS; IPHONEOS_DEPLOYMENT_TARGET = 26.5; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = uk.sr2.dns.Stickers; + PRODUCT_BUNDLE_IDENTIFIER = "$(STICKERS_PRODUCT_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; + PROVISIONING_PROFILE_SPECIFIER = "$(STICKERS_PROVISIONING_PROFILE_SPECIFIER)"; SKIP_INSTALL = YES; STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_EMIT_LOC_STRINGS = YES;