damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

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 }