Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
f28b15e84a
|
|||
|
e2cf6ffab2
|
|||
| 2a19d5d831 |
@@ -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 */,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -27,19 +27,26 @@ struct TranslateView: View {
|
||||
let damus_state: DamusState
|
||||
let event: NostrEvent
|
||||
let size: EventViewKind
|
||||
|
||||
|
||||
@Binding var isAppleTranslationPopoverPresented: Bool
|
||||
|
||||
@ObservedObject var translations_model: TranslationModel
|
||||
|
||||
init(damus_state: DamusState, event: NostrEvent, size: EventViewKind) {
|
||||
init(damus_state: DamusState, event: NostrEvent, size: EventViewKind, isAppleTranslationPopoverPresented: Binding<Bool>) {
|
||||
self.damus_state = damus_state
|
||||
self.event = event
|
||||
self.size = size
|
||||
self._isAppleTranslationPopoverPresented = isAppleTranslationPopoverPresented
|
||||
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()
|
||||
if damus_state.settings.translation_service == .none {
|
||||
isAppleTranslationPopoverPresented = true
|
||||
} else {
|
||||
translate()
|
||||
}
|
||||
}
|
||||
.translate_button_style()
|
||||
}
|
||||
@@ -74,17 +81,25 @@ struct TranslateView: View {
|
||||
}
|
||||
|
||||
func should_transl(_ note_lang: String) -> Bool {
|
||||
should_translate(event: event, our_keypair: damus_state.keypair, settings: damus_state.settings, note_lang: note_lang)
|
||||
guard should_translate(event: event, our_keypair: damus_state.keypair, note_lang: note_lang) else {
|
||||
return false
|
||||
}
|
||||
|
||||
if TranslationService.isAppleTranslationSupported {
|
||||
return damus_state.settings.translation_service == .none || damus_state.settings.can_translate
|
||||
} else {
|
||||
return damus_state.settings.can_translate
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
switch self.translations_model.state {
|
||||
case .havent_tried:
|
||||
if damus_state.settings.auto_translate {
|
||||
if damus_state.settings.auto_translate && damus_state.settings.translation_service != .none {
|
||||
Text("")
|
||||
} else if let note_lang = translations_model.note_language, should_transl(note_lang) {
|
||||
TranslateButton
|
||||
TranslateButton
|
||||
} else {
|
||||
Text("")
|
||||
}
|
||||
@@ -114,9 +129,11 @@ extension View {
|
||||
}
|
||||
|
||||
struct TranslateView_Previews: PreviewProvider {
|
||||
@State static var isAppleTranslationPopoverPresented: Bool = false
|
||||
|
||||
static var previews: some View {
|
||||
let ds = test_damus_state
|
||||
TranslateView(damus_state: ds, event: test_note, size: .normal)
|
||||
TranslateView(damus_state: ds, event: test_note, size: .normal, isAppleTranslationPopoverPresented: $isAppleTranslationPopoverPresented)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,13 @@ enum TranslationService: String, CaseIterable, Identifiable, StringCodable {
|
||||
var model: Model {
|
||||
switch self {
|
||||
case .none:
|
||||
return .init(tag: self.rawValue, displayName: NSLocalizedString("none_translation_service", value: "None", comment: "Dropdown option for selecting no translation service."))
|
||||
let displayName: String
|
||||
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.")
|
||||
}
|
||||
return .init(tag: self.rawValue, displayName: displayName)
|
||||
case .purple:
|
||||
return .init(tag: self.rawValue, displayName: NSLocalizedString("Damus Purple", comment: "Dropdown option for selecting Damus Purple as a translation service."))
|
||||
case .libretranslate:
|
||||
@@ -51,4 +57,16 @@ enum TranslationService: String, CaseIterable, Identifiable, StringCodable {
|
||||
return .init(tag: self.rawValue, displayName: NSLocalizedString("translate.nostr.wine (DeepL, Pay with BTC)", comment: "Dropdown option for selecting translate.nostr.wine as the translation service."))
|
||||
}
|
||||
}
|
||||
|
||||
static var isAppleTranslationSupported: Bool {
|
||||
#if targetEnvironment(macCatalyst)
|
||||
return false
|
||||
#else
|
||||
if #available(iOS 17.4, macOS 14.4, *) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+15
-11
@@ -244,16 +244,12 @@ class EventCache {
|
||||
}
|
||||
}
|
||||
|
||||
func should_translate(event: NostrEvent, our_keypair: Keypair, settings: UserSettingsStore, note_lang: String?) -> Bool {
|
||||
guard settings.can_translate else {
|
||||
return false
|
||||
}
|
||||
|
||||
func should_translate(event: NostrEvent, our_keypair: Keypair, note_lang: String?) -> Bool {
|
||||
// don't translate reposts, longform, etc
|
||||
if event.kind != 1 {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Do not translate self-authored notes if logged in with a private key
|
||||
// as we can assume the user can understand their own notes.
|
||||
// The detected language prediction could be incorrect and not in the list of preferred languages.
|
||||
@@ -261,25 +257,33 @@ func should_translate(event: NostrEvent, our_keypair: Keypair, settings: UserSet
|
||||
if our_keypair.privkey != nil && our_keypair.pubkey == event.pubkey {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
if let note_lang {
|
||||
let preferredLanguages = Set(Locale.preferredLanguages.map { localeToLanguage($0) })
|
||||
|
||||
|
||||
// Don't translate if its in our preferred languages
|
||||
guard !preferredLanguages.contains(note_lang) else {
|
||||
// if its the same, give up and don't retry
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// we should start translating if we have auto_translate on
|
||||
return true
|
||||
}
|
||||
|
||||
func can_and_should_translate(event: NostrEvent, our_keypair: Keypair, settings: UserSettingsStore, note_lang: String?) -> Bool {
|
||||
guard settings.can_translate else {
|
||||
return false
|
||||
}
|
||||
|
||||
return should_translate(event: event, our_keypair: our_keypair, note_lang: note_lang)
|
||||
}
|
||||
|
||||
func should_preload_translation(event: NostrEvent, our_keypair: Keypair, current_status: TranslateStatus, settings: UserSettingsStore, note_lang: String?) -> Bool {
|
||||
switch current_status {
|
||||
case .havent_tried:
|
||||
return should_translate(event: event, our_keypair: our_keypair, settings: settings, note_lang: note_lang) && settings.auto_translate
|
||||
return can_and_should_translate(event: event, our_keypair: our_keypair, settings: settings, note_lang: note_lang) && settings.auto_translate
|
||||
case .translating: return false
|
||||
case .translated: return false
|
||||
case .not_needed: return false
|
||||
@@ -413,7 +417,7 @@ func preload_event(plan: PreloadPlan, state: DamusState) async {
|
||||
|
||||
var translations: TranslateStatus? = nil
|
||||
// We have to recheck should_translate here now that we have note_language
|
||||
if plan.load_translations && should_translate(event: plan.event, our_keypair: our_keypair, settings: settings, note_lang: note_language) && settings.auto_translate
|
||||
if plan.load_translations && can_and_should_translate(event: plan.event, our_keypair: our_keypair, settings: settings, note_lang: note_language) && settings.auto_translate
|
||||
{
|
||||
translations = await translate_note(profiles: profiles, keypair: our_keypair, event: plan.event, settings: settings, note_lang: note_language, purple: state.purple)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import SwiftUI
|
||||
import LinkPresentation
|
||||
import NaturalLanguage
|
||||
import MarkdownUI
|
||||
import Translation
|
||||
|
||||
struct Blur: UIViewRepresentable {
|
||||
var style: UIBlurEffect.Style = .systemUltraThinMaterial
|
||||
@@ -32,6 +33,8 @@ struct NoteContentView: View {
|
||||
let preview_height: CGFloat?
|
||||
let options: EventViewOptions
|
||||
|
||||
@State var isAppleTranslationPopoverPresented: Bool = false
|
||||
|
||||
@ObservedObject var artifacts_model: NoteArtifactsModel
|
||||
@ObservedObject var preview_model: PreviewModel
|
||||
@ObservedObject var settings: UserSettingsStore
|
||||
@@ -96,7 +99,17 @@ struct NoteContentView: View {
|
||||
}
|
||||
|
||||
var translateView: some View {
|
||||
TranslateView(damus_state: damus_state, event: event, size: self.size)
|
||||
#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 {
|
||||
@@ -145,7 +158,7 @@ struct NoteContentView: View {
|
||||
}
|
||||
}
|
||||
|
||||
if !options.contains(.no_translate) && (size == .selected || 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)
|
||||
@@ -298,7 +311,16 @@ struct NoteContentView: View {
|
||||
Markdown(md.markdown)
|
||||
.padding([.leading, .trailing, .top])
|
||||
case .separated(let separated):
|
||||
MainContent(artifacts: separated)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user