diff --git a/damus/Components/TranslateView.swift b/damus/Components/TranslateView.swift index 4f2c27a4..8a2d55a9 100644 --- a/damus/Components/TranslateView.swift +++ b/damus/Components/TranslateView.swift @@ -14,13 +14,31 @@ struct TranslateView: View { let size: EventViewKind @State var checkingTranslationStatus: Bool = false - @State var currentLanguage: String = "en" - @State var noteLanguage: String? = nil - @State var show_translated_note: Bool = false - @State var translated_artifacts: NoteArtifacts? = nil - @State var translatable: Bool = false + @State var translatable: Bool = true + + @State var noteLanguage: String? + @State var show_translated_note: Bool + @State var translated_artifacts: NoteArtifacts? let preferredLanguages = Set(Locale.preferredLanguages.map { localeToLanguage($0) }) + + init(damus_state: DamusState, event: NostrEvent, size: EventViewKind) { + self.damus_state = damus_state + self.event = event + self.size = size + self._noteLanguage = State(initialValue: damus_state.translations.detectLanguage(event, state: damus_state)) + + if let translationWithLanguage = damus_state.translations.cachedTranslation(event) { + self._noteLanguage = State(initialValue: translationWithLanguage.language) + + let translatedBlocks = event.get_blocks(content: translationWithLanguage.translation) + self._translated_artifacts = State.init(initialValue: render_blocks(blocks: translatedBlocks, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)) + } else { + self._translated_artifacts = State(initialValue: nil) + } + + self._show_translated_note = State(initialValue: damus_state.settings.auto_translate) + } var TranslateButton: some View { Button(NSLocalizedString("Translate Note", comment: "Button to translate note from different language.")) { @@ -42,8 +60,9 @@ struct TranslateView: View { let translationWithLanguage = await damus_state.translations.translate(event, state: damus_state) DispatchQueue.main.async { guard translationWithLanguage != nil else { - noteLanguage = currentLanguage + noteLanguage = damus_state.translations.targetLanguage checkingTranslationStatus = false + show_translated_note = false translatable = false return } @@ -54,6 +73,8 @@ struct TranslateView: View { let translatedBlocks = event.get_blocks(content: translationWithLanguage!.translation) translated_artifacts = render_blocks(blocks: translatedBlocks, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey) + translatable = true + checkingTranslationStatus = false } } @@ -72,11 +93,15 @@ struct TranslateView: View { 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 + if translatable { + 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 { + EmptyView() + } } else { EmptyView() } @@ -85,25 +110,17 @@ struct TranslateView: View { var body: some View { Group { - if let note_lang = noteLanguage, noteLanguage != currentLanguage { + if let note_lang = noteLanguage, note_lang != damus_state.translations.targetLanguage { MainContent(note_lang: note_lang) + .task { + if show_translated_note { + processTranslation() + } + } } else { Text("") } } - .task { - 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) - - let autoTranslate = damus_state.settings.auto_translate - if autoTranslate { - processTranslation() - } - show_translated_note = autoTranslate - } - } } } diff --git a/damus/Models/Translations.swift b/damus/Models/Translations.swift index 224d2196..0d6fab0b 100644 --- a/damus/Models/Translations.swift +++ b/damus/Models/Translations.swift @@ -56,6 +56,29 @@ class Translations: ObservableObject { return language } + /** + Returns true if the given translation is effectively the same as the original note, ignoring whitespaces and new lines. + */ + private func translationSameAsOriginal(_ translation: String, event: NostrEvent, state: DamusState) -> Bool { + return translation.trimmingCharacters(in: .whitespacesAndNewlines) == event.get_content(state.keypair.privkey).trimmingCharacters(in: .whitespacesAndNewlines) + } + + func hasCachedTranslation(_ event: NostrEvent) -> Bool { + return languages[event] != nil + } + + func cachedTranslation(_ event: NostrEvent) -> TranslationWithLanguage? { + if let cachedLanguage = languages[event] { + if let cachedTranslation = translations[event] { + return TranslationWithLanguage(translation: cachedTranslation, language: cachedLanguage) + } else { + return nil + } + } else { + return nil + } + } + func translate(_ event: NostrEvent, state: DamusState) async -> TranslationWithLanguage? { guard shouldTranslate(event, state: state) else { return nil @@ -65,30 +88,29 @@ class Translations: ObservableObject { 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 languages[event] != nil { + return cachedTranslation(event) } - // 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 + do { + guard let translationWithLanguage = try await translator.translate(event.get_content(state.keypair.privkey), from: noteLanguage, to: targetLanguage) else { + return nil + } + + // If the translated content is identical to the original content, don't return the translation. + if translationSameAsOriginal(translationWithLanguage.translation, event: event, state: state) { + // Nil out the translation as it's the same as the original. + translations[event] = nil + // Leave an entry so that we don't attempt to translate it again in the future. + languages[event] = targetLanguage + return nil + } else { + translations[event] = translationWithLanguage.translation + languages[event] = translationWithLanguage.language + return translationWithLanguage + } + } catch { return nil - } else { - return translationWithLanguage } } @@ -99,18 +121,30 @@ class Translations: ObservableObject { return false } + // Avoid translating if no translation service is configured. + switch settings.translation_service { + case .none: + return false + case .libretranslate: + if URLComponents(string: settings.libretranslate_url) == nil { + return false + } + case .deepl: + if settings.deepl_api_key == "" { + return false + } + } + + // If translation was attempted before, use the results of the cached translation to determine if it should be shown. + if languages[event] != nil { + return translations[event] != nil + } + // 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 != "" - } + return true } } diff --git a/damus/Views/NoteContentView.swift b/damus/Views/NoteContentView.swift index 69c26d38..902e2874 100644 --- a/damus/Views/NoteContentView.swift +++ b/damus/Views/NoteContentView.swift @@ -29,6 +29,7 @@ struct NoteContentView: View { let size: EventViewKind let preview_height: CGFloat? let options: EventViewOptions + let translatable: Bool @State var artifacts: NoteArtifacts @State var preview: LinkViewRepresentable? @@ -39,6 +40,7 @@ struct NoteContentView: View { self.show_images = show_images self.size = size self.options = options + self.translatable = damus_state.translations.shouldTranslate(event, state: damus_state) self._artifacts = State(initialValue: artifacts) self.preview_height = lookup_cached_preview_size(previews: damus_state.previews, evid: event.id) self._preview = State(initialValue: load_cached_preview(previews: damus_state.previews, evid: event.id)) @@ -100,11 +102,13 @@ struct NoteContentView: View { } } - if with_padding { - translateView - .padding(.horizontal) - } else { - translateView + if translatable { + if with_padding { + translateView + .padding(.horizontal) + } else { + translateView + } } if show_images && artifacts.images.count > 0 {