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 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 {
let damus_state: DamusState
let event: NostrEvent
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) })
var TranslateButton: some View {
Button(NSLocalizedString("Translate Note", comment: "Button to translate note from different language.")) {
show_translated_note = true
self.translated = .trying
}
.translate_button_style()
}
func Translated(lang: String, artifacts: NoteArtifacts) -> some View {
return Group {
Button(String(format: NSLocalizedString("Translated from %@", comment: "Button to indicate that the note has been translated from a different language."), lang)) {
show_translated_note = false
}
.translate_button_style()
func TranslatedView(lang: String?, artifacts: NoteArtifacts) -> some View {
return VStack(alignment: .leading) {
Text(String(format: NSLocalizedString("Translated from %@", comment: "Button to indicate that the note has been translated from a different language."), lang ?? "ja"))
.foregroundColor(.gray)
.font(.footnote)
.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 {
return Group {
let languageName = Locale.current.localizedString(forLanguageCode: note_lang)
if let languageName, let translated_artifacts, show_translated_note {
Translated(lang: languageName, artifacts: translated_artifacts)
} else if !damus_state.settings.auto_translate {
TranslateButton
} else {
Text("")
}
func failed_attempt() {
self.translated = .not_needed
damus_state.events.store_translation_artifacts(evid: event.id, translated: .not_needed)
}
func attempt_translation() async {
guard case .trying = translated else {
return
}
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 {
Group {
if let note_lang = noteLanguage, noteLanguage != currentLanguage {
MainContent(note_lang: note_lang)
} else {
switch translated {
case .havent_tried:
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("")
}
}
.onChange(of: translated) { val in
guard case .trying = translated else {
return
}
Task {
await attempt_translation()
}
}
.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)
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
await attempt_translation()
}
}
}

View File

@@ -13,6 +13,8 @@ class EventCache {
private var events: [String: NostrEvent] = [:]
private var replies = ReplyMap()
private var cancellable: AnyCancellable?
private var translations: [String: TranslateStatus] = [:]
private var artifacts: [String: NoteArtifacts] = [:]
//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] {
var parents: [NostrEvent] = []
@@ -87,6 +105,8 @@ class EventCache {
private func prune() {
events = [:]
translations = [:]
artifacts = [:]
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 images: [URL]
let invoices: [Invoice]