diff options
| author | mojganii <mojgan.jelodar@mullvad.net> | 2025-07-29 13:13:31 +0200 |
|---|---|---|
| committer | mojganii <mojgan.jelodar@mullvad.net> | 2025-07-29 13:13:31 +0200 |
| commit | 367cd54f9c392ca145a4789fa29aacc223276ac4 (patch) | |
| tree | e797e39645c19eedec62415f0a115dbc11f0219b /ios | |
| parent | 18efeec135262bb372ec1b748458fabc9491003e (diff) | |
| download | mullvadvpn-367cd54f9c392ca145a4789fa29aacc223276ac4.tar.xz mullvadvpn-367cd54f9c392ca145a4789fa29aacc223276ac4.zip | |
Add support for the listed languages
Diffstat (limited to 'ios')
31 files changed, 513 insertions, 40 deletions
diff --git a/ios/Localizations/AppLanguage.swift b/ios/Localizations/AppLanguage.swift new file mode 100644 index 0000000000..b299938235 --- /dev/null +++ b/ios/Localizations/AppLanguage.swift @@ -0,0 +1,145 @@ +// +// AppLanguage.swift +// MullvadVPN +// +// Created by Mojgan on 2025-07-16. +// Copyright © 2025 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +/** + * TODO: + * Edit the "Localization Cleanup (Release Build)" build script phase after + * multi-language support is completed and released. + * + * Note: + * - Localization is not available for the Staging configuration, which is used by `UITest`. + * - When the functionality is finished, the script should: + * • Remove bilingual content only for Staging. + * • Eliminate the Debug configuration check. + */ +enum AppLanguage: String, CaseIterable, Identifiable { + case english = "en" + case danish = "da" + case german = "de" + case spanish = "es" + case finnish = "fi" + case french = "fr" + case italian = "it" + case japanese = "ja" + case korean = "ko" + case burmese = "my" + case norwegianBokmal = "nb" + case dutch = "nl" + case polish = "pl" + case portuguese = "pt" + case russian = "ru" + case swedish = "sv" + case thai = "th" + case turkish = "tr" + case chineseSimplified = "zh-Hans" // Maps to zh-CN + case chineseTraditional = "zh-Hant" // Maps to zh-TW + + var id: String { rawValue } + + var displayName: String { + switch self { + case .english: "English" + case .danish: "Dansk" + case .german: "Deutsch" + case .spanish: "Español" + case .finnish: "Suomi" + case .french: "Français" + case .italian: "Italiano" + case .japanese: "日本語" + case .korean: "한국어" + case .burmese: "မြန်မာ" + case .norwegianBokmal: "Norsk Bokmål" + case .dutch: "Nederlands" + case .polish: "Polski" + case .portuguese: "Português" + case .russian: "Русский" + case .swedish: "Svenska" + case .thai: "ไทย" + case .turkish: "Türkçe" + case .chineseSimplified: "简体中文" + case .chineseTraditional: "繁體中文" + } + } + + var countryCodeForFlag: String { + switch self { + case .english: "us" // English → US flag (or "gb" for UK) + case .danish: "dk" + case .german: "de" + case .spanish: "es" + case .finnish: "fi" + case .french: "fr" + case .italian: "it" + case .japanese: "jp" + case .korean: "kr" + case .burmese: "mm" + case .norwegianBokmal: "no" + case .dutch: "nl" + case .polish: "pl" + case .portuguese: "pt" + case .russian: "ru" + case .swedish: "se" + case .thai: "th" + case .turkish: "tr" + case .chineseSimplified: "cn" + case .chineseTraditional: "tw" + } + } + + static var allSorted: [AppLanguage] { + AppLanguage.allCases + .sorted { $0.displayName.localizedCaseInsensitiveCompare($1.displayName) == .orderedAscending } + } + + static func from(_ code: String) -> AppLanguage { + AppLanguage(rawValue: code) ?? .english + } + + var flagEmoji: String { + let base: UInt32 = 127397 + var flagString = "" + for scalar in countryCodeForFlag.uppercased().unicodeScalars { + guard let scalarValue = UnicodeScalar(base + scalar.value) else { return "" } + flagString.unicodeScalars.append(scalarValue) + } + return flagString + } + + static var currentLanguage: AppLanguage { + let defaultCode = AppLanguage.english.rawValue + let fullCode = Locale.preferredLanguages.first ?? defaultCode + + if #available(iOS 16, *) { + let locale = Locale(identifier: fullCode) + if let script = locale.language.script?.identifier { + switch script { + case "Hans": + return .chineseSimplified + case "Hant": + return .chineseTraditional + default: + break + } + } + } else { + if fullCode.contains("Hans") { + return .chineseSimplified + } else if fullCode.contains("Hant") { + return .chineseTraditional + } + } + + // Otherwise, try to get languageCode (e.g., "en", "fr") + let locale = Locale(identifier: fullCode) + let langCode = locale.languageCode ?? defaultCode + + return AppLanguage.from(langCode) + } +} diff --git a/ios/Localizations/da.lproj/Localizable.strings b/ios/Localizations/da.lproj/Localizable.strings new file mode 100644 index 0000000000..f40aa19b9a --- /dev/null +++ b/ios/Localizations/da.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + MullvadVPN + + Created by Mojgan on 2025-07-16. + Copyright © 2025 Mullvad VPN AB. All rights reserved. +*/ diff --git a/ios/Localizations/de.lproj/Localizable.strings b/ios/Localizations/de.lproj/Localizable.strings new file mode 100644 index 0000000000..f40aa19b9a --- /dev/null +++ b/ios/Localizations/de.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + MullvadVPN + + Created by Mojgan on 2025-07-16. + Copyright © 2025 Mullvad VPN AB. All rights reserved. +*/ diff --git a/ios/Localizations/en.lproj/Localizable.strings b/ios/Localizations/en.lproj/Localizable.strings new file mode 100644 index 0000000000..f40aa19b9a --- /dev/null +++ b/ios/Localizations/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + MullvadVPN + + Created by Mojgan on 2025-07-16. + Copyright © 2025 Mullvad VPN AB. All rights reserved. +*/ diff --git a/ios/Localizations/es.lproj/Localizable.strings b/ios/Localizations/es.lproj/Localizable.strings new file mode 100644 index 0000000000..f40aa19b9a --- /dev/null +++ b/ios/Localizations/es.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + MullvadVPN + + Created by Mojgan on 2025-07-16. + Copyright © 2025 Mullvad VPN AB. All rights reserved. +*/ diff --git a/ios/Localizations/fi.lproj/Localizable.strings b/ios/Localizations/fi.lproj/Localizable.strings new file mode 100644 index 0000000000..f40aa19b9a --- /dev/null +++ b/ios/Localizations/fi.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + MullvadVPN + + Created by Mojgan on 2025-07-16. + Copyright © 2025 Mullvad VPN AB. All rights reserved. +*/ diff --git a/ios/Localizations/fr.lproj/Localizable.strings b/ios/Localizations/fr.lproj/Localizable.strings new file mode 100644 index 0000000000..f40aa19b9a --- /dev/null +++ b/ios/Localizations/fr.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + MullvadVPN + + Created by Mojgan on 2025-07-16. + Copyright © 2025 Mullvad VPN AB. All rights reserved. +*/ diff --git a/ios/Localizations/it.lproj/Localizable.strings b/ios/Localizations/it.lproj/Localizable.strings new file mode 100644 index 0000000000..f40aa19b9a --- /dev/null +++ b/ios/Localizations/it.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + MullvadVPN + + Created by Mojgan on 2025-07-16. + Copyright © 2025 Mullvad VPN AB. All rights reserved. +*/ diff --git a/ios/Localizations/ja.lproj/Localizable.strings b/ios/Localizations/ja.lproj/Localizable.strings new file mode 100644 index 0000000000..f40aa19b9a --- /dev/null +++ b/ios/Localizations/ja.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + MullvadVPN + + Created by Mojgan on 2025-07-16. + Copyright © 2025 Mullvad VPN AB. All rights reserved. +*/ diff --git a/ios/Localizations/ko.lproj/Localizable.strings b/ios/Localizations/ko.lproj/Localizable.strings new file mode 100644 index 0000000000..f40aa19b9a --- /dev/null +++ b/ios/Localizations/ko.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + MullvadVPN + + Created by Mojgan on 2025-07-16. + Copyright © 2025 Mullvad VPN AB. All rights reserved. +*/ diff --git a/ios/Localizations/my.lproj/Localizable.strings b/ios/Localizations/my.lproj/Localizable.strings new file mode 100644 index 0000000000..f40aa19b9a --- /dev/null +++ b/ios/Localizations/my.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + MullvadVPN + + Created by Mojgan on 2025-07-16. + Copyright © 2025 Mullvad VPN AB. All rights reserved. +*/ diff --git a/ios/Localizations/nb.lproj/Localizable.strings b/ios/Localizations/nb.lproj/Localizable.strings new file mode 100644 index 0000000000..f40aa19b9a --- /dev/null +++ b/ios/Localizations/nb.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + MullvadVPN + + Created by Mojgan on 2025-07-16. + Copyright © 2025 Mullvad VPN AB. All rights reserved. +*/ diff --git a/ios/Localizations/nl.lproj/Localizable.strings b/ios/Localizations/nl.lproj/Localizable.strings new file mode 100644 index 0000000000..f40aa19b9a --- /dev/null +++ b/ios/Localizations/nl.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + MullvadVPN + + Created by Mojgan on 2025-07-16. + Copyright © 2025 Mullvad VPN AB. All rights reserved. +*/ diff --git a/ios/Localizations/pl.lproj/Localizable.strings b/ios/Localizations/pl.lproj/Localizable.strings new file mode 100644 index 0000000000..f40aa19b9a --- /dev/null +++ b/ios/Localizations/pl.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + MullvadVPN + + Created by Mojgan on 2025-07-16. + Copyright © 2025 Mullvad VPN AB. All rights reserved. +*/ diff --git a/ios/Localizations/pt-PT.lproj/Localizable.strings b/ios/Localizations/pt-PT.lproj/Localizable.strings new file mode 100644 index 0000000000..f40aa19b9a --- /dev/null +++ b/ios/Localizations/pt-PT.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + MullvadVPN + + Created by Mojgan on 2025-07-16. + Copyright © 2025 Mullvad VPN AB. All rights reserved. +*/ diff --git a/ios/Localizations/ru.lproj/Localizable.strings b/ios/Localizations/ru.lproj/Localizable.strings new file mode 100644 index 0000000000..f40aa19b9a --- /dev/null +++ b/ios/Localizations/ru.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + MullvadVPN + + Created by Mojgan on 2025-07-16. + Copyright © 2025 Mullvad VPN AB. All rights reserved. +*/ diff --git a/ios/Localizations/sv.lproj/Localizable.strings b/ios/Localizations/sv.lproj/Localizable.strings new file mode 100644 index 0000000000..f40aa19b9a --- /dev/null +++ b/ios/Localizations/sv.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + MullvadVPN + + Created by Mojgan on 2025-07-16. + Copyright © 2025 Mullvad VPN AB. All rights reserved. +*/ diff --git a/ios/Localizations/th.lproj/Localizable.strings b/ios/Localizations/th.lproj/Localizable.strings new file mode 100644 index 0000000000..f40aa19b9a --- /dev/null +++ b/ios/Localizations/th.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + MullvadVPN + + Created by Mojgan on 2025-07-16. + Copyright © 2025 Mullvad VPN AB. All rights reserved. +*/ diff --git a/ios/Localizations/tr.lproj/Localizable.strings b/ios/Localizations/tr.lproj/Localizable.strings new file mode 100644 index 0000000000..f40aa19b9a --- /dev/null +++ b/ios/Localizations/tr.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + MullvadVPN + + Created by Mojgan on 2025-07-16. + Copyright © 2025 Mullvad VPN AB. All rights reserved. +*/ diff --git a/ios/Localizations/zh-Hans.lproj/Localizable.strings b/ios/Localizations/zh-Hans.lproj/Localizable.strings new file mode 100644 index 0000000000..f40aa19b9a --- /dev/null +++ b/ios/Localizations/zh-Hans.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + MullvadVPN + + Created by Mojgan on 2025-07-16. + Copyright © 2025 Mullvad VPN AB. All rights reserved. +*/ diff --git a/ios/Localizations/zh-Hant.lproj/Localizable.strings b/ios/Localizations/zh-Hant.lproj/Localizable.strings new file mode 100644 index 0000000000..f40aa19b9a --- /dev/null +++ b/ios/Localizations/zh-Hant.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + MullvadVPN + + Created by Mojgan on 2025-07-16. + Copyright © 2025 Mullvad VPN AB. All rights reserved. +*/ diff --git a/ios/MullvadTypes/PersistentAccessMethod.swift b/ios/MullvadTypes/PersistentAccessMethod.swift index 9a3310d06f..ff816405b6 100644 --- a/ios/MullvadTypes/PersistentAccessMethod.swift +++ b/ios/MullvadTypes/PersistentAccessMethod.swift @@ -82,12 +82,12 @@ public enum PersistentProxyConfiguration: Codable, Equatable, Sendable { extension PersistentProxyConfiguration { /// Socks autentication method. - public enum SocksAuthentication: Codable, Equatable { + public enum SocksAuthentication: Codable, Equatable, Sendable { case noAuthentication case authentication(UserCredential) } - public struct UserCredential: Codable, Equatable { + public struct UserCredential: Codable, Equatable, Sendable { public let username: String public let password: String diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index a4bf13fb88..b0bd9e504a 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -980,6 +980,16 @@ F05769BB2C6661EE00D9778B /* TunnelSettingsStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F05769BA2C6661EE00D9778B /* TunnelSettingsStrategy.swift */; }; F05919752C45194B00C301F3 /* EphemeralPeerKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = F05919742C45194B00C301F3 /* EphemeralPeerKey.swift */; }; F05919802C45515200C301F3 /* EphemeralPeerExchangeActor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A948809A2BC9308D0090A44C /* EphemeralPeerExchangeActor.swift */; }; + F05DCE9E2E38C563009A9B85 /* AppLanguage.swift in Sources */ = {isa = PBXBuildFile; fileRef = F05DCE872E38C563009A9B85 /* AppLanguage.swift */; }; + F05DCE9F2E38C563009A9B85 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F05DCE9C2E38C563009A9B85 /* Localizable.strings */; }; + F05DCEA02E38C5F1009A9B85 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F05DCE9C2E38C563009A9B85 /* Localizable.strings */; }; + F05DCEA12E38C5F1009A9B85 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F05DCE9C2E38C563009A9B85 /* Localizable.strings */; }; + F05DCEA22E38C5F1009A9B85 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F05DCE9C2E38C563009A9B85 /* Localizable.strings */; }; + F05DCEA32E38C5F1009A9B85 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F05DCE9C2E38C563009A9B85 /* Localizable.strings */; }; + F05DCEA42E38C5F1009A9B85 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F05DCE9C2E38C563009A9B85 /* Localizable.strings */; }; + F05DCEA52E38C5F1009A9B85 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F05DCE9C2E38C563009A9B85 /* Localizable.strings */; }; + F05DCEA62E38C5F1009A9B85 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F05DCE9C2E38C563009A9B85 /* Localizable.strings */; }; + F05DCEA72E38C5F1009A9B85 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F05DCE9C2E38C563009A9B85 /* Localizable.strings */; }; F05F39972B21C735006E60A7 /* RelayCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820675A26E6576800655B05 /* RelayCache.swift */; }; F05F39982B21C73C006E60A7 /* CachedRelays.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA87626B024A600B8C587 /* CachedRelays.swift */; }; F06045E62B231EB700B2D37A /* URLSessionTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = F06045E52B231EB700B2D37A /* URLSessionTransport.swift */; }; @@ -2442,6 +2452,27 @@ F05919782C45402E00C301F3 /* SingleHopEphemeralPeerExchanger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleHopEphemeralPeerExchanger.swift; sourceTree = "<group>"; }; F059197C2C454C9200C301F3 /* MultiHopEphemeralPeerExchanger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiHopEphemeralPeerExchanger.swift; sourceTree = "<group>"; }; F059197E2C454CE000C301F3 /* EphemeralPeerExchangingProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EphemeralPeerExchangingProtocol.swift; sourceTree = "<group>"; }; + F05DCE872E38C563009A9B85 /* AppLanguage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLanguage.swift; sourceTree = "<group>"; }; + F05DCE882E38C563009A9B85 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Localizable.strings; sourceTree = "<group>"; }; + F05DCE892E38C563009A9B85 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; }; + F05DCE8A2E38C563009A9B85 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; }; + F05DCE8B2E38C563009A9B85 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = "<group>"; }; + F05DCE8C2E38C563009A9B85 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = "<group>"; }; + F05DCE8D2E38C563009A9B85 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; }; + F05DCE8E2E38C563009A9B85 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = "<group>"; }; + F05DCE8F2E38C563009A9B85 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; }; + F05DCE902E38C563009A9B85 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Localizable.strings; sourceTree = "<group>"; }; + F05DCE912E38C563009A9B85 /* my */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = my; path = my.lproj/Localizable.strings; sourceTree = "<group>"; }; + F05DCE922E38C563009A9B85 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/Localizable.strings; sourceTree = "<group>"; }; + F05DCE932E38C563009A9B85 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; }; + F05DCE942E38C563009A9B85 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = "<group>"; }; + F05DCE952E38C563009A9B85 /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/Localizable.strings"; sourceTree = "<group>"; }; + F05DCE962E38C563009A9B85 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = "<group>"; }; + F05DCE972E38C563009A9B85 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Localizable.strings; sourceTree = "<group>"; }; + F05DCE982E38C563009A9B85 /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/Localizable.strings; sourceTree = "<group>"; }; + F05DCE992E38C563009A9B85 /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = "<group>"; }; + F05DCE9A2E38C563009A9B85 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; }; + F05DCE9B2E38C563009A9B85 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; sourceTree = "<group>"; }; F06045E52B231EB700B2D37A /* URLSessionTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionTransport.swift; sourceTree = "<group>"; }; F06045E92B23217E00B2D37A /* ShadowsocksTransport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShadowsocksTransport.swift; sourceTree = "<group>"; }; F06045EB2B2322A500B2D37A /* Jittered.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Jittered.swift; sourceTree = "<group>"; }; @@ -3917,12 +3948,16 @@ children = ( 58F3C0A824A50C0E003E76BE /* Assets */, 58ECD29023F178FD004298B6 /* Configurations */, + 584F991F2902CBDD001F858D /* Frameworks */, + F05DCE9D2E38C563009A9B85 /* Localizations */, 01EF6F2D2B6A51B100125696 /* mullvad-api.h */, 8556EB512B9A1C6900D26DD4 /* MullvadApi.swift */, 58D223F4294C8FF00029F5F8 /* MullvadLogging */, F0ACE3092BE4E478006D5333 /* MullvadMockData */, 06799ABD28F98E1D00ACD94E /* MullvadREST */, 58FBFBE7291622580020E046 /* MullvadRESTTests */, + A992DA1E2C24709F00DE7CE5 /* MullvadRustRuntime */, + A9D9A4C12C36D53C004088DD /* MullvadRustRuntimeTests */, 58B2FDD42AA71D2A003EB5C6 /* MullvadSettings */, 581943F228F8014500B0CB5E /* MullvadTypes */, 58CE5E62224146200008646E /* MullvadVPN */, @@ -3933,15 +3968,12 @@ 58CE5E7A224146470008646E /* PacketTunnel */, 58C7A4372A863F450060C66F /* PacketTunnelCore */, 58C7A4432A863F490060C66F /* PacketTunnelCoreTests */, + 58CE5E61224146200008646E /* Products */, 7A88DCCF2A8FABBE00D2FF0E /* Routing */, 7A88DCDD2A8FABBE00D2FF0E /* RoutingTests */, 589A454A28DDF59B00565204 /* Shared */, 7A83C3FC2A55B39500DFB83A /* TestPlans */, 58695A9E2A4ADA9200328DB3 /* TunnelObfuscationTests */, - A992DA1E2C24709F00DE7CE5 /* MullvadRustRuntime */, - A9D9A4C12C36D53C004088DD /* MullvadRustRuntimeTests */, - 58CE5E61224146200008646E /* Products */, - 584F991F2902CBDD001F858D /* Frameworks */, ); sourceTree = "<group>"; }; @@ -4659,6 +4691,15 @@ path = PostQuantum; sourceTree = "<group>"; }; + F05DCE9D2E38C563009A9B85 /* Localizations */ = { + isa = PBXGroup; + children = ( + F05DCE872E38C563009A9B85 /* AppLanguage.swift */, + F05DCE9C2E38C563009A9B85 /* Localizable.strings */, + ); + path = Localizations; + sourceTree = "<group>"; + }; F06045F02B2324DA00B2D37A /* ApiHandlers */ = { isa = PBXGroup; children = ( @@ -5195,6 +5236,7 @@ 58CE5E5E224146200008646E /* Resources */, 58CE5E85224146470008646E /* Embed Foundation Extensions */, 06799AD628F98E1D00ACD94E /* Embed Frameworks */, + F0DABA422E27D20900EB4E21 /* Localization Cleanup (Release Build) */, ); buildRules = ( ); @@ -5557,7 +5599,26 @@ hasScannedForEncodings = 0; knownRegions = ( en, + th, Base, + fr, + de, + es, + it, + "pt-PT", + nl, + sv, + da, + nb, + ja, + ko, + "zh-Hans", + "zh-Hant", + ru, + pl, + tr, + fi, + my, ); mainGroup = 58CE5E57224146200008646E; packageReferences = ( @@ -5597,6 +5658,7 @@ buildActionMask = 2147483647; files = ( 062B45A328FD4CA700746E77 /* le_root_cert.cer in Resources */, + F05DCEA22E38C5F1009A9B85 /* Localizable.strings in Resources */, 7A95B67B2D5F758300687524 /* relays.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -5619,6 +5681,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + F05DCEA52E38C5F1009A9B85 /* Localizable.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -5636,6 +5699,7 @@ A9BA08312BA32FA9005A7A2D /* PrivacyInfo.xcprivacy in Resources */, 7A83C3FF2A55B72E00DFB83A /* MullvadVPNApp.xctestplan in Resources */, 58727283265D173C00F315B2 /* LaunchScreen.storyboard in Resources */, + F05DCE9F2E38C563009A9B85 /* Localizable.strings in Resources */, 5859A55529CD9DD900F66591 /* changes.txt in Resources */, 587DCCEF287D84A500CE821E /* countries.geo.json in Resources */, 58CE5E6B224146210008646E /* Assets.xcassets in Resources */, @@ -5647,6 +5711,7 @@ buildActionMask = 2147483647; files = ( A9BA08322BA32FB6005A7A2D /* PrivacyInfo.xcprivacy in Resources */, + F05DCEA02E38C5F1009A9B85 /* Localizable.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -5654,6 +5719,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + F05DCEA12E38C5F1009A9B85 /* Localizable.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -5661,6 +5727,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + F05DCEA32E38C5F1009A9B85 /* Localizable.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -5668,6 +5735,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + F05DCEA42E38C5F1009A9B85 /* Localizable.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -5682,6 +5750,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + F05DCEA62E38C5F1009A9B85 /* Localizable.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -5717,6 +5786,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + F05DCEA72E38C5F1009A9B85 /* Localizable.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -5834,6 +5904,24 @@ shellPath = /bin/sh; shellScript = "exec > $PROJECT_DIR/relays-prebuild.log 2>&1\n\n$PROJECT_DIR/relays-prebuild.sh\n"; }; + F0DABA422E27D20900EB4E21 /* Localization Cleanup (Release Build) */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Localization Cleanup (Release Build)"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Run the following steps if the build configuration is NOT Debug\n# OR if the configuration is Staging (used for UITests).\nif [ \"$CONFIGURATION\" != \"Debug\" ] || [ \"$CONFIGURATION\" = \"Staging\" ]; then\n echo \"Removing non-English localizations for Release or Staging build\"\n find \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}\" -type d -name \"*.lproj\" ! -name \"en.lproj\" -exec rm -r {} +\nfi\n"; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -6435,6 +6523,7 @@ F91CCBFA2DFAC8ED007F1925 /* DeviceListView.swift in Sources */, 7A9CCCBF2A96302800DD6A34 /* SettingsCoordinator.swift in Sources */, 58F70FE52AEA707800E6890E /* StoreTransactionLog.swift in Sources */, + F05DCE9E2E38C563009A9B85 /* AppLanguage.swift in Sources */, F9394EF02DC0B58D009595EA /* MullvadListNavigationItemView.swift in Sources */, 582AE3102440A6CA00E6733A /* InputTextFormatter.swift in Sources */, 7A6F2FAD2AFD3DA7006D0856 /* CustomDNSViewController.swift in Sources */, @@ -7240,6 +7329,36 @@ }; /* End PBXTargetDependency section */ +/* Begin PBXVariantGroup section */ + F05DCE9C2E38C563009A9B85 /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + F05DCE882E38C563009A9B85 /* da */, + F05DCE892E38C563009A9B85 /* de */, + F05DCE8A2E38C563009A9B85 /* en */, + F05DCE8B2E38C563009A9B85 /* es */, + F05DCE8C2E38C563009A9B85 /* fi */, + F05DCE8D2E38C563009A9B85 /* fr */, + F05DCE8E2E38C563009A9B85 /* it */, + F05DCE8F2E38C563009A9B85 /* ja */, + F05DCE902E38C563009A9B85 /* ko */, + F05DCE912E38C563009A9B85 /* my */, + F05DCE922E38C563009A9B85 /* nb */, + F05DCE932E38C563009A9B85 /* nl */, + F05DCE942E38C563009A9B85 /* pl */, + F05DCE952E38C563009A9B85 /* pt-PT */, + F05DCE962E38C563009A9B85 /* ru */, + F05DCE972E38C563009A9B85 /* sv */, + F05DCE982E38C563009A9B85 /* th */, + F05DCE992E38C563009A9B85 /* tr */, + F05DCE9A2E38C563009A9B85 /* zh-Hans */, + F05DCE9B2E38C563009A9B85 /* zh-Hant */, + ); + name = Localizable.strings; + sourceTree = "<group>"; + }; +/* End PBXVariantGroup section */ + /* Begin XCBuildConfiguration section */ 06799AD428F98E1D00ACD94E /* Debug */ = { isa = XCBuildConfiguration; @@ -7597,6 +7716,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -7661,6 +7781,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -8307,6 +8428,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -8381,6 +8503,10 @@ DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = CKG9MXH72F; ENABLE_BITCODE = NO; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "DEBUG=1", + ); INFOPLIST_FILE = "MullvadVPN/Supporting Files/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -9163,6 +9289,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; diff --git a/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift b/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift index 7d0196560c..b7113fb11e 100644 --- a/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift +++ b/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift @@ -104,6 +104,7 @@ public enum AccessibilityIdentifier: Equatable { case daitaCell case daitaFilterPill case obfuscationFilterPill + case languageCell // Labels case accountPageDeviceNameLabel diff --git a/ios/MullvadVPN/Coordinators/Settings/SettingsCoordinator.swift b/ios/MullvadVPN/Coordinators/Settings/SettingsCoordinator.swift index dae1b67605..898a518411 100644 --- a/ios/MullvadVPN/Coordinators/Settings/SettingsCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/Settings/SettingsCoordinator.swift @@ -38,6 +38,9 @@ enum SettingsNavigationRoute: Equatable { /// DAITA route. case daita + + /// Language route. + case language } /// Top-level settings coordinator. @@ -141,6 +144,15 @@ final class SettingsCoordinator: Coordinator, Presentable, Presenting, SettingsV presentChild(safariCoordinator, animated: animated, completion: completion) + case .language: + logger.debug("Show App's settings for \(route)") + + if let url = URL(string: UIApplication.openSettingsURLString) { + if UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url, options: [:], completionHandler: nil) + } + } + default: // Ignore navigation if the route is already presented. guard currentRoute != route else { diff --git a/ios/MullvadVPN/Coordinators/Settings/SettingsViewControllerFactory.swift b/ios/MullvadVPN/Coordinators/Settings/SettingsViewControllerFactory.swift index ea2ef51e32..bef1dd4f7e 100644 --- a/ios/MullvadVPN/Coordinators/Settings/SettingsViewControllerFactory.swift +++ b/ios/MullvadVPN/Coordinators/Settings/SettingsViewControllerFactory.swift @@ -57,6 +57,9 @@ struct SettingsViewControllerFactory { case .faq: // Handled separately and presented as a modal. .failed + case .language: + // Handled separately and presented settings. + .failed case .vpnSettings: makeVPNSettingsViewCoordinator() case .problemReport: diff --git a/ios/MullvadVPN/View controllers/Settings/SettingsCellFactory.swift b/ios/MullvadVPN/View controllers/Settings/SettingsCellFactory.swift index e837f65cd4..1e76a42e9f 100644 --- a/ios/MullvadVPN/View controllers/Settings/SettingsCellFactory.swift +++ b/ios/MullvadVPN/View controllers/Settings/SettingsCellFactory.swift @@ -149,6 +149,25 @@ final class SettingsCellFactory: @preconcurrency CellFactoryProtocol, Sendable { cell.setAccessibilityIdentifier(item.accessibilityIdentifier) cell.disclosureType = .chevron + case .language: + guard let cell = cell as? SettingsCell else { return } + + cell.titleLabel.text = NSLocalizedString( + "LANGUAGE_CELL_LABEL", + tableName: "Settings", + value: "Language", + comment: "" + ) + + cell.detailTitleLabel.text = NSLocalizedString( + "LANGUAGE_CELL_DETAIL_LABEL", + tableName: "Settings", + value: viewModel.currentLanguage, + comment: "" + ) + + cell.setAccessibilityIdentifier(item.accessibilityIdentifier) + cell.disclosureType = .chevron } } } diff --git a/ios/MullvadVPN/View controllers/Settings/SettingsDataSource.swift b/ios/MullvadVPN/View controllers/Settings/SettingsDataSource.swift index 138fabf450..74c09d704a 100644 --- a/ios/MullvadVPN/View controllers/Settings/SettingsDataSource.swift +++ b/ios/MullvadVPN/View controllers/Settings/SettingsDataSource.swift @@ -46,6 +46,7 @@ final class SettingsDataSource: UITableViewDiffableDataSource<SettingsDataSource case apiAccess case version case problemReport + case language } enum Item: String { @@ -56,23 +57,26 @@ final class SettingsDataSource: UITableViewDiffableDataSource<SettingsDataSource case apiAccess case daita case multihop + case language var accessibilityIdentifier: AccessibilityIdentifier { switch self { case .vpnSettings: - return .vpnSettingsCell + .vpnSettingsCell case .changelog: - return .versionCell + .versionCell case .problemReport: - return .problemReportCell + .problemReportCell case .faq: - return .faqCell + .faqCell case .apiAccess: - return .apiAccessCell + .apiAccessCell case .daita: - return .daitaCell + .daitaCell case .multihop: - return .multihopCell + .multihopCell + case .language: + .languageCell } } @@ -109,14 +113,14 @@ final class SettingsDataSource: UITableViewDiffableDataSource<SettingsDataSource registerClasses() updateDataSnapshot() - interactor.didUpdateDeviceState = { [weak self] _ in + interactor.didUpdateSettings = { [weak self] in self?.updateDataSnapshot() } storedAccountData = interactor.deviceState.accountData } - func reload(from tunnelSettings: LatestTunnelSettings) { - settingsCellFactory.viewModel = SettingsViewModel(from: tunnelSettings) + func reload() { + settingsCellFactory.viewModel = SettingsViewModel(from: interactor.tunnelSettings) var snapshot = snapshot() snapshot.reconfigureItems(snapshot.itemIdentifiers) @@ -135,9 +139,15 @@ final class SettingsDataSource: UITableViewDiffableDataSource<SettingsDataSource } func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - tableView.dequeueReusableHeaderFooterView( - withIdentifier: HeaderFooterReuseIdentifier.spacer.rawValue - ) + guard let section = sectionIdentifier(for: section) else { return nil } + return switch section { + case .language: + nil + default: + tableView.dequeueReusableHeaderFooterView( + withIdentifier: HeaderFooterReuseIdentifier.spacer.rawValue + ) + } } // MARK: - Private @@ -163,6 +173,11 @@ final class SettingsDataSource: UITableViewDiffableDataSource<SettingsDataSource ], toSection: .vpnSettings) } + #if DEBUG + snapshot.appendSections([.language]) + snapshot.appendItems([.language], toSection: .language) + #endif + snapshot.appendSections([.apiAccess]) snapshot.appendItems([.apiAccess], toSection: .apiAccess) diff --git a/ios/MullvadVPN/View controllers/Settings/SettingsInteractor.swift b/ios/MullvadVPN/View controllers/Settings/SettingsInteractor.swift index de2585737c..7509bf5215 100644 --- a/ios/MullvadVPN/View controllers/Settings/SettingsInteractor.swift +++ b/ios/MullvadVPN/View controllers/Settings/SettingsInteractor.swift @@ -13,28 +13,27 @@ import MullvadSettings final class SettingsInteractor { private let tunnelManager: TunnelManager private var tunnelObserver: TunnelObserver? + var didUpdateSettings: (() -> Void)? - var didUpdateDeviceState: ((DeviceState) -> Void)? - var didUpdateTunnelSettings: ((LatestTunnelSettings) -> Void)? - - var tunnelSettings: LatestTunnelSettings { - tunnelManager.settings - } - - var deviceState: DeviceState { - tunnelManager.deviceState - } + private(set) var tunnelSettings: LatestTunnelSettings + private(set) var deviceState: DeviceState init(tunnelManager: TunnelManager) { self.tunnelManager = tunnelManager + self.tunnelSettings = tunnelManager.settings + self.deviceState = tunnelManager.deviceState let tunnelObserver = TunnelBlockObserver( didUpdateDeviceState: { [weak self] _, deviceState, _ in - self?.didUpdateDeviceState?(deviceState) + guard let self = self else { return } + self.deviceState = deviceState + self.didUpdateSettings?() }, didUpdateTunnelSettings: { [weak self] _, settings in - self?.didUpdateTunnelSettings?(settings) + guard let self = self else { return } + self.tunnelSettings = settings + self.didUpdateSettings?() } ) diff --git a/ios/MullvadVPN/View controllers/Settings/SettingsViewController.swift b/ios/MullvadVPN/View controllers/Settings/SettingsViewController.swift index 784fcfa000..783cac1ee1 100644 --- a/ios/MullvadVPN/View controllers/Settings/SettingsViewController.swift +++ b/ios/MullvadVPN/View controllers/Settings/SettingsViewController.swift @@ -70,10 +70,15 @@ class SettingsViewController: UITableViewController { dataSource = SettingsDataSource(tableView: tableView, interactor: interactor) dataSource?.delegate = self - interactor.didUpdateTunnelSettings = { [weak self] newSettings in - self?.dataSource?.reload(from: newSettings) + interactor.didUpdateSettings = { [weak self] in + self?.dataSource?.reload() } } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + dataSource?.reload() + } } extension SettingsViewController: @preconcurrency SettingsDataSourceDelegate { @@ -108,19 +113,21 @@ extension SettingsDataSource.Item { var navigationRoute: SettingsNavigationRoute? { switch self { case .vpnSettings: - return .vpnSettings + .vpnSettings case .changelog: - return .changelog + .changelog case .problemReport: - return .problemReport + .problemReport case .faq: - return .faq + .faq case .apiAccess: - return .apiAccess + .apiAccess case .daita: - return .daita + .daita case .multihop: - return .multihop + .multihop + case .language: + .language } } } diff --git a/ios/MullvadVPN/View controllers/Settings/SettingsViewModel.swift b/ios/MullvadVPN/View controllers/Settings/SettingsViewModel.swift index fc698caa50..77abcf9f05 100644 --- a/ios/MullvadVPN/View controllers/Settings/SettingsViewModel.swift +++ b/ios/MullvadVPN/View controllers/Settings/SettingsViewModel.swift @@ -12,6 +12,11 @@ struct SettingsViewModel { private(set) var daitaSettings: DAITASettings private(set) var multihopState: MultihopState + var currentLanguage: String { + let currentLanguage = AppLanguage.currentLanguage + return "\(currentLanguage.flagEmoji) \(currentLanguage.displayName)" + } + init(from tunnelSettings: LatestTunnelSettings = LatestTunnelSettings()) { daitaSettings = tunnelSettings.daita multihopState = tunnelSettings.tunnelMultihopState |
