Compare commits

...

2 Commits

Author SHA1 Message Date
f28b15e84a WIP 2024-10-04 19:00:16 +02:00
e2cf6ffab2 Add Apple offline language downloads in settings 2024-09-28 00:28:04 -04:00
9 changed files with 393 additions and 6 deletions

View File

@@ -33,6 +33,12 @@
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 */; };
3AEEB1FD2CAA3F7B004653F8 /* OfflineTranslateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AEEB1FC2CAA3F7B004653F8 /* OfflineTranslateView.swift */; };
3AEEB1FE2CAA3F7B004653F8 /* OfflineTranslateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AEEB1FC2CAA3F7B004653F8 /* OfflineTranslateView.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 */; };
@@ -1329,9 +1335,12 @@
3AEB8003297CCEA800713A25 /* tr-TR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "tr-TR"; path = "tr-TR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3AEB8004297CCEA800713A25 /* tr-TR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "tr-TR"; path = "tr-TR.lproj/Localizable.strings"; sourceTree = "<group>"; };
3AEB8005297CCEA900713A25 /* tr-TR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "tr-TR"; path = "tr-TR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3AEEB1FC2CAA3F7B004653F8 /* OfflineTranslateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfflineTranslateView.swift; sourceTree = "<group>"; };
3AF6336829884C6B0005672A /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3AF6336929884C6B0005672A /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/Localizable.strings"; sourceTree = "<group>"; };
3AF6336A29884C6B0005672A /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "pt-PT"; path = "pt-PT.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3AF9B5A72CA0B5CF0021A08E /* LanguageSortComparator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageSortComparator.swift; sourceTree = "<group>"; };
3AF9B5AA2CA0D2D90021A08E /* AppleTranslationSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleTranslationSettingsView.swift; sourceTree = "<group>"; };
3CCD1E692A874C4E0099A953 /* Nip98HTTPAuth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Nip98HTTPAuth.swift; sourceTree = "<group>"; };
4C011B5C2BD0A56A002F2F9B /* ChatEventView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatEventView.swift; sourceTree = "<group>"; };
4C011B5D2BD0A56A002F2F9B /* ChatroomThreadView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatroomThreadView.swift; sourceTree = "<group>"; };
@@ -2276,6 +2285,7 @@
4C1A9A2029DDD3E100516EAC /* KeySettingsView.swift */,
4C1A9A2429DDDF2600516EAC /* ZapSettingsView.swift */,
4C1A9A2629DDE31900516EAC /* TranslationSettingsView.swift */,
3AF9B5AA2CA0D2D90021A08E /* AppleTranslationSettingsView.swift */,
E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */,
5053ACA62A56DF3B00851AE3 /* DeveloperSettingsView.swift */,
D7FD12252BD345A700CF195B /* FirstAidSettingsView.swift */,
@@ -2743,6 +2753,7 @@
D74AAFCE2B155D8C006CF0F4 /* ZapDataModel.swift */,
D74AAFD32B155ECB006CF0F4 /* Zaps+.swift */,
D74AAFD52B155F0C006CF0F4 /* WalletConnect+.swift */,
3AF9B5A72CA0B5CF0021A08E /* LanguageSortComparator.swift */,
);
path = Util;
sourceTree = "<group>";
@@ -3007,6 +3018,7 @@
4C28595F2A12A2BE004746F7 /* SupporterBadge.swift */,
5C6E1DAC2A193EC2008FC15A /* GradientButtonStyle.swift */,
5CC868DC2AA29B3200FB22BA /* NeutralButtonStyle.swift */,
3AEEB1FC2CAA3F7B004653F8 /* OfflineTranslateView.swift */,
);
path = Components;
sourceTree = "<group>";
@@ -3774,6 +3786,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 */,
@@ -3849,6 +3862,7 @@
4CB8838F296F781C00DC99E7 /* ReactionsView.swift in Sources */,
B5C60C202B530D5100C5ECA7 /* MuteItem.swift in Sources */,
4C75EFB328049D640006080F /* NostrEvent.swift in Sources */,
3AEEB1FE2CAA3F7B004653F8 /* OfflineTranslateView.swift in Sources */,
4C32B9582A9AD44700DC3548 /* VeriferOptions.swift in Sources */,
D74AAFC22B153395006CF0F4 /* HeadlessDamusState.swift in Sources */,
4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */,
@@ -3971,6 +3985,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 +4370,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 +4465,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 */,
@@ -4529,6 +4546,7 @@
D73E5F2B2C6A97F4007EB227 /* ReplyPart.swift in Sources */,
D73E5F2C2C6A97F4007EB227 /* ProxyView.swift in Sources */,
D73E5F2D2C6A97F4007EB227 /* SelectedEventView.swift in Sources */,
3AEEB1FD2CAA3F7B004653F8 /* OfflineTranslateView.swift in Sources */,
D73E5F2E2C6A97F4007EB227 /* EventBody.swift in Sources */,
D73E5F302C6A97F4007EB227 /* EventProfile.swift in Sources */,
D73E5F312C6A97F4007EB227 /* EventMenu.swift in Sources */,

