diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index ad1e5d4f..8b2e7e5c 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -33,6 +33,10 @@ 3ACB685F297633BC00C46468 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3ACB685D297633BC00C46468 /* Localizable.strings */; }; 3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */; }; 3AE45AF6297BB2E700C1D842 /* LibreTranslateServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */; }; + 3AF9B5A82CA0B5CF0021A08E /* LanguageSortComparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF9B5A72CA0B5CF0021A08E /* LanguageSortComparator.swift */; }; + 3AF9B5A92CA0B5CF0021A08E /* LanguageSortComparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF9B5A72CA0B5CF0021A08E /* LanguageSortComparator.swift */; }; + 3AF9B5AB2CA0D2D90021A08E /* AppleTranslationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF9B5AA2CA0D2D90021A08E /* AppleTranslationSettingsView.swift */; }; + 3AF9B5AC2CA0D2D90021A08E /* AppleTranslationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF9B5AA2CA0D2D90021A08E /* AppleTranslationSettingsView.swift */; }; 3CCD1E6A2A874C4E0099A953 /* Nip98HTTPAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCD1E692A874C4E0099A953 /* Nip98HTTPAuth.swift */; }; 4C011B5E2BD0A56A002F2F9B /* ChatEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C011B5C2BD0A56A002F2F9B /* ChatEventView.swift */; }; 4C011B5F2BD0A56A002F2F9B /* ChatroomThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C011B5D2BD0A56A002F2F9B /* ChatroomThreadView.swift */; }; @@ -1332,6 +1336,8 @@ 3AF6336829884C6B0005672A /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/InfoPlist.strings"; sourceTree = ""; }; 3AF6336929884C6B0005672A /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/Localizable.strings"; sourceTree = ""; }; 3AF6336A29884C6B0005672A /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "pt-PT"; path = "pt-PT.lproj/Localizable.stringsdict"; sourceTree = ""; }; + 3AF9B5A72CA0B5CF0021A08E /* LanguageSortComparator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageSortComparator.swift; sourceTree = ""; }; + 3AF9B5AA2CA0D2D90021A08E /* AppleTranslationSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleTranslationSettingsView.swift; sourceTree = ""; }; 3CCD1E692A874C4E0099A953 /* Nip98HTTPAuth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Nip98HTTPAuth.swift; sourceTree = ""; }; 4C011B5C2BD0A56A002F2F9B /* ChatEventView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatEventView.swift; sourceTree = ""; }; 4C011B5D2BD0A56A002F2F9B /* ChatroomThreadView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatroomThreadView.swift; sourceTree = ""; }; @@ -2276,6 +2282,7 @@ 4C1A9A2029DDD3E100516EAC /* KeySettingsView.swift */, 4C1A9A2429DDDF2600516EAC /* ZapSettingsView.swift */, 4C1A9A2629DDE31900516EAC /* TranslationSettingsView.swift */, + 3AF9B5AA2CA0D2D90021A08E /* AppleTranslationSettingsView.swift */, E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */, 5053ACA62A56DF3B00851AE3 /* DeveloperSettingsView.swift */, D7FD12252BD345A700CF195B /* FirstAidSettingsView.swift */, @@ -2743,6 +2750,7 @@ D74AAFCE2B155D8C006CF0F4 /* ZapDataModel.swift */, D74AAFD32B155ECB006CF0F4 /* Zaps+.swift */, D74AAFD52B155F0C006CF0F4 /* WalletConnect+.swift */, + 3AF9B5A72CA0B5CF0021A08E /* LanguageSortComparator.swift */, ); path = Util; sourceTree = ""; @@ -3774,6 +3782,7 @@ 5C8711DE2C460C06007879C2 /* PostingTimelineView.swift in Sources */, 4CA9275D2A28FF630098A105 /* LongformView.swift in Sources */, 4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */, + 3AF9B5AC2CA0D2D90021A08E /* AppleTranslationSettingsView.swift in Sources */, D7EDED1E2B11797D0018B19C /* LongformEvent.swift in Sources */, 504323A92A3495B6006AE6DC /* RelayModelCache.swift in Sources */, 5C4D9EA72C042FA5005EA0F7 /* HighlightDraftContentView.swift in Sources */, @@ -3971,6 +3980,7 @@ 4C7D09622A098D0E00943473 /* WalletConnect.swift in Sources */, 4C3EA67928FF7ABF00C48A62 /* list.c in Sources */, 4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */, + 3AF9B5A92CA0B5CF0021A08E /* LanguageSortComparator.swift in Sources */, 4C12535E2A76CA870004F4B8 /* SwitchedTimelineNotify.swift in Sources */, D74F430A2B23F0BE00425B75 /* DamusPurple.swift in Sources */, 9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */, @@ -4355,6 +4365,7 @@ D73E5E7E2C6A97F4007EB227 /* Hashtags.swift in Sources */, D73E5E7F2C6A97F4007EB227 /* LocalNotification.swift in Sources */, D73E5E802C6A97F4007EB227 /* CredentialHandler.swift in Sources */, + 3AF9B5AB2CA0D2D90021A08E /* AppleTranslationSettingsView.swift in Sources */, D73E5E812C6A97F4007EB227 /* KeyboardVisible.swift in Sources */, D73E5E832C6A97F4007EB227 /* AVPlayer+Additions.swift in Sources */, D73E5E842C6A97F4007EB227 /* Zaps+.swift in Sources */, @@ -4449,6 +4460,7 @@ D73E5EDE2C6A97F4007EB227 /* AppearanceSettingsView.swift in Sources */, D73E5EDF2C6A97F4007EB227 /* KeySettingsView.swift in Sources */, D73E5EE02C6A97F4007EB227 /* ZapSettingsView.swift in Sources */, + 3AF9B5A82CA0B5CF0021A08E /* LanguageSortComparator.swift in Sources */, D73E5F792C6A9C4C007EB227 /* HomeModel.swift in Sources */, D73E5EE12C6A97F4007EB227 /* TranslationSettingsView.swift in Sources */, D73E5EE22C6A97F4007EB227 /* SearchSettingsView.swift in Sources */, diff --git a/damus/Util/LanguageSortComparator.swift b/damus/Util/LanguageSortComparator.swift new file mode 100644 index 00000000..1b6f0963 --- /dev/null +++ b/damus/Util/LanguageSortComparator.swift @@ -0,0 +1,51 @@ +// +// LanguageSortComparator.swift +// damus +// +// Created by Terry Yiu on 9/22/24. +// + +import Foundation + +struct LanguageSortComparator: SortComparator { + var order: SortOrder + + func compare(_ lhs: Locale.Language, _ rhs: Locale.Language) -> ComparisonResult { + let comparisonResult = compareForward(lhs, rhs) + switch order { + case .forward: + return comparisonResult + case .reverse: + switch comparisonResult { + case .orderedAscending: + return .orderedDescending + case .orderedDescending: + return .orderedAscending + case .orderedSame: + return .orderedSame + } + } + } + + private func compareForward(_ lhs: Locale.Language, _ rhs: Locale.Language) -> ComparisonResult { + let currentLocale = Locale.current + let localizedLhs = currentLocale.localizedString(forLanguage: lhs) + let localizedRhs = currentLocale.localizedString(forLanguage: rhs) + + return localizedLhs.localizedCompare(localizedRhs) + } +} + +extension Locale { + func localizedString(forLanguage language: Locale.Language) -> String { + guard let languageCode = language.languageCode, let localizedLanguageCode = localizedString(forLanguageCode: languageCode.identifier) else { + return language.languageCode?.identifier ?? language.minimalIdentifier + } + + if let region = language.region, let localizedRegion = localizedString(forRegionCode: region.identifier) { + return "\(localizedLanguageCode) (\(localizedRegion))" + } else { + return localizedLanguageCode + } + } +} diff --git a/damus/Views/Settings/AppleTranslationSettingsView.swift b/damus/Views/Settings/AppleTranslationSettingsView.swift new file mode 100644 index 00000000..0e41b681 --- /dev/null +++ b/damus/Views/Settings/AppleTranslationSettingsView.swift @@ -0,0 +1,90 @@ +// +// AppleTranslationSettingsView.swift +// damus +// +// Created by Terry Yiu on 9/22/24. +// + +import SwiftUI +import Translation + +@available(iOS 18.0, *) +struct AppleTranslationSettingsView: View { + @ObservedObject var settings: UserSettingsStore + + @State private var supportedLanguages: [Locale.Language] = [] + @State private var installedLanguages = Set() + @State private var installingLanguages = Set() + @State private var languageTranslationConfigurations = [Locale.Language: TranslationSession.Configuration]() + + var body: some View { + if settings.translation_service == .none { + if !installedLanguages.isEmpty { + Section(NSLocalizedString("Available Offline", comment: "Section for downloaded languages for Apple offline translations.")) { + ForEach(installedLanguages.sorted(using: LanguageSortComparator(order: .forward)), id: \.self) { language in + Text(Locale.current.localizedString(forLanguage: language)) + } + } + } + + Section(NSLocalizedString("Languages Available for Download", comment: "Section for downloadable languages for Apple offline translations.")) { + ForEach(supportedLanguages.filter { !installedLanguages.contains($0) }, id: \.self) { language in + HStack { + Text(Locale.current.localizedString(forLanguage: language)) + Button( + action: { + installingLanguages.insert(language) + languageTranslationConfigurations[language]?.invalidate() + }, + label: { + Image(systemName: "arrow.down.circle") + } + ) + } + .translationTask(languageTranslationConfigurations[language]) { session in + if installingLanguages.contains(language) { + do { + // Display a sheet asking the user's permission + // to start downloading the language pairing by + // translating a dummy string. + // + // We do not use `session.prepareTranslation()` because + // it does not throw errors as loudly as `session.translate` does, + // which helps us indicate when language download is complete. + _ = try await session.translate("A") + installedLanguages.insert(language) + installingLanguages.remove(language) + } catch { + // Handle any errors. + print("Error downloading language \(language): \(error)") + installingLanguages.remove(language) + } + } + } + } + } + .onAppear { + Task { + let languageAvailability = LanguageAvailability() + supportedLanguages = await languageAvailability.supportedLanguages + supportedLanguages.sort(using: LanguageSortComparator(order: .forward)) + installedLanguages.removeAll() + + for supportedLanguage in supportedLanguages { + let status = await languageAvailability.status(from: supportedLanguage, to: nil) + switch status { + case .installed: + installedLanguages.insert(supportedLanguage) + case .supported: + languageTranslationConfigurations[supportedLanguage] = TranslationSession.Configuration( + source: supportedLanguage + ) + default: + break + } + } + } + } + } + } +} diff --git a/damus/Views/Settings/TranslationSettingsView.swift b/damus/Views/Settings/TranslationSettingsView.swift index de4e95d3..2ddf8749 100644 --- a/damus/Views/Settings/TranslationSettingsView.swift +++ b/damus/Views/Settings/TranslationSettingsView.swift @@ -6,13 +6,14 @@ // import SwiftUI +import Translation struct TranslationSettingsView: View { @ObservedObject var settings: UserSettingsStore var damus_state: DamusState - @Environment(\.dismiss) var dismiss - + @Environment(\.dismiss) var dismiss + var body: some View { Form { Section(NSLocalizedString("Translations", comment: "Section title for selecting the translation service.")) { @@ -100,8 +101,15 @@ struct TranslationSettingsView: View { Toggle(NSLocalizedString("Translate DMs", comment: "Toggle to translate direct messages."), isOn: $settings.translate_dms) .toggleStyle(.switch) */ + } else if #available(iOS 18.0, *) { + Toggle(NSLocalizedString("Automatically translate notes", comment: "Toggle to automatically translate notes."), isOn: $settings.auto_translate) + .toggleStyle(.switch) } } + + if #available(iOS 18.0, *) { + AppleTranslationSettingsView(settings: settings) + } } .navigationTitle(NSLocalizedString("Translation", comment: "Navigation title for translation settings.")) .onReceive(handle_notify(.switched_timeline)) { _ in