commit ae82114a33e2df266490e9e91765ee632c597cff
parent 5a238502cb76a712f7dfa94917a2cd868aaf75e7
Author: Terry Yiu <963907+tyiu@users.noreply.github.com>
Date: Thu, 30 Mar 2023 11:15:35 -0400
Refactor auto-translations and add caching
Changelog-Added: Add auto-translation caching to ruduce api usage
Closes: #843
Diffstat:
13 files changed, 236 insertions(+), 126 deletions(-)
diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj
@@ -17,10 +17,11 @@
3A3040F329A91366008A0F29 /* ProfileViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040F229A91366008A0F29 /* ProfileViewTests.swift */; };
3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */; };
3A4325A82961E11400BFCD9D /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 3A4325AA2961E11400BFCD9D /* Localizable.stringsdict */; };
+ 3A48E23B29D518F000BA313D /* Translations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A48E23A29D518F000BA313D /* Translations.swift */; };
3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FC297E3CFF0090C62D /* RepostsModel.swift */; };
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FE297E3D900090C62D /* RepostsView.swift */; };
3AA24802297E3DC20090C62D /* RepostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA24801297E3DC20090C62D /* RepostView.swift */; };
- 3AA59D1D2999B0400061C48E /* DraftsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA59D1C2999B0400061C48E /* DraftsModel.swift */; };
+ 3AA59D1D2999B0400061C48E /* Drafts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA59D1C2999B0400061C48E /* Drafts.swift */; };
3AAA95CA298DF87B00F3D526 /* TranslationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAA95C9298DF87B00F3D526 /* TranslationService.swift */; };
3AAA95CC298E07E900F3D526 /* DeepLPlan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAA95CB298E07E900F3D526 /* DeepLPlan.swift */; };
3AB72AB9298ECF30004BB58C /* Translator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB72AB8298ECF30004BB58C /* Translator.swift */; };
@@ -310,6 +311,7 @@
3A41E559299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A41E55A299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = "<group>"; };
3A41E55B299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = id; path = id.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
+ 3A48E23A29D518F000BA313D /* Translations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Translations.swift; sourceTree = "<group>"; };
3A4F3320297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-FR"; path = "fr-FR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A4F3321297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-FR"; path = "fr-FR.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A4F3322297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "fr-FR"; path = "fr-FR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
@@ -338,7 +340,7 @@
3AA247FC297E3CFF0090C62D /* RepostsModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepostsModel.swift; sourceTree = "<group>"; };
3AA247FE297E3D900090C62D /* RepostsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostsView.swift; sourceTree = "<group>"; };
3AA24801297E3DC20090C62D /* RepostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostView.swift; sourceTree = "<group>"; };
- 3AA59D1C2999B0400061C48E /* DraftsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftsModel.swift; sourceTree = "<group>"; };
+ 3AA59D1C2999B0400061C48E /* Drafts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Drafts.swift; sourceTree = "<group>"; };
3AA5E70229B682A5002701ED /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = "<group>"; };
3AA5E70329B682AD002701ED /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3AA5E70429B682B3002701ED /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = uk; path = uk.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
@@ -791,9 +793,10 @@
3AAA95C9298DF87B00F3D526 /* TranslationService.swift */,
3AAA95CB298E07E900F3D526 /* DeepLPlan.swift */,
4CE8795A2996C47A00F758CC /* ZapsModel.swift */,
- 3AA59D1C2999B0400061C48E /* DraftsModel.swift */,
+ 3AA59D1C2999B0400061C48E /* Drafts.swift */,
4C54AA0629A540BA003E4487 /* NotificationsModel.swift */,
4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */,
+ 3A48E23A29D518F000BA313D /* Translations.swift */,
);
path = Models;
sourceTree = "<group>";
@@ -1608,6 +1611,7 @@
4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */,
F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */,
4C3A1D332960DB0500558C0F /* Markdown.swift in Sources */,
+ 3A48E23B29D518F000BA313D /* Translations.swift in Sources */,
4CE879552996BAB900F758CC /* RelayPaidDetail.swift in Sources */,
4CF0ABD42980996B00D66079 /* Report.swift in Sources */,
4C06670B28FDE64700038D2A /* damus.c in Sources */,
@@ -1627,7 +1631,7 @@
4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */,
4C3AC7A52836987600E1F516 /* MainTabView.swift in Sources */,
7C0F392F29B57CAF0039859C /* Binding+.swift in Sources */,
- 3AA59D1D2999B0400061C48E /* DraftsModel.swift in Sources */,
+ 3AA59D1D2999B0400061C48E /* Drafts.swift in Sources */,
3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */,
4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */,
);
diff --git a/damus/Components/TranslateView.swift b/damus/Components/TranslateView.swift
@@ -15,18 +15,48 @@ struct TranslateView: View {
@State var checkingTranslationStatus: Bool = false
@State var currentLanguage: String = "en"
@State var noteLanguage: String? = nil
- @State var translated_note: String? = nil
@State var show_translated_note: Bool = false
@State var translated_artifacts: NoteArtifacts? = nil
+ @State var translatable: Bool = false
let preferredLanguages = Set(Locale.preferredLanguages.map { localeToLanguage($0) })
var TranslateButton: some View {
Button(NSLocalizedString("Translate Note", comment: "Button to translate note from different language.")) {
show_translated_note = true
+ processTranslation()
}
.translate_button_style()
}
+
+ func processTranslation() {
+ guard noteLanguage != nil && !checkingTranslationStatus && translatable else {
+ return
+ }
+
+ checkingTranslationStatus = true
+ show_translated_note = true
+
+ Task {
+ let translationWithLanguage = await damus_state.translations.translate(event, state: damus_state)
+ DispatchQueue.main.async {
+ guard translationWithLanguage != nil else {
+ noteLanguage = currentLanguage
+ checkingTranslationStatus = false
+ translatable = false
+ return
+ }
+
+ noteLanguage = translationWithLanguage!.language
+
+ // Render translated note.
+ let translatedBlocks = event.get_blocks(content: translationWithLanguage!.translation)
+ translated_artifacts = render_blocks(blocks: translatedBlocks, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)
+
+ checkingTranslationStatus = false
+ }
+ }
+ }
func Translated(lang: String, artifacts: NoteArtifacts) -> some View {
return Group {
@@ -39,8 +69,8 @@ struct TranslateView: View {
}
}
- func CheckingStatus(lang: String) -> some View {
- return Button(String(format: NSLocalizedString("Translating from %@...", comment: "Button to indicate that the note is in the process of being translated from a different language."), lang)) {
+ func CheckingStatus() -> some View {
+ return Button(NSLocalizedString("Translating...", comment: "Button to indicate that the note is in the process of being translated from a different language.")) {
show_translated_note = false
}
.translate_button_style()
@@ -48,15 +78,17 @@ struct TranslateView: View {
func MainContent(note_lang: String) -> some View {
return Group {
- let languageName = Locale.current.localizedString(forLanguageCode: note_lang)
- if let lang = languageName, show_translated_note {
- if checkingTranslationStatus {
- CheckingStatus(lang: lang)
- } else if let artifacts = translated_artifacts {
- Translated(lang: lang, artifacts: artifacts)
+ if translatable {
+ let languageName = Locale.current.localizedString(forLanguageCode: note_lang)
+ if let lang = languageName, show_translated_note {
+ if checkingTranslationStatus {
+ CheckingStatus()
+ } else if let artifacts = translated_artifacts {
+ Translated(lang: lang, artifacts: artifacts)
+ }
+ } else {
+ TranslateButton
}
- } else {
- TranslateButton
}
}
}
@@ -70,55 +102,17 @@ struct TranslateView: View {
}
}
.task {
- guard noteLanguage == nil && !checkingTranslationStatus && damus_state.settings.can_translate(damus_state.pubkey) else {
- return
- }
-
- checkingTranslationStatus = true
-
- if #available(iOS 16, *) {
- currentLanguage = Locale.current.language.languageCode?.identifier ?? "en"
- } else {
- currentLanguage = Locale.current.languageCode ?? "en"
- }
-
- noteLanguage = event.note_language(damus_state.keypair.privkey) ?? currentLanguage
-
- guard let note_lang = noteLanguage else {
- noteLanguage = currentLanguage
- translated_note = nil
- checkingTranslationStatus = false
- return
- }
-
- if !preferredLanguages.contains(note_lang) {
- do {
- // If the note language is different from our preferred languages, send a translation request.
- let translator = Translator(damus_state.settings)
- let originalContent = event.get_content(damus_state.keypair.privkey)
- translated_note = try await translator.translate(originalContent, from: note_lang, to: currentLanguage)
+ DispatchQueue.main.async {
+ currentLanguage = damus_state.translations.targetLanguage
+ noteLanguage = damus_state.translations.detectLanguage(event, state: damus_state)
+ translatable = damus_state.translations.shouldTranslate(event, state: damus_state)
- if originalContent == translated_note {
- // If the translation is the same as the original, don't bother showing it.
- noteLanguage = currentLanguage
- translated_note = nil
- }
- } catch {
- // If for whatever reason we're not able to figure out the language of the note, or translate the note, fail gracefully and do not retry. It's not the end of the world. Don't want to take down someone's translation server with an accidental denial of service attack.
- noteLanguage = currentLanguage
- translated_note = nil
+ let autoTranslate = damus_state.settings.auto_translate
+ if autoTranslate {
+ processTranslation()
}
+ show_translated_note = autoTranslate
}
-
- if let translated = translated_note {
- // Render translated note.
- let translatedBlocks = event.get_blocks(content: translated)
- translated_artifacts = render_blocks(blocks: translatedBlocks, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)
- }
-
- checkingTranslationStatus = false
-
- show_translated_note = damus_state.settings.auto_translate
}
}
}
diff --git a/damus/ContentView.swift b/damus/ContentView.swift
@@ -611,6 +611,8 @@ struct ContentView: View {
pool.register_handler(sub_id: sub_id, handler: home.handle_event)
+ let settings = UserSettingsStore()
+
self.damus_state = DamusState(pool: pool,
keypair: keypair,
likes: EventCounter(our_pubkey: pubkey),
@@ -622,12 +624,13 @@ struct ContentView: View {
previews: PreviewCache(),
zaps: Zaps(our_pubkey: pubkey),
lnurls: LNUrls(),
- settings: UserSettingsStore(),
+ settings: settings,
relay_filters: relay_filters,
relay_metadata: metadatas,
drafts: Drafts(),
events: EventCache(),
- bookmarks: BookmarksManager(pubkey: pubkey)
+ bookmarks: BookmarksManager(pubkey: pubkey),
+ translations: Translations(settings)
)
home.damus_state = self.damus_state!
diff --git a/damus/Models/DamusState.swift b/damus/Models/DamusState.swift
@@ -26,6 +26,7 @@ struct DamusState {
let drafts: Drafts
let events: EventCache
let bookmarks: BookmarksManager
+ let translations: Translations
var pubkey: String {
return keypair.pubkey
@@ -36,6 +37,7 @@ struct DamusState {
}
static var empty: DamusState {
- return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts(), events: EventCache(), bookmarks: BookmarksManager(pubkey: ""))
+ let settings = UserSettingsStore()
+ return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: settings, relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts(), events: EventCache(), bookmarks: BookmarksManager(pubkey: ""), translations: Translations(settings))
}
}
diff --git a/damus/Models/Drafts.swift b/damus/Models/Drafts.swift
@@ -0,0 +1,13 @@
+//
+// Drafts.swift
+// damus
+//
+// Created by Terry Yiu on 2/12/23.
+//
+
+import Foundation
+
+class Drafts: ObservableObject {
+ @Published var post: NSMutableAttributedString = NSMutableAttributedString(string: "")
+ @Published var replies: [NostrEvent: NSMutableAttributedString] = [:]
+}
diff --git a/damus/Models/DraftsModel.swift b/damus/Models/DraftsModel.swift
@@ -1,13 +0,0 @@
-//
-// DraftsModel.swift
-// damus
-//
-// Created by Terry Yiu on 2/12/23.
-//
-
-import Foundation
-
-class Drafts: ObservableObject {
- @Published var post: NSMutableAttributedString = NSMutableAttributedString(string: "")
- @Published var replies: [NostrEvent: NSMutableAttributedString] = [:]
-}
diff --git a/damus/Models/Translations.swift b/damus/Models/Translations.swift
@@ -0,0 +1,116 @@
+//
+// Translations.swift
+// damus
+//
+// Created by Terry Yiu on 3/29/23.
+//
+
+import Foundation
+import NaturalLanguage
+
+class Translations: ObservableObject {
+ private static let languageDetectionMinConfidence = 0.5
+
+ @Published var translations: [NostrEvent: String] = [:]
+ @Published var languages: [NostrEvent: String] = [:]
+
+ let settings: UserSettingsStore
+
+ let translator: Translator
+
+ let targetLanguage = currentLanguage()
+ let preferredLanguages = Set(Locale.preferredLanguages.map { localeToLanguage($0) })
+
+ init(_ settings: UserSettingsStore) {
+ self.settings = settings
+ self.translator = Translator(settings)
+ }
+
+ /**
+ Attempts to detect the language of the content of a given nostr event using Apple's offline NaturalLanguage API.
+ The detected language will be returned only if it has a 50% or more confidence.
+ This is a best effort guess and could be incorrect.
+ */
+ func detectLanguage(_ event: NostrEvent, state: DamusState) -> String? {
+ if let cachedLanguage = languages[event] {
+ return cachedLanguage
+ }
+
+ // Rely on Apple's NLLanguageRecognizer to tell us which language it thinks the note is in
+ // and filter on only the text portions of the content as URLs and hashtags confuse the language recognizer.
+ let originalBlocks = event.blocks(state.keypair.privkey)
+ let originalOnlyText = originalBlocks.compactMap { $0.is_text }.joined(separator: " ")
+
+ // Only accept language recognition hypothesis if there's at least a 50% probability that it's accurate.
+ let languageRecognizer = NLLanguageRecognizer()
+ languageRecognizer.processString(originalOnlyText)
+
+ guard let locale = languageRecognizer.languageHypotheses(withMaximum: 1).first(where: { $0.value >= Translations.languageDetectionMinConfidence })?.key.rawValue else {
+ return nil
+ }
+
+ // Remove the variant component and just take the language part as translation services typically only supports the variant-less language.
+ // Moreover, speakers of one variant can generally understand other variants.
+ let language = localeToLanguage(locale)
+ languages[event] = language
+ return language
+ }
+
+ func translate(_ event: NostrEvent, state: DamusState) async -> TranslationWithLanguage? {
+ guard shouldTranslate(event, state: state) else {
+ return nil
+ }
+
+ guard let noteLanguage = detectLanguage(event, state: state) else {
+ return nil
+ }
+
+ let translationWithLanguage: TranslationWithLanguage
+
+ if let cachedTranslation = translations[event] {
+ translationWithLanguage = TranslationWithLanguage(translation: cachedTranslation, language: noteLanguage)
+ } else {
+ do {
+ guard let _translationWithLanguage = try await translator.translate(event.get_content(state.keypair.privkey), from: noteLanguage, to: targetLanguage) else {
+ return nil
+ }
+
+ translationWithLanguage = _translationWithLanguage
+ translations[event] = translationWithLanguage.translation
+ languages[event] = translationWithLanguage.language
+ } catch {
+ return nil
+ }
+ }
+
+ // If the translated content is identical to the original content, don't return the translation.
+ if translationWithLanguage.translation == event.get_content(state.keypair.privkey) {
+ languages[event] = targetLanguage
+ return nil
+ } else {
+ return translationWithLanguage
+ }
+ }
+
+ func shouldTranslate(_ event: NostrEvent, state: DamusState) -> Bool {
+ // Do not translate self-authored content because if the language recognizer guesses the wrong language for your own note,
+ // it's annoying and unexpected for the translation to show up.
+ if event.pubkey == state.pubkey && state.is_privkey_user {
+ return false
+ }
+
+ // Avoid translating notes if language cannot be detected or if it is in one of the user's preferred languages.
+ guard let noteLanguage = detectLanguage(event, state: state), !preferredLanguages.contains(noteLanguage) else {
+ return false
+ }
+
+ switch settings.translation_service {
+ case .none:
+ return false
+ case .libretranslate:
+ return URLComponents(string: settings.libretranslate_url) != nil
+ case .deepl:
+ return settings.deepl_api_key != ""
+ }
+ }
+}
diff --git a/damus/Models/UserSettingsStore.swift b/damus/Models/UserSettingsStore.swift
@@ -280,17 +280,6 @@ class UserSettingsStore: ObservableObject {
private func clearDeepLApiKey() throws {
try Vault.deletePrivateKey(keychainConfiguration: DamusDeepLKeychainConfiguration())
}
-
- func can_translate(_ pubkey: String) -> Bool {
- switch translation_service {
- case .none:
- return false
- case .libretranslate:
- return URLComponents(string: libretranslate_url) != nil
- case .deepl:
- return deepl_api_key != ""
- }
- }
}
struct DamusLibreTranslateKeychainConfiguration: KeychainConfiguration {
diff --git a/damus/Nostr/NostrEvent.swift b/damus/Nostr/NostrEvent.swift
@@ -260,25 +260,6 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
return event_is_reply(self, privkey: privkey)
}
- func note_language(_ privkey: String?) -> String? {
- // Rely on Apple's NLLanguageRecognizer to tell us which language it thinks the note is in
- // and filter on only the text portions of the content as URLs and hashtags confuse the language recognizer.
- let originalBlocks = blocks(privkey)
- let originalOnlyText = originalBlocks.compactMap { $0.is_text }.joined(separator: " ")
-
- // Only accept language recognition hypothesis if there's at least a 50% probability that it's accurate.
- let languageRecognizer = NLLanguageRecognizer()
- languageRecognizer.processString(originalOnlyText)
-
- guard let locale = languageRecognizer.languageHypotheses(withMaximum: 1).first(where: { $0.value >= 0.5 })?.key.rawValue else {
- return nil
- }
-
- // Remove the variant component and just take the language part as translation services typically only supports the variant-less language.
- // Moreover, speakers of one variant can generally understand other variants.
- return localeToLanguage(locale)
- }
-
public var referenced_ids: [ReferencedId] {
return get_referenced_ids(key: "e")
}
diff --git a/damus/Util/LocalizationUtil.swift b/damus/Util/LocalizationUtil.swift
@@ -22,6 +22,14 @@ func localizedStringFormat(key: String, locale: Locale?) -> String {
return bundle.localizedString(forKey: key, value: fallback, table: nil)
}
+func currentLanguage() -> String {
+ if #available(iOS 16, *) {
+ return Locale.current.language.languageCode?.identifier ?? "en"
+ } else {
+ return Locale.current.languageCode ?? "en"
+ }
+}
+
/**
Removes the variant part of a locale code so that it contains only the language code.
*/
diff --git a/damus/Util/Translator.swift b/damus/Util/Translator.swift
@@ -20,18 +20,18 @@ public struct Translator {
self.userSettingsStore = userSettingsStore
}
- public func translate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? {
+ public func translate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> TranslationWithLanguage? {
switch userSettingsStore.translation_service {
case .libretranslate:
return try await translateWithLibreTranslate(text, from: sourceLanguage, to: targetLanguage)
case .deepl:
return try await translateWithDeepL(text, from: sourceLanguage, to: targetLanguage)
case .none:
- return text
+ return nil
}
}
- private func translateWithLibreTranslate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? {
+ private func translateWithLibreTranslate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> TranslationWithLanguage? {
let url = try makeURL(userSettingsStore.libretranslate_url, path: "/translate")
var request = URLRequest(url: url)
@@ -51,10 +51,12 @@ public struct Translator {
let translatedText: String
}
let response: Response = try await decodedData(for: request)
- return response.translatedText
+ let translation = response.translatedText
+
+ return TranslationWithLanguage(translation: translation, language: targetLanguage)
}
- private func translateWithDeepL(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? {
+ private func translateWithDeepL(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> TranslationWithLanguage? {
if userSettingsStore.deepl_api_key == "" {
return nil
}
@@ -68,10 +70,9 @@ public struct Translator {
struct RequestBody: Encodable {
let text: [String]
- let source_lang: String
let target_lang: String
}
- let body = RequestBody(text: [text], source_lang: sourceLanguage.uppercased(), target_lang: targetLanguage.uppercased())
+ let body = RequestBody(text: [text], target_lang: targetLanguage.uppercased())
request.httpBody = try encoder.encode(body)
struct Response: Decodable {
@@ -83,7 +84,13 @@ public struct Translator {
}
let response: Response = try await decodedData(for: request)
- return response.translations.map { $0.text }.joined(separator: " ")
+
+ if response.translations.isEmpty {
+ return nil
+ }
+
+ let translation = response.translations.map { $0.text }.joined(separator: " ")
+ return TranslationWithLanguage(translation: translation, language: response.translations.first!.detected_source_language)
}
private func makeURL(_ baseUrl: String, path: String) throws -> URL {
@@ -104,6 +111,11 @@ public struct Translator {
}
}
+public struct TranslationWithLanguage {
+ let translation: String
+ let language: String
+}
+
private extension URLSession {
func data(for request: URLRequest) async throws -> Data {
var task: URLSessionDataTask?
diff --git a/damus/Views/NoteContentView.swift b/damus/Views/NoteContentView.swift
@@ -95,13 +95,11 @@ struct NoteContentView: View {
}
}
- if size == .selected || damus_state.settings.auto_translate {
- if with_padding {
- translateView
- .padding(.horizontal)
- } else {
- translateView
- }
+ if with_padding {
+ translateView
+ .padding(.horizontal)
+ } else {
+ translateView
}
if show_images && artifacts.images.count > 0 {
diff --git a/damus/Views/SearchHomeView.swift b/damus/Views/SearchHomeView.swift
@@ -15,8 +15,6 @@ struct SearchHomeView: View {
@State var search: String = ""
@FocusState private var isFocused: Bool
- let preferredLanguages = Set(Locale.preferredLanguages.map { localeToLanguage($0) })
-
var SearchInput: some View {
HStack {
HStack{
@@ -54,12 +52,17 @@ struct SearchHomeView: View {
return true
}
+ // Always show your own posts.
+ if $0.pubkey == damus_state.pubkey {
+ return true
+ }
+
// If we can't determine the note's language with 50%+ confidence, lean on the side of caution and show it anyway.
- guard let noteLanguage = $0.note_language(damus_state.keypair.privkey) else {
+ guard let noteLanguage = damus_state.translations.detectLanguage($0, state: damus_state) else {
return true
}
- return preferredLanguages.contains(noteLanguage)
+ return damus_state.translations.preferredLanguages.contains(noteLanguage)
}
)
.refreshable {