Compare commits
2 Commits
tyiu/refac
...
tyiu/auto-
| Author | SHA1 | Date | |
|---|---|---|---|
|
74e099de8e
|
|||
|
85d5c4eda5
|
@@ -17,6 +17,7 @@
|
||||
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 */; };
|
||||
@@ -323,6 +324,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>"; };
|
||||
@@ -821,6 +823,7 @@
|
||||
3AA59D1C2999B0400061C48E /* DraftsModel.swift */,
|
||||
4C54AA0629A540BA003E4487 /* NotificationsModel.swift */,
|
||||
4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */,
|
||||
3A48E23A29D518F000BA313D /* Translations.swift */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
@@ -1663,6 +1666,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 */,
|
||||
|
||||
@@ -14,20 +14,71 @@ struct TranslateView: View {
|
||||
let size: EventViewKind
|
||||
|
||||
@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 = 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.")) {
|
||||
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 = damus_state.translations.targetLanguage
|
||||
checkingTranslationStatus = false
|
||||
show_translated_note = 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)
|
||||
|
||||
translatable = true
|
||||
|
||||
checkingTranslationStatus = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Translated(lang: String, artifacts: NoteArtifacts) -> some View {
|
||||
return Group {
|
||||
@@ -42,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()
|
||||
}
|
||||
@@ -55,63 +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 {
|
||||
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 = 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -609,6 +609,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),
|
||||
@@ -620,7 +622,7 @@ 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(),
|
||||
@@ -628,7 +630,8 @@ struct ContentView: View {
|
||||
bookmarks: BookmarksManager(pubkey: pubkey),
|
||||
postbox: PostBox(pool: pool),
|
||||
bootstrap_relays: bootstrap_relays,
|
||||
replies: ReplyCounter(our_pubkey: pubkey)
|
||||
replies: ReplyCounter(our_pubkey: pubkey),
|
||||
translations: Translations(settings)
|
||||
)
|
||||
home.damus_state = self.damus_state!
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ struct DamusState {
|
||||
let postbox: PostBox
|
||||
let bootstrap_relays: [String]
|
||||
let replies: ReplyCounter
|
||||
|
||||
let translations: Translations
|
||||
var pubkey: String {
|
||||
return keypair.pubkey
|
||||
}
|
||||
@@ -39,6 +39,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: ""), postbox: PostBox(pool: RelayPool()), bootstrap_relays: [], replies: ReplyCounter(our_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: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts(), events: EventCache(), bookmarks: BookmarksManager(pubkey: ""), postbox: PostBox(pool: RelayPool()), bootstrap_relays: [], replies: ReplyCounter(our_pubkey: ""), translations: Translations(settings))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//
|
||||
// DraftsModel.swift
|
||||
// DraftModel.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Terry Yiu on 2/12/23.
|
||||
|
||||
150
damus/Models/Translations.swift
Normal file
150
damus/Models/Translations.swift
Normal file
@@ -0,0 +1,150 @@
|
||||
//
|
||||
// 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
|
||||
}
|
||||
|
||||
/**
|
||||
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
|
||||
}
|
||||
|
||||
guard let noteLanguage = detectLanguage(event, state: state) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if languages[event] != nil {
|
||||
return cachedTranslation(event)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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 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
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -336,17 +336,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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -20,18 +20,28 @@ public struct Translator {
|
||||
self.userSettingsStore = userSettingsStore
|
||||
}
|
||||
|
||||
public func translate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? {
|
||||
/**
|
||||
Translates a string from source language to target language.
|
||||
If the translation provider supports its own language detection, it may determine the source language by itself that could be
|
||||
different from what is passed in as the sourceLanguage argument.
|
||||
The source language that is actually used in the translation will be returned as part of the TranslationWithLanguage object.
|
||||
If the translation was unable to be fetched for whatever reason, nil is returned.
|
||||
*/
|
||||
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)
|
||||
return try await translateWithDeepL(text, to: targetLanguage)
|
||||
case .none:
|
||||
return text
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func translateWithLibreTranslate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? {
|
||||
/**
|
||||
Translates a string from sourceLanguage to targetLanguage using LibreTranslate. We do not rely on LibreTranslate's language detection API as it requires a separate API call. Instead, we rely on the passed in sourceLanguage argument.
|
||||
*/
|
||||
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 +61,15 @@ 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? {
|
||||
/**
|
||||
Translates a string to targetLanguage using DeepL. We do not accept a sourceLanguage as an argument as DeepL performs language detection within the translate API, its models are generally fairly accurate, and does not require a separate API call like LibreTranslate.
|
||||
*/
|
||||
private func translateWithDeepL(_ text: String, to targetLanguage: String) async throws -> TranslationWithLanguage? {
|
||||
if userSettingsStore.deepl_api_key == "" {
|
||||
return nil
|
||||
}
|
||||
@@ -68,10 +83,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 +97,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 +124,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?
|
||||
|
||||
@@ -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,7 +102,7 @@ struct NoteContentView: View {
|
||||
}
|
||||
}
|
||||
|
||||
if size == .selected || damus_state.settings.auto_translate {
|
||||
if translatable {
|
||||
if with_padding {
|
||||
translateView
|
||||
.padding(.horizontal)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// Always show your own posts.
|
||||
if $0.pubkey == damus_state.pubkey {
|
||||
return true
|
||||
}
|
||||
|
||||
return preferredLanguages.contains(noteLanguage)
|
||||
// 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 = damus_state.translations.detectLanguage($0, state: damus_state) else {
|
||||
return true
|
||||
}
|
||||
|
||||
return damus_state.translations.preferredLanguages.contains(noteLanguage)
|
||||
}
|
||||
)
|
||||
.refreshable {
|
||||
|
||||
Reference in New Issue
Block a user