Cache translations, fix translation popping

Completely refactor Translate View. Simplify bool logic into a state enum.

Changelog-Fixed: Fix translation text popping
Changelog-Added: Cache translations
This commit is contained in:
William Casarin
2023-04-06 10:15:42 -07:00
parent 95fb7bccf8
commit c5341ba337
3 changed files with 144 additions and 78 deletions

View File

@@ -8,109 +8,151 @@
import SwiftUI import SwiftUI
import NaturalLanguage import NaturalLanguage
struct Translated: Equatable {
let artifacts: NoteArtifacts
let language: String
}
enum TranslateStatus: Equatable {
case havent_tried
case trying
case translated(Translated)
case not_needed
}
struct TranslateView: View { struct TranslateView: View {
let damus_state: DamusState let damus_state: DamusState
let event: NostrEvent let event: NostrEvent
let size: EventViewKind let size: EventViewKind
let currentLanguage: String
@State var translated: TranslateStatus
init(damus_state: DamusState, event: NostrEvent, size: EventViewKind) {
self.damus_state = damus_state
self.event = event
self.size = size
if #available(iOS 16, *) {
self.currentLanguage = Locale.current.language.languageCode?.identifier ?? "en"
} else {
self.currentLanguage = Locale.current.languageCode ?? "en"
}
if let cached = damus_state.events.lookup_translated_artifacts(evid: event.id) {
self._translated = State(initialValue: cached)
} else {
let initval: TranslateStatus = self.damus_state.settings.auto_translate ? .trying : .havent_tried
self._translated = State(initialValue: initval)
}
}
@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
let preferredLanguages = Set(Locale.preferredLanguages.map { localeToLanguage($0) }) let preferredLanguages = Set(Locale.preferredLanguages.map { localeToLanguage($0) })
var TranslateButton: some View { var TranslateButton: some View {
Button(NSLocalizedString("Translate Note", comment: "Button to translate note from different language.")) { Button(NSLocalizedString("Translate Note", comment: "Button to translate note from different language.")) {
show_translated_note = true self.translated = .trying
} }
.translate_button_style() .translate_button_style()
} }
func Translated(lang: String, artifacts: NoteArtifacts) -> some View { func TranslatedView(lang: String?, artifacts: NoteArtifacts) -> some View {
return Group { return VStack(alignment: .leading) {
Button(String(format: NSLocalizedString("Translated from %@", comment: "Button to indicate that the note has been translated from a different language."), lang)) { Text(String(format: NSLocalizedString("Translated from %@", comment: "Button to indicate that the note has been translated from a different language."), lang ?? "ja"))
show_translated_note = false .foregroundColor(.gray)
} .font(.footnote)
.translate_button_style() .padding([.top, .bottom], 10)
SelectableText(attributedString: artifacts.content, size: self.size) if self.size == .selected {
SelectableText(attributedString: artifacts.content, size: self.size)
} else {
Text(artifacts.content)
.font(eventviewsize_to_font(self.size))
}
} }
} }
func MainContent(note_lang: String) -> some View { func failed_attempt() {
return Group { self.translated = .not_needed
let languageName = Locale.current.localizedString(forLanguageCode: note_lang) damus_state.events.store_translation_artifacts(evid: event.id, translated: .not_needed)
if let languageName, let translated_artifacts, show_translated_note { }
Translated(lang: languageName, artifacts: translated_artifacts)
} else if !damus_state.settings.auto_translate { func attempt_translation() async {
TranslateButton guard case .trying = translated else {
} else { return
Text("")
}
} }
guard damus_state.settings.can_translate(damus_state.pubkey) else {
return
}
let note_lang = event.note_language(damus_state.keypair.privkey) ?? currentLanguage
// Don't translate if its in our preferred languages
guard !preferredLanguages.contains(note_lang) else {
failed_attempt()
return
}
// 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)
let translated_note = try? await translator.translate(originalContent, from: note_lang, to: currentLanguage)
guard let translated_note else {
// if its the same, give up and don't retry
failed_attempt()
return
}
guard originalContent != translated_note else {
// if its the same, give up and don't retry
failed_attempt()
return
}
// Render translated note
let translated_blocks = event.get_blocks(content: translated_note)
let artifacts = render_blocks(blocks: translated_blocks, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)
// and cache it
self.translated = .translated(Translated(artifacts: artifacts, language: note_lang))
damus_state.events.store_translation_artifacts(evid: event.id, translated: self.translated)
} }
var body: some View { var body: some View {
Group { Group {
if let note_lang = noteLanguage, noteLanguage != currentLanguage { switch translated {
MainContent(note_lang: note_lang) case .havent_tried:
} else { if damus_state.settings.auto_translate {
Text("")
} else {
TranslateButton
}
case .trying:
Text("Translating...")
.foregroundColor(.gray)
.font(.footnote)
.padding([.top, .bottom], 10)
case .translated(let translated):
let languageName = Locale.current.localizedString(forLanguageCode: translated.language)
TranslatedView(lang: languageName, artifacts: translated.artifacts)
case .not_needed:
Text("") Text("")
} }
} }
.onChange(of: translated) { val in
guard case .trying = translated else {
return
}
Task {
await attempt_translation()
}
}
.task { .task {
guard noteLanguage == nil && !checkingTranslationStatus && damus_state.settings.can_translate(damus_state.pubkey) else { await attempt_translation()
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)
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
}
}
if let translated_note {
// Render translated note.
let translated_blocks = event.get_blocks(content: translated_note)
translated_artifacts = render_blocks(blocks: translated_blocks, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)
}
checkingTranslationStatus = false
show_translated_note = damus_state.settings.auto_translate
} }
} }
} }

View File

@@ -13,6 +13,8 @@ class EventCache {
private var events: [String: NostrEvent] = [:] private var events: [String: NostrEvent] = [:]
private var replies = ReplyMap() private var replies = ReplyMap()
private var cancellable: AnyCancellable? private var cancellable: AnyCancellable?
private var translations: [String: TranslateStatus] = [:]
private var artifacts: [String: NoteArtifacts] = [:]
//private var thread_latest: [String: Int64] //private var thread_latest: [String: Int64]
@@ -24,6 +26,22 @@ class EventCache {
} }
} }
func store_translation_artifacts(evid: String, translated: TranslateStatus) {
self.translations[evid] = translated
}
func store_artifacts(evid: String, artifacts: NoteArtifacts) {
self.artifacts[evid] = artifacts
}
func lookup_artifacts(evid: String) -> NoteArtifacts? {
return self.artifacts[evid]
}
func lookup_translated_artifacts(evid: String) -> TranslateStatus? {
return self.translations[evid]
}
func parent_events(event: NostrEvent) -> [NostrEvent] { func parent_events(event: NostrEvent) -> [NostrEvent] {
var parents: [NostrEvent] = [] var parents: [NostrEvent] = []
@@ -87,6 +105,8 @@ class EventCache {
private func prune() { private func prune() {
events = [:] events = [:]
translations = [:]
artifacts = [:]
replies.replies = [:] replies.replies = [:]
} }
} }

View File

@@ -234,7 +234,11 @@ struct NoteContentView_Previews: PreviewProvider {
} }
} }
struct NoteArtifacts { struct NoteArtifacts: Equatable {
static func == (lhs: NoteArtifacts, rhs: NoteArtifacts) -> Bool {
return lhs.content == rhs.content
}
let content: AttributedString let content: AttributedString
let images: [URL] let images: [URL]
let invoices: [Invoice] let invoices: [Invoice]