View File

@@ -0,0 +1,168 @@
//
// OfflineTranslateView.swift
// damus
//
// Created by Terry Yiu on 9/29/24.
//
import SwiftUI
import SwiftUI
import NaturalLanguage
import Translation
fileprivate let MIN_UNIQUE_CHARS = 2
@available(iOS 18.0, macOS 15.0, *)
@available(macCatalyst, unavailable)
struct OfflineTranslateView: View {
let damus_state: DamusState
let event: NostrEvent
let size: EventViewKind
@ObservedObject var translations_model: TranslationModel
@State private var translationConfiguration: TranslationSession.Configuration?
// @State private var languageStatus: LanguageAvailability.Status?
init(damus_state: DamusState, event: NostrEvent, size: EventViewKind) {
self.damus_state = damus_state
self.event = event
self.size = size
self._translations_model = ObservedObject(wrappedValue: damus_state.events.get_cache_data(event.id).translations_model)
}
var TranslateButton: some View {
Button(NSLocalizedString("Translate Note", comment: "Button to translate note from different language.")) {
translate()
}
.translate_button_style()
}
func TranslatedView(lang: String?, artifacts: NoteArtifactsSeparated, font_size: Double) -> some View {
return VStack(alignment: .leading) {
let translatedFromLanguageString = String(format: NSLocalizedString("Translated from %@", comment: "Button to indicate that the note has been translated from a different language."), lang ?? "ja")
Text(translatedFromLanguageString)
.foregroundColor(.gray)
.font(.footnote)
.padding([.top, .bottom], 10)
if self.size == .selected {
SelectableText(damus_state: damus_state, event: event, attributedString: artifacts.content.attributed, size: self.size)
} else {
artifacts.content.text
.font(eventviewsize_to_font(self.size, font_size: font_size))
}
}
}
func translate() {
guard /*let languageStatus, */translations_model.state == .havent_tried && damus_state.settings.translation_service == .none && damus_state.settings.translate_offline/* && languageStatus != .unsupported*/, let note_language = translations_model.note_language else {
return
}
guard translationConfiguration == nil else {
translationConfiguration?.invalidate()
return
}
translationConfiguration = TranslationSession.Configuration(
source: Locale.Language(identifier: note_language))
}
// func setLanguageStatus() async {
// guard languageStatus == nil else {
// return
// }
//
// guard let note_language = translations_model.note_language else {
// languageStatus = .unsupported
// return
// }
//
// let languageAvailability = LanguageAvailability()
// let language = Locale.Language(identifier: note_language)
// languageStatus = await languageAvailability.status(from: language, to: nil)
// }
var body: some View {
if let note_lang = translations_model.note_language, damus_state.settings.translation_service == .none && damus_state.settings.translate_offline && should_translate(event: event, our_keypair: damus_state.keypair, note_lang: note_lang) {
Group {
switch self.translations_model.state {
case .havent_tried:
if damus_state.settings.auto_translate/* && languageStatus == .installed*/ {
Text("")
} else {
TranslateButton
}
case .translating:
Text("")
case .translated(let translated):
let languageName = Locale.current.localizedString(forLanguageCode: translated.language)
TranslatedView(lang: languageName, artifacts: translated.artifacts, font_size: damus_state.settings.font_size)
case .not_needed:
Text("")
}
}
.onAppear {
// Task { @MainActor in
// await setLanguageStatus()
// }
translate()
}
.translationTask(translationConfiguration) { translationSession in
Task { @MainActor in
do {
guard let note_language = translations_model.note_language, translations_model.state == .havent_tried/*, languageStatus != .unsupported*/ else {
return
}
translations_model.state = .translating
let originalContent = event.get_content(damus_state.keypair)
let response = try await translationSession.translate(originalContent)
let translated_note = response.targetText
guard originalContent != translated_note else {
// if its the same, give up and don't retry
translations_model.state = .not_needed
return
}
guard translationMeetsStringDistanceRequirements(original: originalContent, translated: translated_note) else {
translations_model.state = .not_needed
return
}
// Render translated note
let translated_blocks = parse_note_content(content: .content(translated_note, event.tags))
let artifacts = render_blocks(blocks: translated_blocks, profiles: damus_state.profiles)
// and cache it
translations_model.state = .translated(Translated(artifacts: artifacts, language: note_language))
} catch {
// code to handle error
print("Error translating note: \(error.localizedDescription)")
translations_model.state = .not_needed
}
}
}
} else {
Text("")
}
}
func translationMeetsStringDistanceRequirements(original: String, translated: String) -> Bool {
return levenshteinDistanceIsGreaterThanOrEqualTo(from: original, to: translated, threshold: MIN_UNIQUE_CHARS)
}
}
@available(iOS 18.0, macOS 15.0, *)
@available(macCatalyst, unavailable)
struct OfflineTranslateView_Previews: PreviewProvider {
static var previews: some View {
let ds = test_damus_state
OfflineTranslateView(damus_state: ds, event: test_note, size: .normal)
}
}

