commit e5c0400b544ee9bf20829d682344f80888427dc5
parent c6c47e824a3b6b247c698a79c14a272991493f07
Author: Terry Yiu <963907+tyiu@users.noreply.github.com>
Date: Mon, 27 Mar 2023 10:43:17 -0400
Merge remote-tracking branch 'terry/tyiu/filter-language'
Changelog-Added: Auto Translation
Diffstat:
7 files changed, 107 insertions(+), 37 deletions(-)
diff --git a/damus/Components/TranslateView.swift b/damus/Components/TranslateView.swift
@@ -18,6 +18,8 @@ struct TranslateView: View {
@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.")) {
@@ -80,24 +82,7 @@ struct TranslateView: View {
currentLanguage = Locale.current.languageCode ?? "en"
}
- // 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(damus_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)
- noteLanguage = languageRecognizer.languageHypotheses(withMaximum: 1).first(where: { $0.value >= 0.5 })?.key.rawValue ?? currentLanguage
-
- if let lang = noteLanguage, noteLanguage != currentLanguage {
- // If the detected dominant language is a variant, remove the variant component and just take the language part as translation services typically only supports the variant-less language.
- if #available(iOS 16, *) {
- noteLanguage = Locale.LanguageCode(stringLiteral: lang).identifier(.alpha2)
- } else {
- noteLanguage = NSLocale(localeIdentifier: lang).languageCode
- }
- }
+ noteLanguage = event.note_language(damus_state.keypair.privkey) ?? currentLanguage
guard let note_lang = noteLanguage else {
noteLanguage = currentLanguage
@@ -106,9 +91,9 @@ struct TranslateView: View {
return
}
- if note_lang != currentLanguage {
+ if !preferredLanguages.contains(note_lang) {
do {
- // If the note language is different from our language, send a translation request.
+ // 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)
@@ -132,11 +117,21 @@ struct TranslateView: View {
}
checkingTranslationStatus = false
-
+
+ show_translated_note = damus_state.settings.auto_translate
}
}
}
+extension View {
+ func translate_button_style() -> some View {
+ return self
+ .font(.footnote)
+ .contentShape(Rectangle())
+ .padding([.top, .bottom], 10)
+ }
+}
+
struct TranslateView_Previews: PreviewProvider {
static var previews: some View {
let ds = test_damus_state()
diff --git a/damus/Models/UserSettingsStore.swift b/damus/Models/UserSettingsStore.swift
@@ -128,6 +128,18 @@ class UserSettingsStore: ObservableObject {
}
}
+ @Published var auto_translate: Bool {
+ didSet {
+ UserDefaults.standard.set(auto_translate, forKey: "auto_translate")
+ }
+ }
+
+ @Published var show_only_preferred_languages: Bool {
+ didSet {
+ UserDefaults.standard.set(show_only_preferred_languages, forKey: "show_only_preferred_languages")
+ }
+ }
+
@Published var translation_service: TranslationService {
didSet {
UserDefaults.standard.set(translation_service.rawValue, forKey: "translation_service")
@@ -210,6 +222,8 @@ class UserSettingsStore: ObservableObject {
left_handed = UserDefaults.standard.object(forKey: "left_handed") as? Bool ?? false
zap_vibration = UserDefaults.standard.object(forKey: "zap_vibration") as? Bool ?? false
disable_animation = should_disable_image_animation()
+ auto_translate = UserDefaults.standard.object(forKey: "auto_translate") as? Bool ?? false
+ show_only_preferred_languages = UserDefaults.standard.object(forKey: "show_only_preferred_languages") as? Bool ?? false
// Note from @tyiu:
// Default translation service is disabled by default for now until we gain some confidence that it is working well in production.
diff --git a/damus/Nostr/NostrEvent.swift b/damus/Nostr/NostrEvent.swift
@@ -10,6 +10,7 @@ import CommonCrypto
import secp256k1
import secp256k1_implementation
import CryptoKit
+import NaturalLanguage
@@ -259,6 +260,25 @@ 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
@@ -21,3 +21,14 @@ func localizedStringFormat(key: String, locale: Locale?) -> String {
let fallback = bundleForLocale(locale: Locale(identifier: "en-US")).localizedString(forKey: key, value: nil, table: nil)
return bundle.localizedString(forKey: key, value: fallback, table: nil)
}
+
+/**
+ Removes the variant part of a locale code so that it contains only the language code.
+ */
+func localeToLanguage(_ locale: String) -> String? {
+ if #available(iOS 16, *) {
+ return Locale.LanguageCode(stringLiteral: locale).identifier(.alpha2)
+ } else {
+ return NSLocale(localeIdentifier: locale).languageCode
+ }
+}
diff --git a/damus/Views/ConfigView.swift b/damus/Views/ConfigView.swift
@@ -153,6 +153,9 @@ struct ConfigView: View {
}
Section(NSLocalizedString("Translations", comment: "Section title for selecting the translation service.")) {
+ Toggle(NSLocalizedString("Show only preferred languages on Universe feed", comment: "Toggle to show notes that are only in the device's preferred languages on the Universe feed and hide notes that are in other languages."), isOn: $settings.show_only_preferred_languages)
+ .toggleStyle(.switch)
+
Picker(NSLocalizedString("Service", comment: "Prompt selection of translation service provider."), selection: $settings.translation_service) {
ForEach(TranslationService.allCases, id: \.self) { server in
Text(server.model.displayName)
@@ -197,6 +200,11 @@ struct ConfigView: View {
Link(NSLocalizedString("Get API Key", comment: "Button to navigate to DeepL website to get a translation API key."), destination: URL(string: "https://www.deepl.com/pro-api")!)
}
}
+
+ if settings.translation_service != .none {
+ Toggle(NSLocalizedString("Automatically translate notes", comment: "Toggle to automatically translate notes."), isOn: $settings.auto_translate)
+ .toggleStyle(.switch)
+ }
}
Section(NSLocalizedString("Miscellaneous", comment: "Section header for miscellaneous user configuration")) {
diff --git a/damus/Views/NoteContentView.swift b/damus/Views/NoteContentView.swift
@@ -61,6 +61,10 @@ struct NoteContentView: View {
var invoicesView: some View {
InvoicesView(our_pubkey: damus_state.keypair.pubkey, invoices: artifacts.invoices)
}
+
+ var translateView: some View {
+ TranslateView(damus_state: damus_state, event: event)
+ }
var previewView: some View {
Group {
@@ -82,7 +86,6 @@ struct NoteContentView: View {
VStack(alignment: .leading) {
if size == .selected {
SelectableText(attributedString: artifacts.content)
- TranslateView(damus_state: damus_state, event: event)
} else {
if with_padding {
truncatedText
@@ -92,6 +95,15 @@ struct NoteContentView: View {
}
}
+ if size == .selected || damus_state.settings.auto_translate {
+ if with_padding {
+ translateView
+ .padding(.horizontal)
+ } else {
+ translateView
+ }
+ }
+
if show_images && artifacts.images.count > 0 {
ImageCarousel(urls: artifacts.images)
} else if !show_images && artifacts.images.count > 0 {
@@ -217,16 +229,6 @@ struct NoteContentView_Previews: PreviewProvider {
}
}
-
-extension View {
- func translate_button_style() -> some View {
- return self
- .font(.footnote)
- .contentShape(Rectangle())
- .padding([.top, .bottom], 10)
- }
-}
-
struct NoteArtifacts {
let content: AttributedString
let images: [URL]
diff --git a/damus/Views/SearchHomeView.swift b/damus/Views/SearchHomeView.swift
@@ -7,12 +7,15 @@
import SwiftUI
import CryptoKit
+import NaturalLanguage
struct SearchHomeView: View {
let damus_state: DamusState
@StateObject var model: SearchHomeModel
@State var search: String = ""
@FocusState private var isFocused: Bool
+
+ let preferredLanguages = Set(Locale.preferredLanguages.map { localeToLanguage($0) })
var SearchInput: some View {
HStack {
@@ -41,12 +44,29 @@ struct SearchHomeView: View {
}
var GlobalContent: some View {
- return TimelineView(events: model.events, loading: $model.loading, damus: damus_state, show_friend_icon: true, filter: { _ in true })
- .refreshable {
- // Fetch new information by unsubscribing and resubscribing to the relay
- model.unsubscribe()
- model.subscribe()
+ return TimelineView(
+ events: model.events,
+ loading: $model.loading,
+ damus: damus_state,
+ show_friend_icon: true,
+ filter: {
+ if damus_state.settings.show_only_preferred_languages == false {
+ 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 {
+ return true
+ }
+
+ return preferredLanguages.contains(noteLanguage)
}
+ )
+ .refreshable {
+ // Fetch new information by unsubscribing and resubscribing to the relay
+ model.unsubscribe()
+ model.subscribe()
+ }
}
var SearchContent: some View {