From 78a023bb52faf5d4fe6ec0e28a64d79882fbde27 Mon Sep 17 00:00:00 2001 From: Kaede Akino Date: Sat, 12 Sep 2026 13:20:56 +0800 Subject: [PATCH] zh-CN to zh-Hans --- .../Agent/ConversationModel.swift | 2 +- .../TermindApplication/AppLanguage.swift | 19 ++++++++--- Scripts/run-screenshot-demo.sh | 2 +- .../Settings/MobileAppSettingsView.swift | 20 ++++++++--- TFI/Features/Workspaces/MobileWorkspace.swift | 7 ++-- TFITests/MobileAssistantRenderingTests.swift | 2 +- TFM/App/ScreenshotDemo.swift | 4 +-- .../FeatureModels/HostLibraryModel+Demo.swift | 2 +- TFM/Features/Settings/SettingsPageView.swift | 20 ++++++----- TFMTests/AppLanguageTests.swift | 33 +++++++++++++++---- TFMTests/ChatMessageTimestampTests.swift | 2 +- TFMTests/ScreenshotDemoTests.swift | 4 +-- TFMTests/WindowToolbarFindTests.swift | 6 ++-- 13 files changed, 86 insertions(+), 37 deletions(-) diff --git a/Frameworks/TermindApplication/Agent/ConversationModel.swift b/Frameworks/TermindApplication/Agent/ConversationModel.swift index d479d4e2..72215003 100644 --- a/Frameworks/TermindApplication/Agent/ConversationModel.swift +++ b/Frameworks/TermindApplication/Agent/ConversationModel.swift @@ -2644,7 +2644,7 @@ extension WeakAgentUserInteraction: AgentUserInteractionHandling {} extension ConversationModel { public static func demo(language: AppLanguage = .english) -> ConversationModel { func text(_ en: String, _ zh: String) -> String { - language == .simplifiedChineseChina ? zh : en + language == .simplifiedChinese ? zh : en } let now = Date(timeIntervalSinceReferenceDate: 809_020_800) let user = ChatMessage( diff --git a/Frameworks/TermindApplication/AppLanguage.swift b/Frameworks/TermindApplication/AppLanguage.swift index 9668a18e..0cd24ee7 100644 --- a/Frameworks/TermindApplication/AppLanguage.swift +++ b/Frameworks/TermindApplication/AppLanguage.swift @@ -29,7 +29,7 @@ public nonisolated enum AppLocalization { public nonisolated enum AppLanguage: String, CaseIterable, Identifiable, Sendable { case system case english = "en" - case simplifiedChineseChina = "zh-Hans-CN" + case simplifiedChinese = "zh-Hans" public nonisolated var id: String { rawValue } @@ -37,7 +37,7 @@ public nonisolated enum AppLanguage: String, CaseIterable, Identifiable, Sendabl switch self { case .system: String(localized: "Follow System") case .english: "English" - case .simplifiedChineseChina: "简体中文(中国)" + case .simplifiedChinese: "简体中文" } } @@ -45,7 +45,16 @@ public nonisolated enum AppLanguage: String, CaseIterable, Identifiable, Sendabl guard let rawValue = defaults.string(forKey: SettingsKey.appLanguage) else { return .system } - return AppLanguage(rawValue: rawValue) ?? .system + if let language = AppLanguage(rawValue: rawValue) { + return language + } + let language = Locale(identifier: rawValue).language + guard language.languageCode?.identifier == "zh", + language.script?.identifier == "Hans" + else { return .system } + + store(.simplifiedChinese, in: defaults) + return .simplifiedChinese } /// Keeps the app's explicit choice separate from Apple's per-app language @@ -58,7 +67,7 @@ public nonisolated enum AppLanguage: String, CaseIterable, Identifiable, Sendabl switch language { case .system: defaults.removeObject(forKey: "AppleLanguages") - case .english, .simplifiedChineseChina: + case .english, .simplifiedChinese: defaults.set([language.rawValue], forKey: "AppleLanguages") } } @@ -67,7 +76,7 @@ public nonisolated enum AppLanguage: String, CaseIterable, Identifiable, Sendabl switch self { case .system: .autoupdatingCurrent case .english: Locale(identifier: "en") - case .simplifiedChineseChina: Locale(identifier: rawValue) + case .simplifiedChinese: Locale(identifier: rawValue) } } } diff --git a/Scripts/run-screenshot-demo.sh b/Scripts/run-screenshot-demo.sh index 4c1e0b34..44680276 100755 --- a/Scripts/run-screenshot-demo.sh +++ b/Scripts/run-screenshot-demo.sh @@ -30,7 +30,7 @@ case "$language" in ;; zh) apple_language="zh-Hans" - apple_locale="zh_CN" + apple_locale="zh-Hans" ;; *) print -u2 "Unknown Demo language: $language" diff --git a/TFI/Features/Settings/MobileAppSettingsView.swift b/TFI/Features/Settings/MobileAppSettingsView.swift index 27565d9b..907e9816 100644 --- a/TFI/Features/Settings/MobileAppSettingsView.swift +++ b/TFI/Features/Settings/MobileAppSettingsView.swift @@ -6,6 +6,19 @@ struct MobileAppSettingsView: View { @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 { Section("Appearance") { @@ -14,10 +27,10 @@ struct MobileAppSettingsView: View { Text("Light").tag("light") Text("Dark").tag("dark") } - Picker("Language", selection: $language) { - ForEach(AppLanguage.allCases) { Text($0.displayName).tag($0.rawValue) } + Picker("Language", selection: languageSelection) { + ForEach(AppLanguage.allCases) { Text($0.displayName).tag($0) } } - if language != launchLanguage.rawValue { + if selectedLanguage != launchLanguage { Text("The new language will be used after Termind relaunches.").font(.caption).foregroundStyle( .secondary) } @@ -43,6 +56,5 @@ struct MobileAppSettingsView: View { } } .navigationTitle("App") - .onChange(of: language) { _, value in AppLanguage.store(AppLanguage(rawValue: value) ?? .system) } } } diff --git a/TFI/Features/Workspaces/MobileWorkspace.swift b/TFI/Features/Workspaces/MobileWorkspace.swift index 567d2039..9acee43e 100644 --- a/TFI/Features/Workspaces/MobileWorkspace.swift +++ b/TFI/Features/Workspaces/MobileWorkspace.swift @@ -1138,14 +1138,17 @@ final class MobileWorkspace: Identifiable { operationError = String(localized: "The SFTP connection is not available.") return false } + let connectionGeneration = generation do { - guard let file = try await sftp.attributes(at: path), !file.isDirectory else { + let file = try await sftp.attributes(at: path) + guard generation == connectionGeneration, connected else { return false } + guard let file, !file.isDirectory else { operationError = String(localized: "The remote file is no longer available.") return false } return download(file) } catch { - if !handleSFTPConnectionError(error, connection: generation) { + if !handleSFTPConnectionError(error, connection: connectionGeneration) { operationError = error.localizedDescription } return false diff --git a/TFITests/MobileAssistantRenderingTests.swift b/TFITests/MobileAssistantRenderingTests.swift index 5cce4b1a..7611e4df 100644 --- a/TFITests/MobileAssistantRenderingTests.swift +++ b/TFITests/MobileAssistantRenderingTests.swift @@ -13,7 +13,7 @@ struct MobileAssistantRenderingTests { guard ProcessInfo.processInfo.environment["TERMIND_ASSISTANT_SCREENSHOTS"] == "1" else { return } for language in ["en", "zh-Hans"] { let app = try MobileAppModel(inMemory: true) - let conversation = ConversationModel.demo(language: language == "en" ? .english : .simplifiedChineseChina) + let conversation = ConversationModel.demo(language: language == "en" ? .english : .simplifiedChinese) app.assistant.library.insertAndSelect(conversation) conversation.composer.draft = language == "en" ? "Check the service logs first." : "先检查服务日志。" conversation.attach(ContextChip(kind: .file, label: "service.log", content: "Example log")) diff --git a/TFM/App/ScreenshotDemo.swift b/TFM/App/ScreenshotDemo.swift index 789ca1b1..2e846363 100644 --- a/TFM/App/ScreenshotDemo.swift +++ b/TFM/App/ScreenshotDemo.swift @@ -78,7 +78,7 @@ import TermindApplication let language: AppLanguage switch languageValue { case "en": language = .english - case "zh": language = .simplifiedChineseChina + case "zh": language = .simplifiedChinese default: throw ParseError.unknownLanguage(languageValue) } return Self(scene: scene, language: language) @@ -267,7 +267,7 @@ import TermindApplication } private func text(en: String, zh: String) -> String { - language == .simplifiedChineseChina ? zh : en + language == .simplifiedChinese ? zh : en } } #endif diff --git a/TFM/Application/FeatureModels/HostLibraryModel+Demo.swift b/TFM/Application/FeatureModels/HostLibraryModel+Demo.swift index af970a80..38a4c211 100644 --- a/TFM/Application/FeatureModels/HostLibraryModel+Demo.swift +++ b/TFM/Application/FeatureModels/HostLibraryModel+Demo.swift @@ -11,7 +11,7 @@ extension HostLibraryModel { /// persistence or networking exists. static func demo(language: AppLanguage = .english) -> HostLibraryModel { func text(_ en: String, _ zh: String) -> String { - language == .simplifiedChineseChina ? zh : en + language == .simplifiedChinese ? zh : en } func distribution(_ id: String) -> DetectedDistribution { guard let distribution = DetectedDistribution(id: id) else { diff --git a/TFM/Features/Settings/SettingsPageView.swift b/TFM/Features/Settings/SettingsPageView.swift index c5806eba..381137f9 100644 --- a/TFM/Features/Settings/SettingsPageView.swift +++ b/TFM/Features/Settings/SettingsPageView.swift @@ -360,7 +360,16 @@ private struct AppSettings: View { @State private var launchLanguage = AppLanguage.stored() private var selectedLanguage: AppLanguage { - AppLanguage(rawValue: languageID) ?? .system + AppLanguage(rawValue: languageID) ?? AppLanguage.stored() + } + + private var languageSelection: Binding { + Binding( + get: { selectedLanguage }, + set: { language in + languageID = language.rawValue + AppLanguage.store(language) + }) } var body: some View { @@ -378,9 +387,9 @@ private struct AppSettings: View { } } - Picker("Language", selection: $languageID) { + Picker("Language", selection: languageSelection) { ForEach(AppLanguage.allCases) { language in - Text(language.displayName).tag(language.rawValue) + Text(language.displayName).tag(language) } } @@ -393,11 +402,6 @@ private struct AppSettings: View { } } } - .onChange(of: languageID) { - let language = AppLanguage(rawValue: languageID) ?? .system - if language.rawValue != languageID { languageID = language.rawValue } - AppLanguage.store(language) - } } } diff --git a/TFMTests/AppLanguageTests.swift b/TFMTests/AppLanguageTests.swift index 38839a1f..d7eca288 100644 --- a/TFMTests/AppLanguageTests.swift +++ b/TFMTests/AppLanguageTests.swift @@ -20,17 +20,38 @@ struct AppLanguageTests { } @Test - func explicitLanguagesWriteFullAppleLanguageTags() throws { + func explicitLanguagesWriteCanonicalLanguageTags() throws { let defaults = try makeDefaults() AppLanguage.store(.english, in: defaults) #expect(AppLanguage.stored(in: defaults) == .english) #expect(defaults.stringArray(forKey: "AppleLanguages") == ["en"]) - AppLanguage.store(.simplifiedChineseChina, in: defaults) - #expect(AppLanguage.stored(in: defaults) == .simplifiedChineseChina) - #expect(defaults.stringArray(forKey: "AppleLanguages") == ["zh-Hans-CN"]) - #expect(AppLanguage.simplifiedChineseChina.locale.identifier == "zh-Hans-CN") + AppLanguage.store(.simplifiedChinese, in: defaults) + #expect(AppLanguage.stored(in: defaults) == .simplifiedChinese) + #expect(defaults.stringArray(forKey: "AppleLanguages") == ["zh-Hans"]) + #expect(AppLanguage.simplifiedChinese.locale.identifier == "zh-Hans") + #expect(AppLanguage.simplifiedChinese.displayName == "简体中文") + } + + @Test(arguments: ["zh-Hans-CN", "zh-CN", "zh_CN", "zh-Hans-SG", "zh"]) + func legacySimplifiedChinesePreferencesMigrateToCanonicalTag(identifier: String) throws { + let defaults = try makeDefaults() + defaults.set(identifier, forKey: SettingsKey.appLanguage) + defaults.set([identifier], forKey: "AppleLanguages") + + #expect(AppLanguage.stored(in: defaults) == .simplifiedChinese) + #expect(defaults.string(forKey: SettingsKey.appLanguage) == "zh-Hans") + #expect(defaults.stringArray(forKey: "AppleLanguages") == ["zh-Hans"]) + } + + @Test(arguments: ["zh-Hant", "unsupported"]) + func nonSimplifiedChinesePreferencesContinueToFollowSystem(identifier: String) throws { + let defaults = try makeDefaults() + defaults.set(identifier, forKey: SettingsKey.appLanguage) + + #expect(AppLanguage.stored(in: defaults) == .system) + #expect(defaults.string(forKey: SettingsKey.appLanguage) == identifier) } @Test @@ -38,7 +59,7 @@ struct AppLanguageTests { let suiteName = "AppLanguageTests.\(UUID().uuidString)" let defaults = try #require(UserDefaults(suiteName: suiteName)) defaults.removePersistentDomain(forName: suiteName) - defaults.set(["zh-Hans-SG"], forKey: "AppleLanguages") + defaults.set(["zh-Hans"], forKey: "AppleLanguages") AppLanguage.store(.system, in: defaults) diff --git a/TFMTests/ChatMessageTimestampTests.swift b/TFMTests/ChatMessageTimestampTests.swift index 5b49809e..a2991ab3 100644 --- a/TFMTests/ChatMessageTimestampTests.swift +++ b/TFMTests/ChatMessageTimestampTests.swift @@ -14,7 +14,7 @@ struct ChatMessageTimestampTests { @Test func todayUsesOnlyTheLocalizedShortTime() throws { let timeZone = try #require(TimeZone(identifier: "Asia/Shanghai")) - let locale = Locale(identifier: "zh_CN") + let locale = Locale(identifier: "zh-Hans") var calendar = Calendar(identifier: .gregorian) calendar.timeZone = timeZone let message = try #require( diff --git a/TFMTests/ScreenshotDemoTests.swift b/TFMTests/ScreenshotDemoTests.swift index e5bc5e89..aef2ed77 100644 --- a/TFMTests/ScreenshotDemoTests.swift +++ b/TFMTests/ScreenshotDemoTests.swift @@ -16,7 +16,7 @@ import TermindRemoteInfrastructure func parsesEverySceneAndLanguageCombination() throws { let languages: [(argument: String, language: AppLanguage)] = [ ("en", .english), - ("zh", .simplifiedChineseChina), + ("zh", .simplifiedChinese), ] for scene in ScreenshotDemoConfiguration.Scene.allCases { @@ -90,7 +90,7 @@ import TermindRemoteInfrastructure @Test func appSettingsSceneUsesTheProductionSettingsRouteInBothLanguages() { - for language in [AppLanguage.english, .simplifiedChineseChina] { + for language in [AppLanguage.english, .simplifiedChinese] { let configuration = ScreenshotDemoConfiguration(scene: .appSettings, language: language) let window = configuration.makeWindow(hosts: .demo()) #expect(window.selectedDestination == .settings) diff --git a/TFMTests/WindowToolbarFindTests.swift b/TFMTests/WindowToolbarFindTests.swift index 3bcca4f3..a4240198 100644 --- a/TFMTests/WindowToolbarFindTests.swift +++ b/TFMTests/WindowToolbarFindTests.swift @@ -189,7 +189,7 @@ struct WindowToolbarFindTests { let window = WindowModel() window.selectAssistant() - let locale = Locale(identifier: "zh-Hans-CN") + let locale = Locale(identifier: "zh-Hans") #expect(VaultWindowTitleProvider().title(in: window, locale: locale) == nil) #expect(AssistantWindowTitleProvider().title(in: window, locale: locale) != nil) } @@ -203,7 +203,7 @@ struct WindowToolbarFindTests { store.insert(conversation) let window = WindowModel(assistantLibrary: AssistantLibraryModel(store: store)) let title = AssistantWindowTitleProvider() - let locale = Locale(identifier: "zh-Hans-CN") + let locale = Locale(identifier: "zh-Hans") window.selectAssistant() #expect(title.title(in: window, locale: locale) == AppLocalization.string("New Chat", locale: locale)) @@ -218,7 +218,7 @@ struct WindowToolbarFindTests { @Test func workspaceTitleLocalizesOnlyBuiltInFallbacks() { let store = HostLibraryModel() - let locale = Locale(identifier: "zh-Hans-CN") + let locale = Locale(identifier: "zh-Hans") let first = Pane(.empty) let second = Pane(.empty) let workspace = Workspace(pane: first)