View File

@@ -85,7 +85,7 @@ struct TranslateView: View {
return false
}
if TranslationService.isAppleTranslationPopoverSupported {
if TranslationService.isAppleTranslationSupported {
return damus_state.settings.translation_service == .none || damus_state.settings.can_translate
} else {
return damus_state.settings.can_translate

View File

@@ -39,7 +39,7 @@ enum TranslationService: String, CaseIterable, Identifiable, StringCodable {
switch self {
case .none:
let displayName: String
if TranslationService.isAppleTranslationPopoverSupported {
if TranslationService.isAppleTranslationSupported {
displayName = NSLocalizedString("apple_translation_service", value: "Apple", comment: "Dropdown option for selecting Apple as a translation service.")
} else {
displayName = NSLocalizedString("none_translation_service", value: "None", comment: "Dropdown option for selecting no translation service.")
@@ -58,11 +58,15 @@ enum TranslationService: String, CaseIterable, Identifiable, StringCodable {
}
}
static var isAppleTranslationPopoverSupported: Bool {
static var isAppleTranslationSupported: Bool {
#if targetEnvironment(macCatalyst)
return false
#else
if #available(iOS 17.4, macOS 14.4, *) {
return true
} else {
return false
}
#endif
}
}

View File

@@ -180,6 +180,9 @@ class UserSettingsStore: ObservableObject {
@Setting(key: "auto_translate", default_value: true)
var auto_translate: Bool
@Setting(key: "translate_offline", default_value: true)
var translate_offline: Bool
@Setting(key: "show_general_statuses", default_value: true)
var show_general_statuses: Bool

View File

@@ -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
}
}
}

View File

@@ -99,7 +99,17 @@ struct NoteContentView: View {
}
var translateView: some View {
#if targetEnvironment(macCatalyst)
TranslateView(damus_state: damus_state, event: event, size: self.size, isAppleTranslationPopoverPresented: $isAppleTranslationPopoverPresented)
#else
if #available(iOS 18.0, macOS 15.0, *), damus_state.settings.translate_offline {
AnyView(OfflineTranslateView(damus_state: damus_state, event: event, size: self.size))
} else {
AnyView(
TranslateView(damus_state: damus_state, event: event, size: self.size, isAppleTranslationPopoverPresented: $isAppleTranslationPopoverPresented)
)
}
#endif
}
func previewView(links: [URL]) -> some View {
@@ -148,7 +158,7 @@ struct NoteContentView: View {
}
}
if !options.contains(.no_translate) && (size == .selected || TranslationService.isAppleTranslationPopoverSupported || damus_state.settings.auto_translate) {
if !options.contains(.no_translate) && (size == .selected || TranslationService.isAppleTranslationSupported || damus_state.settings.auto_translate) {
if with_padding {
translateView
.padding(.horizontal)
@@ -301,9 +311,13 @@ struct NoteContentView: View {
Markdown(md.markdown)
.padding([.leading, .trailing, .top])
case .separated(let separated):
if #available(iOS 17.4, macOS 14.4, *) {
if #available(iOS 18.0, macOS 15.0, *), damus_state.settings.auto_translate {
MainContent(artifacts: separated)
} else if #available(iOS 17.4, macOS 14.4, *) {
MainContent(artifacts: separated)
#if !targetEnvironment(macCatalyst)
.translationPresentation(isPresented: $isAppleTranslationPopoverPresented, text: event.get_content(damus_state.keypair))
#endif
} else {
MainContent(artifacts: separated)
}

View File

@@ -0,0 +1,91 @@
//
// AppleTranslationSettingsView.swift
// damus
//
// Created by Terry Yiu on 9/22/24.
//
import SwiftUI
import Translation
@available(iOS 18.0, macOS 15.0, *)
@available(macCatalyst, unavailable)
struct AppleTranslationSettingsView: View {
@ObservedObject var settings: UserSettingsStore
@State private var supportedLanguages: [Locale.Language] = []
@State private var installedLanguages = Set<Locale.Language>()
@State private var installingLanguages = Set<Locale.Language>()
@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
}
}
}
}
}
}
}

