diff --git a/Memory/ARCHITECTURE.md b/Memory/ARCHITECTURE.md index 079c5d3c..cacc3820 100644 --- a/Memory/ARCHITECTURE.md +++ b/Memory/ARCHITECTURE.md @@ -80,6 +80,8 @@ App appearance is a global UserDefaults preference applied by the macOS app delegate through `NSApplication.appearance`; following System clears the override. Opening at login reflects `SMAppService` on every Settings visit rather than a mirrored preference. A quiet login hides the app without suppressing its window. +macOS owns an explicit in-app language preference. Mobile delegates language selection +to Apple's per-app Settings and never stores or injects its own locale override. ## Remote sessions, terminal, and files diff --git a/TFI/App/MobileLanguagePreferenceMigration.swift b/TFI/App/MobileLanguagePreferenceMigration.swift new file mode 100644 index 00000000..dcd4c75b --- /dev/null +++ b/TFI/App/MobileLanguagePreferenceMigration.swift @@ -0,0 +1,10 @@ +import Foundation +import TermindApplication + +nonisolated enum MobileLanguagePreferenceMigration { + static func run(in defaults: UserDefaults = .standard) { + guard defaults.object(forKey: SettingsKey.appLanguage) != nil else { return } + defaults.removeObject(forKey: SettingsKey.appLanguage) + defaults.removeObject(forKey: "AppleLanguages") + } +} diff --git a/TFI/App/TermindIOSApp.swift b/TFI/App/TermindIOSApp.swift index aa3fcea1..8349b1ba 100644 --- a/TFI/App/TermindIOSApp.swift +++ b/TFI/App/TermindIOSApp.swift @@ -6,10 +6,10 @@ import TermindApplication struct TermindIOSApp: App { @UIApplicationDelegateAdaptor(MobileAppDelegate.self) private var appDelegate @AppStorage("mobile.appearance") private var appearance = "system" - private let launchLanguage = AppLanguage.stored() private let startup: Result init() { + MobileLanguagePreferenceMigration.run() startup = Result { let isTesting = ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil return try MobileAppModel(dependencies: isTesting ? .testing() : .live()) @@ -23,7 +23,6 @@ struct TermindIOSApp: App { MobileQuickActionSceneRoot(app: app) .modelContainer(app.container) .environment(\.mobileInterfaceIdiom, MobileInterfaceIdiom.current) - .environment(\.locale, launchLanguage.locale) .preferredColorScheme(appearance == "dark" ? .dark : appearance == "light" ? .light : nil) case .failure: ContentUnavailableView( diff --git a/TFI/Features/Settings/MobileAppSettingsView.swift b/TFI/Features/Settings/MobileAppSettingsView.swift index eeb9bad7..f3726321 100644 --- a/TFI/Features/Settings/MobileAppSettingsView.swift +++ b/TFI/Features/Settings/MobileAppSettingsView.swift @@ -5,21 +5,6 @@ struct MobileAppSettingsView: View { var appReview: AppReviewController? var reviewScene: MobileSceneModel? @AppStorage("mobile.appearance") private var appearance = "system" - @AppStorage(SettingsKey.appLanguage) private var language = AppLanguage.system.rawValue - @State private var launchLanguage = AppLanguage.stored() - - private var selectedLanguage: AppLanguage { - AppLanguage(rawValue: language) ?? AppLanguage.stored() - } - - private var languageSelection: Binding { - Binding( - get: { selectedLanguage }, - set: { language in - self.language = language.rawValue - AppLanguage.store(language) - }) - } var body: some View { Form { @@ -29,20 +14,6 @@ struct MobileAppSettingsView: View { Text("Light").tag("light") Text("Dark").tag("dark") } - Picker("Language", selection: languageSelection) { - ForEach(AppLanguage.allCases) { - Text( - $0.displayName( - deviceRegion: Locale.autoupdatingCurrent.region?.identifier, - canDisplayTaiwanFlag: DeviceCensorship.canDisplayTaiwanFlag - ) - ).tag($0) - } - } - if selectedLanguage != launchLanguage { - Text("The new language will be used after Termind relaunches.").font(.caption).foregroundStyle( - .secondary) - } } Section("Version") { LabeledContent( diff --git a/TFITests/MobileLanguagePreferenceMigrationTests.swift b/TFITests/MobileLanguagePreferenceMigrationTests.swift new file mode 100644 index 00000000..947afe78 --- /dev/null +++ b/TFITests/MobileLanguagePreferenceMigrationTests.swift @@ -0,0 +1,37 @@ +import Foundation +import TermindApplication +import Testing + +@testable import TermindIOS + +struct MobileLanguagePreferenceMigrationTests { + @Test func removesLegacyInAppLanguagePreferences() throws { + let (defaults, suiteName) = try makeDefaults() + defer { defaults.removePersistentDomain(forName: suiteName) } + defaults.set(AppLanguage.simplifiedChinese.rawValue, forKey: SettingsKey.appLanguage) + defaults.set([AppLanguage.simplifiedChinese.rawValue], forKey: "AppleLanguages") + + MobileLanguagePreferenceMigration.run(in: defaults) + + let domain = defaults.persistentDomain(forName: suiteName) + #expect(domain?[SettingsKey.appLanguage] == nil) + #expect(domain?["AppleLanguages"] == nil) + } + + @Test func preservesSystemManagedPerAppLanguageWithoutLegacyMarker() throws { + let (defaults, suiteName) = try makeDefaults() + defer { defaults.removePersistentDomain(forName: suiteName) } + defaults.set([AppLanguage.traditionalChinese.rawValue], forKey: "AppleLanguages") + + MobileLanguagePreferenceMigration.run(in: defaults) + + #expect(defaults.stringArray(forKey: "AppleLanguages") == [AppLanguage.traditionalChinese.rawValue]) + } + + private func makeDefaults() throws -> (UserDefaults, String) { + let suiteName = "MobileLanguagePreferenceMigrationTests.\(UUID().uuidString)" + let defaults = try #require(UserDefaults(suiteName: suiteName)) + defaults.removePersistentDomain(forName: suiteName) + return (defaults, suiteName) + } +}