Translator.swift (7473B)
1 // 2 // Translator.swift 3 // damus 4 // 5 // Created by Terry Yiu on 2/4/23. 6 // 7 8 import Foundation 9 #if canImport(FoundationNetworking) 10 import FoundationNetworking 11 #endif 12 13 public struct Translator { 14 private let userSettingsStore: UserSettingsStore 15 private let purple: DamusPurple 16 private let session = URLSession.shared 17 private let encoder = JSONEncoder() 18 private let decoder = JSONDecoder() 19 20 init(_ userSettingsStore: UserSettingsStore, purple: DamusPurple) { 21 self.userSettingsStore = userSettingsStore 22 self.purple = purple 23 } 24 25 public func translate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? { 26 // Do not attempt to translate if the source and target languages are the same. 27 guard sourceLanguage != targetLanguage else { 28 return nil 29 } 30 31 switch userSettingsStore.translation_service { 32 case .purple: 33 return try await translateWithPurple(text, from: sourceLanguage, to: targetLanguage) 34 case .libretranslate: 35 return try await translateWithLibreTranslate(text, from: sourceLanguage, to: targetLanguage) 36 case .nokyctranslate: 37 return try await translateWithNoKYCTranslate(text, from: sourceLanguage, to: targetLanguage) 38 case .winetranslate: 39 return try await translateWithWineTranslate(text, from: sourceLanguage, to: targetLanguage) 40 case .deepl: 41 return try await translateWithDeepL(text, from: sourceLanguage, to: targetLanguage) 42 case .none: 43 return nil 44 } 45 } 46 47 private func translateWithLibreTranslate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? { 48 let url = try makeURL(userSettingsStore.libretranslate_url, path: "/translate") 49 50 var request = URLRequest(url: url) 51 request.httpMethod = "POST" 52 request.setValue("application/json", forHTTPHeaderField: "Content-Type") 53 54 struct RequestBody: Encodable { 55 let q: String 56 let source: String 57 let target: String 58 let api_key: String? 59 } 60 let body = RequestBody(q: text, source: sourceLanguage, target: targetLanguage, api_key: userSettingsStore.libretranslate_api_key) 61 request.httpBody = try encoder.encode(body) 62 63 struct Response: Decodable { 64 let translatedText: String 65 } 66 let response: Response = try await decodedData(for: request) 67 return response.translatedText 68 } 69 70 private func translateWithDeepL(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? { 71 if userSettingsStore.deepl_api_key == "" { 72 return nil 73 } 74 75 let url = try makeURL(userSettingsStore.deepl_plan.model.url, path: "/v2/translate") 76 77 var request = URLRequest(url: url) 78 request.httpMethod = "POST" 79 request.setValue("application/json", forHTTPHeaderField: "Content-Type") 80 request.setValue("DeepL-Auth-Key \(userSettingsStore.deepl_api_key)", forHTTPHeaderField: "Authorization") 81 82 struct RequestBody: Encodable { 83 let text: [String] 84 let source_lang: String 85 let target_lang: String 86 } 87 let body = RequestBody(text: [text], source_lang: sourceLanguage.uppercased(), target_lang: targetLanguage.uppercased()) 88 request.httpBody = try encoder.encode(body) 89 90 struct Response: Decodable { 91 let translations: [DeepLTranslations] 92 } 93 struct DeepLTranslations: Decodable { 94 let detected_source_language: String 95 let text: String 96 } 97 98 let response: Response = try await decodedData(for: request) 99 return response.translations.map { $0.text }.joined(separator: " ") 100 } 101 102 private func translateWithPurple(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? { 103 return try await self.purple.translate(text: text, source: sourceLanguage, target: targetLanguage) 104 } 105 106 private func translateWithNoKYCTranslate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? { 107 let url = try makeURL("https://translate.nokyctranslate.com", path: "/translate") 108 109 var request = URLRequest(url: url) 110 request.httpMethod = "POST" 111 request.setValue("application/json", forHTTPHeaderField: "Content-Type") 112 113 struct RequestBody: Encodable { 114 let q: String 115 let source: String 116 let target: String 117 let api_key: String? 118 } 119 let body = RequestBody(q: text, source: sourceLanguage, target: targetLanguage, api_key: userSettingsStore.nokyctranslate_api_key) 120 request.httpBody = try encoder.encode(body) 121 122 struct Response: Decodable { 123 let translatedText: String 124 } 125 let response: Response = try await decodedData(for: request) 126 return response.translatedText 127 } 128 129 private func translateWithWineTranslate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? { 130 let url = try makeURL("https://translate.nostr.wine", path: "/translate") 131 132 var request = URLRequest(url: url) 133 request.httpMethod = "POST" 134 request.setValue("application/json", forHTTPHeaderField: "Content-Type") 135 136 struct RequestBody: Encodable { 137 let q: String 138 let source: String 139 let target: String 140 let api_key: String? 141 } 142 let body = RequestBody(q: text, source: sourceLanguage, target: targetLanguage, api_key: userSettingsStore.winetranslate_api_key) 143 request.httpBody = try encoder.encode(body) 144 145 struct Response: Decodable { 146 let translatedText: String 147 } 148 let response: Response = try await decodedData(for: request) 149 return response.translatedText 150 } 151 152 private func makeURL(_ baseUrl: String, path: String) throws -> URL { 153 guard var components = URLComponents(string: baseUrl) else { 154 throw URLError(.badURL) 155 } 156 components.path = path 157 guard let url = components.url else { 158 throw URLError(.badURL) 159 } 160 return url 161 } 162 163 private func decodedData<Output: Decodable>(for request: URLRequest) async throws -> Output { 164 let data = try await session.data(for: request) 165 let result = try decoder.decode(Output.self, from: data) 166 return result 167 } 168 } 169 170 private extension URLSession { 171 func data(for request: URLRequest) async throws -> Data { 172 var task: URLSessionDataTask? 173 let onCancel = { task?.cancel() } 174 return try await withTaskCancellationHandler( 175 operation: { 176 try await withCheckedThrowingContinuation { continuation in 177 task = dataTask(with: request) { data, _, error in 178 guard let data = data else { 179 let error = error ?? URLError(.badServerResponse) 180 return continuation.resume(throwing: error) 181 } 182 continuation.resume(returning: data) 183 } 184 task?.resume() 185 } 186 }, 187 onCancel: { onCancel() } 188 ) 189 } 190 }