View File

@@ -6,13 +6,14 @@
//
import SwiftUI
import Translation
struct TranslationSettingsView: View {
@ObservedObject var settings: UserSettingsStore
var damus_state: DamusState
@Environment(\.dismiss) var dismiss
var body: some View {
Form {
Section(NSLocalizedString("Translations", comment: "Section title for selecting the translation service.")) {
@@ -102,11 +103,48 @@ struct TranslationSettingsView: View {
*/
}
}
#if !targetEnvironment(macCatalyst)
if #available(iOS 18.0, macOS 15.0, *), settings.translation_service == .none {
Toggle(NSLocalizedString("Automatically translate notes", comment: "Toggle to automatically translate notes."), isOn: $settings.auto_translate)
.toggleStyle(.switch)
Section (
content: {
Toggle(NSLocalizedString("On-Device Mode", comment: "Toggle to always translate offline using downloaded languages."), isOn: $settings.translate_offline)
}, footer: {
Text("Always translate offline using downloaded languages. Offline translations may not be as accurate as online translations. Apple may collect usage metrics, but this data does not include the original or translated content.", comment: "Section footer explaining the implications of enabling offline translations.")
}
)
AppleTranslationSettingsView(settings: settings)
}
#endif
}
.navigationTitle(NSLocalizedString("Translation", comment: "Navigation title for translation settings."))
.onReceive(handle_notify(.switched_timeline)) { _ in
dismiss()
}
.onChange(of: settings.auto_translate) { newValue in
// Apple automatic translation can occur only if offline translations are enabled.
if settings.translation_service == .none && newValue && !settings.translate_offline {
settings.translate_offline = true
}
}
.onChange(of: settings.translate_offline) { newValue in
// Apple automatic translation can occur only if offline translations are enabled.
if settings.translation_service == .none && !newValue && settings.auto_translate {
settings.auto_translate = false
}
}
.onChange(of: settings.translation_service) { newValue in
// Apple automatic translation can occur only if offline translations are enabled.
// If automatic translations are enabled for a non-Apple translation service,
// and then the translation service is switched to Apple, offline translations need to be enabled.
if newValue == .none && settings.auto_translate && !settings.translate_offline {
settings.translate_offline = true
}
}
}
}