Compare commits
1 Commits
nprofile-q
...
tyiu/deepl
| Author | SHA1 | Date | |
|---|---|---|---|
|
cd9e8bf9b1
|
@@ -15,6 +15,9 @@
|
|||||||
3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FC297E3CFF0090C62D /* RepostsModel.swift */; };
|
3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FC297E3CFF0090C62D /* RepostsModel.swift */; };
|
||||||
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FE297E3D900090C62D /* RepostsView.swift */; };
|
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FE297E3D900090C62D /* RepostsView.swift */; };
|
||||||
3AA24802297E3DC20090C62D /* RepostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA24801297E3DC20090C62D /* RepostView.swift */; };
|
3AA24802297E3DC20090C62D /* RepostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA24801297E3DC20090C62D /* RepostView.swift */; };
|
||||||
|
3AAA95CA298DF87B00F3D526 /* TranslationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAA95C9298DF87B00F3D526 /* TranslationService.swift */; };
|
||||||
|
3AAA95CC298E07E900F3D526 /* DeepLPlan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAA95CB298E07E900F3D526 /* DeepLPlan.swift */; };
|
||||||
|
3AB72AB9298ECF30004BB58C /* Translator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB72AB8298ECF30004BB58C /* Translator.swift */; };
|
||||||
3ACB685C297633BC00C46468 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3ACB685A297633BC00C46468 /* InfoPlist.strings */; };
|
3ACB685C297633BC00C46468 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3ACB685A297633BC00C46468 /* InfoPlist.strings */; };
|
||||||
3ACB685F297633BC00C46468 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3ACB685D297633BC00C46468 /* Localizable.strings */; };
|
3ACB685F297633BC00C46468 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3ACB685D297633BC00C46468 /* Localizable.strings */; };
|
||||||
3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */; };
|
3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */; };
|
||||||
@@ -244,9 +247,12 @@
|
|||||||
3AA247FC297E3CFF0090C62D /* RepostsModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepostsModel.swift; sourceTree = "<group>"; };
|
3AA247FC297E3CFF0090C62D /* RepostsModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepostsModel.swift; sourceTree = "<group>"; };
|
||||||
3AA247FE297E3D900090C62D /* RepostsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostsView.swift; sourceTree = "<group>"; };
|
3AA247FE297E3D900090C62D /* RepostsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostsView.swift; sourceTree = "<group>"; };
|
||||||
3AA24801297E3DC20090C62D /* RepostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostView.swift; sourceTree = "<group>"; };
|
3AA24801297E3DC20090C62D /* RepostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostView.swift; sourceTree = "<group>"; };
|
||||||
|
3AAA95C9298DF87B00F3D526 /* TranslationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslationService.swift; sourceTree = "<group>"; };
|
||||||
|
3AAA95CB298E07E900F3D526 /* DeepLPlan.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLPlan.swift; sourceTree = "<group>"; };
|
||||||
3AB5B86A2986D8A3006599D2 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
3AB5B86A2986D8A3006599D2 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||||
3AB5B86B2986D8A3006599D2 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
|
3AB5B86B2986D8A3006599D2 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
3AB5B86C2986D8A3006599D2 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = de; path = de.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
3AB5B86C2986D8A3006599D2 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = de; path = de.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||||
|
3AB72AB8298ECF30004BB58C /* Translator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Translator.swift; sourceTree = "<group>"; };
|
||||||
3AC524EE298C000B00693EBF /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
3AC524EE298C000B00693EBF /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||||
3AC524EF298C000B00693EBF /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = "<group>"; };
|
3AC524EF298C000B00693EBF /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
3AC524F0298C000B00693EBF /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ar; path = ar.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
3AC524F0298C000B00693EBF /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ar; path = ar.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||||
@@ -617,6 +623,8 @@
|
|||||||
4CF0ABD32980996B00D66079 /* Report.swift */,
|
4CF0ABD32980996B00D66079 /* Report.swift */,
|
||||||
4CF0ABDD2981A69500D66079 /* MutelistModel.swift */,
|
4CF0ABDD2981A69500D66079 /* MutelistModel.swift */,
|
||||||
3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */,
|
3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */,
|
||||||
|
3AAA95C9298DF87B00F3D526 /* TranslationService.swift */,
|
||||||
|
3AAA95CB298E07E900F3D526 /* DeepLPlan.swift */,
|
||||||
);
|
);
|
||||||
path = Models;
|
path = Models;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -729,6 +737,7 @@
|
|||||||
4CB883A52975F83C00DC99E7 /* LNUrlPayRequest.swift */,
|
4CB883A52975F83C00DC99E7 /* LNUrlPayRequest.swift */,
|
||||||
4CB883A72975FC1800DC99E7 /* Zaps.swift */,
|
4CB883A72975FC1800DC99E7 /* Zaps.swift */,
|
||||||
4CB883B5297730E400DC99E7 /* LNUrls.swift */,
|
4CB883B5297730E400DC99E7 /* LNUrls.swift */,
|
||||||
|
3AB72AB8298ECF30004BB58C /* Translator.swift */,
|
||||||
);
|
);
|
||||||
path = Util;
|
path = Util;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -1106,6 +1115,7 @@
|
|||||||
4C7FF7D52823313F009601DB /* Mentions.swift in Sources */,
|
4C7FF7D52823313F009601DB /* Mentions.swift in Sources */,
|
||||||
4C633350283D40E500B1C9C3 /* HomeModel.swift in Sources */,
|
4C633350283D40E500B1C9C3 /* HomeModel.swift in Sources */,
|
||||||
4C987B57283FD07F0042CE38 /* FollowersModel.swift in Sources */,
|
4C987B57283FD07F0042CE38 /* FollowersModel.swift in Sources */,
|
||||||
|
3AB72AB9298ECF30004BB58C /* Translator.swift in Sources */,
|
||||||
4C363A9028247A1D006E126D /* NostrLink.swift in Sources */,
|
4C363A9028247A1D006E126D /* NostrLink.swift in Sources */,
|
||||||
4C0A3F8C280F5FCA000448DE /* ChatroomView.swift in Sources */,
|
4C0A3F8C280F5FCA000448DE /* ChatroomView.swift in Sources */,
|
||||||
4C477C9E282C3A4800033AA3 /* TipCounter.swift in Sources */,
|
4C477C9E282C3A4800033AA3 /* TipCounter.swift in Sources */,
|
||||||
@@ -1126,6 +1136,7 @@
|
|||||||
4C75EFB328049D640006080F /* NostrEvent.swift in Sources */,
|
4C75EFB328049D640006080F /* NostrEvent.swift in Sources */,
|
||||||
4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */,
|
4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */,
|
||||||
4C363A8428233689006E126D /* Parser.swift in Sources */,
|
4C363A8428233689006E126D /* Parser.swift in Sources */,
|
||||||
|
3AAA95CA298DF87B00F3D526 /* TranslationService.swift in Sources */,
|
||||||
4CE4F9E328528C5200C00DD9 /* AddRelayView.swift in Sources */,
|
4CE4F9E328528C5200C00DD9 /* AddRelayView.swift in Sources */,
|
||||||
4C363A9A28283854006E126D /* Reply.swift in Sources */,
|
4C363A9A28283854006E126D /* Reply.swift in Sources */,
|
||||||
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */,
|
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */,
|
||||||
@@ -1234,6 +1245,7 @@
|
|||||||
4CF0ABD42980996B00D66079 /* Report.swift in Sources */,
|
4CF0ABD42980996B00D66079 /* Report.swift in Sources */,
|
||||||
4C0A3F97280F8E02000448DE /* ThreadView.swift in Sources */,
|
4C0A3F97280F8E02000448DE /* ThreadView.swift in Sources */,
|
||||||
4C06670B28FDE64700038D2A /* damus.c in Sources */,
|
4C06670B28FDE64700038D2A /* damus.c in Sources */,
|
||||||
|
3AAA95CC298E07E900F3D526 /* DeepLPlan.swift in Sources */,
|
||||||
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */,
|
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */,
|
||||||
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */,
|
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */,
|
||||||
3AE45AF6297BB2E700C1D842 /* LibreTranslateServer.swift in Sources */,
|
3AE45AF6297BB2E700C1D842 /* LibreTranslateServer.swift in Sources */,
|
||||||
|
|||||||
@@ -71,10 +71,7 @@ struct TranslateView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.task {
|
.task {
|
||||||
let translate_url = damus_state.settings.libretranslate_url
|
guard noteLanguage == nil && !checkingTranslationStatus && damus_state.settings.can_translate(damus_state.pubkey) else {
|
||||||
let api_key = damus_state.settings.libretranslate_api_key
|
|
||||||
|
|
||||||
guard noteLanguage == nil && !checkingTranslationStatus && translate_url != "" else {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,7 +88,7 @@ struct TranslateView: View {
|
|||||||
noteLanguage = NLLanguageRecognizer.dominantLanguage(for: content)?.rawValue ?? currentLanguage
|
noteLanguage = NLLanguageRecognizer.dominantLanguage(for: content)?.rawValue ?? currentLanguage
|
||||||
|
|
||||||
if let lang = noteLanguage, noteLanguage != 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 LibreTranslate typically only supports the variant-less language.
|
// 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, *) {
|
if #available(iOS 16, *) {
|
||||||
noteLanguage = Locale.LanguageCode(stringLiteral: lang).identifier(.alpha2)
|
noteLanguage = Locale.LanguageCode(stringLiteral: lang).identifier(.alpha2)
|
||||||
} else {
|
} else {
|
||||||
@@ -109,7 +106,7 @@ struct TranslateView: View {
|
|||||||
if note_lang != currentLanguage {
|
if note_lang != currentLanguage {
|
||||||
do {
|
do {
|
||||||
// If the note language is different from our language, send a translation request.
|
// If the note language is different from our language, send a translation request.
|
||||||
let translator = Translator(translate_url, apiKey: api_key)
|
let translator = Translator(damus_state.settings)
|
||||||
translated_note = try await translator.translate(content, from: note_lang, to: currentLanguage)
|
translated_note = try await translator.translate(content, from: note_lang, to: currentLanguage)
|
||||||
} catch {
|
} 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.
|
// 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.
|
||||||
|
|||||||
35
damus/Models/DeepLPlan.swift
Normal file
35
damus/Models/DeepLPlan.swift
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
//
|
||||||
|
// DeepLPlan.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Terry Yiu on 2/3/23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum DeepLPlan: String, CaseIterable, Identifiable {
|
||||||
|
var id: String { self.rawValue }
|
||||||
|
|
||||||
|
struct Model: Identifiable, Hashable {
|
||||||
|
var id: String { self.tag }
|
||||||
|
var tag: String
|
||||||
|
var displayName: String
|
||||||
|
var url: String
|
||||||
|
}
|
||||||
|
|
||||||
|
case free
|
||||||
|
case pro
|
||||||
|
|
||||||
|
var model: Model {
|
||||||
|
switch self {
|
||||||
|
case .free:
|
||||||
|
return .init(tag: self.rawValue, displayName: NSLocalizedString("Free", comment: "Dropdown option for selecting Free plan for DeepL translation service."), url: "https://api-free.deepl.com")
|
||||||
|
case .pro:
|
||||||
|
return .init(tag: self.rawValue, displayName: NSLocalizedString("Pro", comment: "Dropdown option for selecting Pro plan for DeepL translation service."), url: "https://api.deepl.com")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static var allModels: [Model] {
|
||||||
|
return Self.allCases.map { $0.model }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,6 @@ enum LibreTranslateServer: String, CaseIterable, Identifiable {
|
|||||||
var url: String?
|
var url: String?
|
||||||
}
|
}
|
||||||
|
|
||||||
case none
|
|
||||||
case argosopentech
|
case argosopentech
|
||||||
case terraprint
|
case terraprint
|
||||||
case vern
|
case vern
|
||||||
@@ -25,8 +24,6 @@ enum LibreTranslateServer: String, CaseIterable, Identifiable {
|
|||||||
|
|
||||||
var model: Model {
|
var model: Model {
|
||||||
switch self {
|
switch self {
|
||||||
case .none:
|
|
||||||
return .init(tag: self.rawValue, displayName: NSLocalizedString("None", comment: "Dropdown option for selecting no translation server."), url: nil)
|
|
||||||
case .argosopentech:
|
case .argosopentech:
|
||||||
return .init(tag: self.rawValue, displayName: "translate.argosopentech.com", url: "https://translate.argosopentech.com")
|
return .init(tag: self.rawValue, displayName: "translate.argosopentech.com", url: "https://translate.argosopentech.com")
|
||||||
case .terraprint:
|
case .terraprint:
|
||||||
|
|||||||
37
damus/Models/TranslationService.swift
Normal file
37
damus/Models/TranslationService.swift
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
//
|
||||||
|
// TranslationService.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Terry Yiu on 2/3/23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum TranslationService: String, CaseIterable, Identifiable {
|
||||||
|
var id: String { self.rawValue }
|
||||||
|
|
||||||
|
struct Model: Identifiable, Hashable {
|
||||||
|
var id: String { self.tag }
|
||||||
|
var tag: String
|
||||||
|
var displayName: String
|
||||||
|
}
|
||||||
|
|
||||||
|
case none
|
||||||
|
case libretranslate
|
||||||
|
case deepl
|
||||||
|
|
||||||
|
var model: Model {
|
||||||
|
switch self {
|
||||||
|
case .none:
|
||||||
|
return .init(tag: self.rawValue, displayName: NSLocalizedString("None", comment: "Dropdown option for selecting no translation service."))
|
||||||
|
case .libretranslate:
|
||||||
|
return .init(tag: self.rawValue, displayName: NSLocalizedString("LibreTranslate (Open Source)", comment: "Dropdown option for selecting LibreTranslate as the translation service."))
|
||||||
|
case .deepl:
|
||||||
|
return .init(tag: self.rawValue, displayName: NSLocalizedString("DeepL (Proprietary, Higher Accuracy)", comment: "Dropdown option for selecting DeepL as the translation service."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static var allModels: [Model] {
|
||||||
|
return Self.allCases.map { $0.model }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,7 +22,23 @@ func get_default_wallet(_ pubkey: String) -> Wallet {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func get_libretranslate_server(_ pubkey: String) -> LibreTranslateServer? {
|
private func get_translation_service(_ pubkey: String) -> TranslationService? {
|
||||||
|
guard let translation_service = UserDefaults.standard.string(forKey: "translation_service") else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return TranslationService(rawValue: translation_service)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func get_deepl_plan(_ pubkey: String) -> DeepLPlan? {
|
||||||
|
guard let server_name = UserDefaults.standard.string(forKey: "deepl_plan") else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return DeepLPlan(rawValue: server_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func get_libretranslate_server(_ pubkey: String) -> LibreTranslateServer? {
|
||||||
guard let server_name = UserDefaults.standard.string(forKey: "libretranslate_server") else {
|
guard let server_name = UserDefaults.standard.string(forKey: "libretranslate_server") else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -30,7 +46,7 @@ func get_libretranslate_server(_ pubkey: String) -> LibreTranslateServer? {
|
|||||||
return LibreTranslateServer(rawValue: server_name)
|
return LibreTranslateServer(rawValue: server_name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func get_libretranslate_url(_ pubkey: String, server: LibreTranslateServer) -> String? {
|
private func get_libretranslate_url(_ pubkey: String, server: LibreTranslateServer) -> String? {
|
||||||
if let url = server.model.url {
|
if let url = server.model.url {
|
||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
@@ -57,6 +73,32 @@ class UserSettingsStore: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Published var translation_service: TranslationService {
|
||||||
|
didSet {
|
||||||
|
UserDefaults.standard.set(translation_service.rawValue, forKey: "translation_service")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Published var deepl_plan: DeepLPlan {
|
||||||
|
didSet {
|
||||||
|
UserDefaults.standard.set(deepl_plan.rawValue, forKey: "deepl_plan")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Published var deepl_api_key: String {
|
||||||
|
didSet {
|
||||||
|
do {
|
||||||
|
if deepl_api_key == "" {
|
||||||
|
try clearDeepLApiKey()
|
||||||
|
} else {
|
||||||
|
try saveDeepLApiKey(deepl_api_key)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// No-op.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Published var libretranslate_server: LibreTranslateServer {
|
@Published var libretranslate_server: LibreTranslateServer {
|
||||||
didSet {
|
didSet {
|
||||||
if oldValue == libretranslate_server {
|
if oldValue == libretranslate_server {
|
||||||
@@ -67,7 +109,7 @@ class UserSettingsStore: ObservableObject {
|
|||||||
|
|
||||||
libretranslate_api_key = ""
|
libretranslate_api_key = ""
|
||||||
|
|
||||||
if libretranslate_server == .custom || libretranslate_server == .none {
|
if libretranslate_server == .custom {
|
||||||
libretranslate_url = ""
|
libretranslate_url = ""
|
||||||
} else {
|
} else {
|
||||||
libretranslate_url = libretranslate_server.model.url!
|
libretranslate_url = libretranslate_server.model.url!
|
||||||
@@ -102,18 +144,25 @@ class UserSettingsStore: ObservableObject {
|
|||||||
show_wallet_selector = should_show_wallet_selector(pubkey)
|
show_wallet_selector = should_show_wallet_selector(pubkey)
|
||||||
|
|
||||||
left_handed = UserDefaults.standard.object(forKey: "left_handed") as? Bool ?? false
|
left_handed = UserDefaults.standard.object(forKey: "left_handed") as? Bool ?? false
|
||||||
|
|
||||||
if let server = get_libretranslate_server(pubkey) {
|
// Note from @tyiu:
|
||||||
self.libretranslate_server = server
|
// Default translation service is disabled by default for now until we gain some confidence that it is working well in production.
|
||||||
self.libretranslate_url = get_libretranslate_url(pubkey, server: server) ?? ""
|
// Instead of throwing all Damus users onto feature immediately, allow for discovery of feature organically.
|
||||||
|
// Also, we are connecting to servers listed as mirrors on the official LibreTranslate GitHub README that do not require API keys.
|
||||||
|
// However, we have not asked them for permission to use, so we're trying to be good neighbors for now.
|
||||||
|
// Opportunity: spin up dedicated trusted LibreTranslate server that requires an API key for any access (or higher rate limit access).
|
||||||
|
if let translation_service = get_translation_service(pubkey) {
|
||||||
|
self.translation_service = translation_service
|
||||||
} else {
|
} else {
|
||||||
// Note from @tyiu:
|
self.translation_service = .none
|
||||||
// Default server is disabled by default for now until we gain some confidence that it is working well in production.
|
}
|
||||||
// Instead of throwing all Damus users onto feature immediately, allow for discovery of feature organically.
|
|
||||||
// Also, we are connecting to servers listed as mirrors on the official LibreTranslate GitHub README that do not require API keys.
|
if let libretranslate_server = get_libretranslate_server(pubkey) {
|
||||||
// However, we have not asked them for permission to use, so we're trying to be good neighbors for now.
|
self.libretranslate_server = libretranslate_server
|
||||||
// Opportunity: spin up dedicated trusted LibreTranslate server that requires an API key for any access (or higher rate limit access).
|
self.libretranslate_url = get_libretranslate_url(pubkey, server: libretranslate_server) ?? ""
|
||||||
libretranslate_server = .none
|
} else {
|
||||||
|
// Choose a random server to distribute load.
|
||||||
|
libretranslate_server = .allCases.filter { $0 != .custom }.randomElement()!
|
||||||
libretranslate_url = ""
|
libretranslate_url = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,15 +171,46 @@ class UserSettingsStore: ObservableObject {
|
|||||||
} catch {
|
} catch {
|
||||||
libretranslate_api_key = ""
|
libretranslate_api_key = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let deepl_plan = get_deepl_plan(pubkey) {
|
||||||
|
self.deepl_plan = deepl_plan
|
||||||
|
} else {
|
||||||
|
self.deepl_plan = .free
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
deepl_api_key = try Vault.getPrivateKey(keychainConfiguration: DamusDeepLKeychainConfiguration())
|
||||||
|
} catch {
|
||||||
|
deepl_api_key = ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveLibreTranslateApiKey(_ apiKey: String) throws {
|
private func saveLibreTranslateApiKey(_ apiKey: String) throws {
|
||||||
try Vault.savePrivateKey(apiKey, keychainConfiguration: DamusLibreTranslateKeychainConfiguration())
|
try Vault.savePrivateKey(apiKey, keychainConfiguration: DamusLibreTranslateKeychainConfiguration())
|
||||||
}
|
}
|
||||||
|
|
||||||
func clearLibreTranslateApiKey() throws {
|
private func clearLibreTranslateApiKey() throws {
|
||||||
try Vault.deletePrivateKey(keychainConfiguration: DamusLibreTranslateKeychainConfiguration())
|
try Vault.deletePrivateKey(keychainConfiguration: DamusLibreTranslateKeychainConfiguration())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func saveDeepLApiKey(_ apiKey: String) throws {
|
||||||
|
try Vault.savePrivateKey(apiKey, keychainConfiguration: DamusDeepLKeychainConfiguration())
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
struct DamusLibreTranslateKeychainConfiguration: KeychainConfiguration {
|
||||||
@@ -138,3 +218,9 @@ struct DamusLibreTranslateKeychainConfiguration: KeychainConfiguration {
|
|||||||
var accessGroup: String? = nil
|
var accessGroup: String? = nil
|
||||||
var accountName = "libretranslate_apikey"
|
var accountName = "libretranslate_apikey"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct DamusDeepLKeychainConfiguration: KeychainConfiguration {
|
||||||
|
var serviceName = "damus"
|
||||||
|
var accessGroup: String? = nil
|
||||||
|
var accountName = "deepl_apikey"
|
||||||
|
}
|
||||||
|
|||||||
127
damus/Util/Translator.swift
Normal file
127
damus/Util/Translator.swift
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
//
|
||||||
|
// Translator.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Terry Yiu on 2/4/23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
#if canImport(FoundationNetworking)
|
||||||
|
import FoundationNetworking
|
||||||
|
#endif
|
||||||
|
|
||||||
|
public struct Translator {
|
||||||
|
private let userSettingsStore: UserSettingsStore
|
||||||
|
private let session = URLSession.shared
|
||||||
|
private let encoder = JSONEncoder()
|
||||||
|
private let decoder = JSONDecoder()
|
||||||
|
|
||||||
|
init(_ userSettingsStore: UserSettingsStore) {
|
||||||
|
self.userSettingsStore = userSettingsStore
|
||||||
|
}
|
||||||
|
|
||||||
|
public func translate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? {
|
||||||
|
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)
|
||||||
|
case .none:
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func translateWithLibreTranslate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? {
|
||||||
|
let url = try makeURL(userSettingsStore.libretranslate_url, path: "/translate")
|
||||||
|
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "POST"
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||||
|
|
||||||
|
struct RequestBody: Encodable {
|
||||||
|
let q: String
|
||||||
|
let source: String
|
||||||
|
let target: String
|
||||||
|
let api_key: String?
|
||||||
|
}
|
||||||
|
let body = RequestBody(q: text, source: sourceLanguage, target: targetLanguage, api_key: userSettingsStore.libretranslate_api_key)
|
||||||
|
request.httpBody = try encoder.encode(body)
|
||||||
|
|
||||||
|
struct Response: Decodable {
|
||||||
|
let translatedText: String
|
||||||
|
}
|
||||||
|
let response: Response = try await decodedData(for: request)
|
||||||
|
return response.translatedText
|
||||||
|
}
|
||||||
|
|
||||||
|
private func translateWithDeepL(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? {
|
||||||
|
if userSettingsStore.deepl_api_key == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = try makeURL(userSettingsStore.deepl_plan.model.url, path: "/v2/translate")
|
||||||
|
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "POST"
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||||
|
request.setValue("DeepL-Auth-Key \(userSettingsStore.deepl_api_key)", forHTTPHeaderField: "Authorization")
|
||||||
|
|
||||||
|
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())
|
||||||
|
request.httpBody = try encoder.encode(body)
|
||||||
|
|
||||||
|
struct Response: Decodable {
|
||||||
|
let translations: [DeepLTranslations]
|
||||||
|
}
|
||||||
|
struct DeepLTranslations: Decodable {
|
||||||
|
let detected_source_language: String
|
||||||
|
let text: String
|
||||||
|
}
|
||||||
|
|
||||||
|
let response: Response = try await decodedData(for: request)
|
||||||
|
return response.translations.map { $0.text }.joined(separator: " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeURL(_ baseUrl: String, path: String) throws -> URL {
|
||||||
|
guard var components = URLComponents(string: baseUrl) else {
|
||||||
|
throw URLError(.badURL)
|
||||||
|
}
|
||||||
|
components.path = path
|
||||||
|
guard let url = components.url else {
|
||||||
|
throw URLError(.badURL)
|
||||||
|
}
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
private func decodedData<Output: Decodable>(for request: URLRequest) async throws -> Output {
|
||||||
|
let data = try await session.data(for: request)
|
||||||
|
let result = try decoder.decode(Output.self, from: data)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension URLSession {
|
||||||
|
func data(for request: URLRequest) async throws -> Data {
|
||||||
|
var task: URLSessionDataTask?
|
||||||
|
let onCancel = { task?.cancel() }
|
||||||
|
return try await withTaskCancellationHandler(
|
||||||
|
operation: {
|
||||||
|
try await withCheckedThrowingContinuation { continuation in
|
||||||
|
task = dataTask(with: request) { data, _, error in
|
||||||
|
guard let data = data else {
|
||||||
|
let error = error ?? URLError(.badServerResponse)
|
||||||
|
return continuation.resume(throwing: error)
|
||||||
|
}
|
||||||
|
continuation.resume(returning: data)
|
||||||
|
}
|
||||||
|
task?.resume()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onCancel: { onCancel() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ struct ConfigView: View {
|
|||||||
@State var confirm_logout: Bool = false
|
@State var confirm_logout: Bool = false
|
||||||
@State var confirm_delete_account: Bool = false
|
@State var confirm_delete_account: Bool = false
|
||||||
@State var show_privkey: Bool = false
|
@State var show_privkey: Bool = false
|
||||||
@State var show_libretranslate_api_key: Bool = false
|
@State var show_api_key: Bool = false
|
||||||
@State var privkey: String
|
@State var privkey: String
|
||||||
@State var privkey_copied: Bool = false
|
@State var privkey_copied: Bool = false
|
||||||
@State var pubkey_copied: Bool = false
|
@State var pubkey_copied: Bool = false
|
||||||
@@ -84,36 +84,50 @@ struct ConfigView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Section(NSLocalizedString("LibreTranslate Translations", comment: "Section title for selecting the server that hosts the LibreTranslate machine translation API.")) {
|
Section(NSLocalizedString("Translations", comment: "Section title for selecting the translation service.")) {
|
||||||
Picker(NSLocalizedString("Server", comment: "Prompt selection of LibreTranslate server to perform machine translations on notes"), selection: $settings.libretranslate_server) {
|
Picker(NSLocalizedString("Service", comment: "Prompt selection of translation service provider."), selection: $settings.translation_service) {
|
||||||
ForEach(LibreTranslateServer.allCases, id: \.self) { server in
|
ForEach(TranslationService.allCases, id: \.self) { server in
|
||||||
Text(server.model.displayName)
|
Text(server.model.displayName)
|
||||||
.tag(server.model.tag)
|
.tag(server.model.tag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if settings.libretranslate_server != .none {
|
if settings.translation_service == .libretranslate {
|
||||||
TextField(NSLocalizedString("URL", comment: "Example URL to LibreTranslate server"), text: $settings.libretranslate_url)
|
Picker(NSLocalizedString("Server", comment: "Prompt selection of LibreTranslate server to perform machine translations on notes"), selection: $settings.libretranslate_server) {
|
||||||
.disableAutocorrection(true)
|
ForEach(LibreTranslateServer.allCases, id: \.self) { server in
|
||||||
.disabled(settings.libretranslate_server != .custom)
|
Text(server.model.displayName)
|
||||||
.autocapitalization(UITextAutocapitalizationType.none)
|
.tag(server.model.tag)
|
||||||
HStack {
|
|
||||||
if show_libretranslate_api_key {
|
|
||||||
TextField(NSLocalizedString("API Key (optional)", comment: "Example URL to LibreTranslate server"), text: $settings.libretranslate_api_key)
|
|
||||||
.disableAutocorrection(true)
|
|
||||||
.autocapitalization(UITextAutocapitalizationType.none)
|
|
||||||
Button(NSLocalizedString("Hide API Key", comment: "Button to hide the LibreTranslate server API key.")) {
|
|
||||||
show_libretranslate_api_key = false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
SecureField(NSLocalizedString("API Key (optional)", comment: "Example URL to LibreTranslate server"), text: $settings.libretranslate_api_key)
|
|
||||||
.disableAutocorrection(true)
|
|
||||||
.autocapitalization(UITextAutocapitalizationType.none)
|
|
||||||
Button(NSLocalizedString("Show API Key", comment: "Button to hide the LibreTranslate server API key.")) {
|
|
||||||
show_libretranslate_api_key = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if settings.libretranslate_server == .custom {
|
||||||
|
TextField(NSLocalizedString("URL", comment: "Example URL to LibreTranslate server"), text: $settings.libretranslate_url)
|
||||||
|
.disableAutocorrection(true)
|
||||||
|
.autocapitalization(UITextAutocapitalizationType.none)
|
||||||
|
}
|
||||||
|
|
||||||
|
SecureField(NSLocalizedString("API Key (optional)", comment: "Prompt for optional entry of API Key to use translation server."), text: $settings.libretranslate_api_key)
|
||||||
|
.disableAutocorrection(true)
|
||||||
|
.disabled(settings.translation_service != .libretranslate)
|
||||||
|
.autocapitalization(UITextAutocapitalizationType.none)
|
||||||
|
}
|
||||||
|
|
||||||
|
if settings.translation_service == .deepl {
|
||||||
|
Picker(NSLocalizedString("Plan", comment: "Prompt selection of DeepL subscription plan to perform machine translations on notes"), selection: $settings.deepl_plan) {
|
||||||
|
ForEach(DeepLPlan.allCases, id: \.self) { server in
|
||||||
|
Text(server.model.displayName)
|
||||||
|
.tag(server.model.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SecureField(NSLocalizedString("API Key (required)", comment: "Prompt for required entry of API Key to use translation server."), text: $settings.deepl_api_key)
|
||||||
|
.disableAutocorrection(true)
|
||||||
|
.disabled(settings.translation_service != .deepl)
|
||||||
|
.autocapitalization(UITextAutocapitalizationType.none)
|
||||||
|
|
||||||
|
if settings.deepl_api_key == "" {
|
||||||
|
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")!)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,6 +188,81 @@ struct ConfigView: View {
|
|||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var libretranslate_view: some View {
|
||||||
|
VStack {
|
||||||
|
Picker(NSLocalizedString("Server", comment: "Prompt selection of LibreTranslate server to perform machine translations on notes"), selection: $settings.libretranslate_server) {
|
||||||
|
ForEach(LibreTranslateServer.allCases, id: \.self) { server in
|
||||||
|
Text(server.model.displayName)
|
||||||
|
.tag(server.model.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TextField(NSLocalizedString("URL", comment: "Example URL to LibreTranslate server"), text: $settings.libretranslate_url)
|
||||||
|
.disableAutocorrection(true)
|
||||||
|
.disabled(settings.libretranslate_server != .custom)
|
||||||
|
.autocapitalization(UITextAutocapitalizationType.none)
|
||||||
|
HStack {
|
||||||
|
let libretranslate_api_key_placeholder = NSLocalizedString("API Key (optional)", comment: "Prompt for optional entry of API Key to use translation server.")
|
||||||
|
if show_api_key {
|
||||||
|
TextField(libretranslate_api_key_placeholder, text: $settings.libretranslate_api_key)
|
||||||
|
.disableAutocorrection(true)
|
||||||
|
.autocapitalization(UITextAutocapitalizationType.none)
|
||||||
|
if settings.libretranslate_api_key != "" {
|
||||||
|
Button(NSLocalizedString("Hide API Key", comment: "Button to hide the LibreTranslate server API key.")) {
|
||||||
|
show_api_key = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
SecureField(libretranslate_api_key_placeholder, text: $settings.libretranslate_api_key)
|
||||||
|
.disableAutocorrection(true)
|
||||||
|
.autocapitalization(UITextAutocapitalizationType.none)
|
||||||
|
if settings.libretranslate_api_key != "" {
|
||||||
|
Button(NSLocalizedString("Show API Key", comment: "Button to show the LibreTranslate server API key.")) {
|
||||||
|
show_api_key = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var deepl_view: some View {
|
||||||
|
VStack {
|
||||||
|
Picker(NSLocalizedString("Plan", comment: "Prompt selection of DeepL subscription plan to perform machine translations on notes"), selection: $settings.deepl_plan) {
|
||||||
|
ForEach(DeepLPlan.allCases, id: \.self) { server in
|
||||||
|
Text(server.model.displayName)
|
||||||
|
.tag(server.model.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
let deepl_api_key_placeholder = NSLocalizedString("API Key (required)", comment: "Prompt for required entry of API Key to use translation server.")
|
||||||
|
if show_api_key {
|
||||||
|
TextField(deepl_api_key_placeholder, text: $settings.deepl_api_key)
|
||||||
|
.disableAutocorrection(true)
|
||||||
|
.autocapitalization(UITextAutocapitalizationType.none)
|
||||||
|
if settings.deepl_api_key != "" {
|
||||||
|
Button(NSLocalizedString("Hide API Key", comment: "Button to hide the DeepL translation API key.")) {
|
||||||
|
show_api_key = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
SecureField(deepl_api_key_placeholder, text: $settings.deepl_api_key)
|
||||||
|
.disableAutocorrection(true)
|
||||||
|
.autocapitalization(UITextAutocapitalizationType.none)
|
||||||
|
if settings.deepl_api_key != "" {
|
||||||
|
Button(NSLocalizedString("Show API Key", comment: "Button to show the DeepL translation API key.")) {
|
||||||
|
show_api_key = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if settings.deepl_api_key == "" {
|
||||||
|
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")!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ConfigView_Previews: PreviewProvider {
|
struct ConfigView_Previews: PreviewProvider {
|
||||||
|
|||||||
@@ -9,10 +9,6 @@ import SwiftUI
|
|||||||
import LinkPresentation
|
import LinkPresentation
|
||||||
import NaturalLanguage
|
import NaturalLanguage
|
||||||
|
|
||||||
#if canImport(FoundationNetworking)
|
|
||||||
import FoundationNetworking
|
|
||||||
#endif
|
|
||||||
|
|
||||||
struct NoteContentView: View {
|
struct NoteContentView: View {
|
||||||
let damus_state: DamusState
|
let damus_state: DamusState
|
||||||
let event: NostrEvent
|
let event: NostrEvent
|
||||||
@@ -154,83 +150,6 @@ func mention_str(_ m: Mention, profiles: Profiles) -> AttributedString {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public struct Translator {
|
|
||||||
private let url: String
|
|
||||||
private let apiKey: String?
|
|
||||||
private let session = URLSession.shared
|
|
||||||
private let encoder = JSONEncoder()
|
|
||||||
private let decoder = JSONDecoder()
|
|
||||||
|
|
||||||
public init(_ url: String, apiKey: String? = nil) {
|
|
||||||
self.url = url
|
|
||||||
self.apiKey = apiKey
|
|
||||||
}
|
|
||||||
|
|
||||||
public func translate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String {
|
|
||||||
let url = try makeURL(path: "/translate")
|
|
||||||
|
|
||||||
var request = URLRequest(url: url)
|
|
||||||
request.httpMethod = "POST"
|
|
||||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
||||||
|
|
||||||
struct RequestBody: Encodable {
|
|
||||||
let q: String
|
|
||||||
let source: String
|
|
||||||
let target: String
|
|
||||||
let api_key: String?
|
|
||||||
}
|
|
||||||
let body = RequestBody(q: text, source: sourceLanguage, target: targetLanguage, api_key: apiKey)
|
|
||||||
request.httpBody = try encoder.encode(body)
|
|
||||||
|
|
||||||
struct Response: Decodable {
|
|
||||||
let translatedText: String
|
|
||||||
}
|
|
||||||
let response: Response = try await decodedData(for: request)
|
|
||||||
return response.translatedText
|
|
||||||
}
|
|
||||||
|
|
||||||
private func makeURL(path: String) throws -> URL {
|
|
||||||
guard var components = URLComponents(string: url) else {
|
|
||||||
throw URLError(.badURL)
|
|
||||||
}
|
|
||||||
components.path = path
|
|
||||||
guard let url = components.url else {
|
|
||||||
throw URLError(.badURL)
|
|
||||||
}
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
|
|
||||||
private func decodedData<Output: Decodable>(for request: URLRequest) async throws -> Output {
|
|
||||||
let data = try await session.data(for: request)
|
|
||||||
let result = try decoder.decode(Output.self, from: data)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension URLSession {
|
|
||||||
func data(for request: URLRequest) async throws -> Data {
|
|
||||||
var task: URLSessionDataTask?
|
|
||||||
let onCancel = { task?.cancel() }
|
|
||||||
return try await withTaskCancellationHandler(
|
|
||||||
operation: {
|
|
||||||
try await withCheckedThrowingContinuation { continuation in
|
|
||||||
task = dataTask(with: request) { data, _, error in
|
|
||||||
guard let data = data else {
|
|
||||||
let error = error ?? URLError(.badServerResponse)
|
|
||||||
return continuation.resume(throwing: error)
|
|
||||||
}
|
|
||||||
continuation.resume(returning: data)
|
|
||||||
}
|
|
||||||
task?.resume()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onCancel: { onCancel() }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
struct NoteContentView_Previews: PreviewProvider {
|
struct NoteContentView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
let state = test_damus_state()
|
let state = test_damus_state()
|
||||||
|
|||||||
Reference in New Issue
Block a user