Reworked translate: Use Mastodon API when available + always show the action in the post context menu

This commit is contained in:
Thomas Ricouard 2023-02-12 13:22:36 +01:00
parent 0449120684
commit 41058919bd
22 changed files with 70 additions and 30 deletions

View file

@ -338,7 +338,7 @@
// MARK: Package: Status
"status.action.translate" = "Tradueix";
"status.action.translate-from-%@" = "Tradueix del %@";
"status.action.translated-label" = "Traduït amb DeepL.com";
"status.action.translated-label-%@" = "Traduït amb %@";
"status.action.bookmark" = "Afegeix als marcadors";
"status.action.boost" = "Impulsa";
"status.action.copy-text" = "Copia el text";

View file

@ -339,7 +339,7 @@
// MARK: Package: Status
"status.action.translate" = "Übersetzen";
"status.action.translate-from-%@" = "Aus %@ übersetzen";
"status.action.translated-label" = "Übersetzt mit DeepL.com";
"status.action.translated-label-%@" = "Übersetzt mit %@";
"status.action.bookmark" = "Lesezeichen";
"status.action.boost" = "Boosten";
"status.action.copy-text" = "Text kopieren";

View file

@ -341,7 +341,7 @@
// MARK: Package: Status
"status.action.translate" = "Translate";
"status.action.translate-from-%@" = "Translate from %@";
"status.action.translated-label" = "Translated using DeepL.com";
"status.action.translated-label-%@" = "Translated using %@";
"status.action.bookmark" = "Bookmark";
"status.action.boost" = "Boost";
"status.action.copy-text" = "Copy Text";

View file

@ -340,7 +340,7 @@
// MARK: Package: Status
"status.action.translate" = "Translate";
"status.action.translate-from-%@" = "Translate from %@";
"status.action.translated-label" = "Translated using DeepL.com";
"status.action.translated-label-%@" = "Translated using %@";
"status.action.bookmark" = "Bookmark";
"status.action.boost" = "Boost";
"status.action.copy-text" = "Copy Text";

View file

@ -340,7 +340,7 @@
// MARK: Package: Status
"status.action.translate" = "Traducir";
"status.action.translate-from-%@" = "Traducir desde %@";
"status.action.translated-label" = "Traducido usando DeepL.com";
"status.action.translated-label-%@" = "Traducido usando %@";
"status.action.bookmark" = "Añadir a marcadores";
"status.action.boost" = "Retootear";
"status.action.copy-text" = "Copiar texto";

View file

@ -339,7 +339,7 @@
// MARK: Package: Status
"status.action.translate" = "Itzuli";
"status.action.translate-from-%@" = "Egin %@(a)ren itzulpena";
"status.action.translated-label" = "DeepL.com erabiliz itzulia";
"status.action.translated-label-%@" = "%@ erabiliz itzulia";
"status.action.bookmark" = "Jarri laster-marka";
"status.action.boost" = "Bultzatu";
"status.action.copy-text" = "Kopiatu testua";

View file

@ -335,7 +335,7 @@
// MARK: Package: Status
"status.action.translate" = "Traduire";
"status.action.translate-from-%@" = "Traduire de %@";
"status.action.translated-label" = "Traduit avec DeepL.com";
"status.action.translated-label-%@" = "Traduit avec %@";
"status.action.bookmark" = "Marquer";
"status.action.boost" = "Promouvoir";
"status.action.copy-text" = "Copier le texte";

View file

@ -340,7 +340,7 @@
// MARK: Package: Status
"status.action.translate" = "Traduci";
"status.action.translate-from-%@" = "Traduci da %@";
"status.action.translated-label" = "Tradotto usando DeepL.com";
"status.action.translated-label-%@" = "Tradotto usando %@";
"status.action.bookmark" = "Salva nei segnalibri";
"status.action.boost" = "Condividi";
"status.action.copy-text" = "Copia il testo";

View file

@ -339,7 +339,7 @@
// MARK: Package: Status
"status.action.translate" = "翻訳";
"status.action.translate-from-%@" = "%@ から翻訳";
"status.action.translated-label" = "DeepL.comを使用して翻訳";
"status.action.translated-label-%@" = "%@ を使用して翻訳";
"status.action.bookmark" = "ブックマーク";
"status.action.boost" = "ブースト";
"status.action.copy-text" = "テキストをコピー";

View file

@ -341,7 +341,7 @@
// MARK: Package: Status
"status.action.translate" = "번역";
"status.action.translate-from-%@" = "%@에서 번역";
"status.action.translated-label" = "DeepL.com을 통해 번역됨";
"status.action.translated-label-%@" = "%@을 통해 번역됨";
"status.action.bookmark" = "보관함에 추가";
"status.action.boost" = "부스트";
"status.action.copy-text" = "복사";

View file

@ -339,7 +339,7 @@
// MARK: Package: Status
"status.action.translate" = "Oversett";
"status.action.translate-from-%@" = "Oversett fra %@";
"status.action.translated-label" = "Oversatt ved hjelp av DeepL.com";
"status.action.translated-label-%@" = "Oversatt ved hjelp av %@";
"status.action.bookmark" = "Bokmerk";
"status.action.boost" = "Forsterk";
"status.action.copy-text" = "Kopier tekst";

View file

@ -333,7 +333,7 @@
// MARK: Package: Status
"status.action.translate" = "Vertaal";
"status.action.translate-from-%@" = "Vertaal uit het %@";
"status.action.translated-label" = "Vertaald met behulp van DeepL.com";
"status.action.translated-label-%@" = "Vertaald met behulp van %@";
"status.action.bookmark" = "Voeg bladwijzer toe";
"status.action.boost" = "Boosten";
"status.action.copy-text" = "Kopieer tekst";

View file

@ -335,7 +335,7 @@
// MARK: Package: Status
"status.action.translate" = "Przetłumacz";
"status.action.translate-from-%@" = "Przetłumacz tekst %@";
"status.action.translated-label" = "Przetłumaczono za pomocą DeepL.com";
"status.action.translated-label-%@" = "Przetłumaczono za pomocą %@";
"status.action.bookmark" = "Dodaj zakładkę";
"status.action.boost" = "Podbij";
"status.action.copy-text" = "Kopiuj tekst";

View file

@ -339,7 +339,7 @@
// MARK: Package: Status
"status.action.translate" = "Traduzir";
"status.action.translate-from-%@" = "Traduzir do %@";
"status.action.translated-label" = "Traduzir usando DeepL.com";
"status.action.translated-label-%@" = "Traduzir usando %@";
"status.action.bookmark" = "Salvar";
"status.action.boost" = "Boost";
"status.action.copy-text" = "Copiar Texto";

View file

@ -335,7 +335,7 @@
// MARK: Package: Status
"status.action.translate" = "Tercüme et";
"status.action.translate-from-%@" = "Tercüme et %@";
"status.action.translated-label" = "DeepL.com tarafından tercüme edildi";
"status.action.translated-label-%@" = "%@ tarafından tercüme edildi";
"status.action.bookmark" = "Yer İmi Ekle";
"status.action.boost" = "Yükselt";
"status.action.copy-text" = "Yazıyı Kopyala";

View file

@ -340,7 +340,7 @@
// MARK: Package: Status
"status.action.translate" = "翻译";
"status.action.translate-from-%@" = "翻译 %@";
"status.action.translated-label" = "由 DeepL.com 翻译";
"status.action.translated-label-%@" = "由 %@ 翻译";
"status.action.bookmark" = "书签";
"status.action.boost" = "转发";
"status.action.copy-text" = "拷贝文本";

View file

@ -0,0 +1,13 @@
import Foundation
public struct StatusTranslation: Decodable {
public let content: HTMLString
public let detectedSourceLanguage: String
public let provider: String
public init(content: String, detectedSourceLanguage: String, provider: String) {
self.content = .init(stringValue: content)
self.detectedSourceLanguage = detectedSourceLanguage
self.provider = provider
}
}

View file

@ -1,6 +1,11 @@
import Foundation
import Models
public struct DeepLClient {
public enum DeepLError: Error {
case notFound
}
private let endpoint = "https://api.deepl.com/v2/translate"
private var APIKey: String {
@ -32,7 +37,7 @@ public struct DeepLClient {
public init() {}
public func request(target: String, source _: String?, text: String) async throws -> String {
public func request(target: String, source _: String?, text: String) async throws -> StatusTranslation {
do {
var components = URLComponents(string: endpoint)!
var queryItems: [URLQueryItem] = []
@ -45,7 +50,12 @@ public struct DeepLClient {
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
let (result, _) = try await URLSession.shared.data(for: request)
let response = try decoder.decode(Response.self, from: result)
return response.translations.first?.text.removingPercentEncoding ?? ""
if let translation = response.translations.first {
return .init(content: translation.text.removingPercentEncoding ?? "",
detectedSourceLanguage: translation.detectedSourceLanguage,
provider: "DeepL.com")
}
throw DeepLError.notFound
} catch {
throw error
}

View file

@ -17,6 +17,7 @@ public enum Statuses: Endpoint {
case bookmark(id: String)
case unbookmark(id: String)
case history(id: String)
case translate(id: String, lang: String?)
public func path() -> String {
switch self {
@ -50,6 +51,8 @@ public enum Statuses: Endpoint {
return "statuses/\(id)/unbookmark"
case let .history(id):
return "statuses/\(id)/history"
case let .translate(id, _):
return "statuses/\(id)/translate"
}
}
@ -59,6 +62,11 @@ public enum Statuses: Endpoint {
return makePaginationParam(sinceId: nil, maxId: maxId, mindId: nil)
case let .favoritedBy(_, maxId):
return makePaginationParam(sinceId: nil, maxId: maxId, mindId: nil)
case let .translate(_, lang):
if let lang {
return [.init(name: "lang", value: lang)]
}
return nil
default:
return nil
}

View file

@ -75,16 +75,15 @@ struct StatusRowContextMenu: View {
Label("status.action.copy-text", systemImage: "doc.on.doc")
}
if let lang = preferences.serverPreferences?.postLanguage ?? Locale.current.language.languageCode?.identifier,
let statusLanguage = viewModel.status.reblog?.language ?? viewModel.status.language,
statusLanguage != lang
if let lang = preferences.serverPreferences?.postLanguage ?? Locale.current.language.languageCode?.identifier
{
Button {
Task {
await viewModel.translate(userLang: lang)
}
} label: {
if let languageName = Locale.current.localizedString(forLanguageCode: statusLanguage) {
if let statusLang = viewModel.status.language,
let languageName = Locale.current.localizedString(forLanguageCode: statusLang) {
Label("status.action.translate-from-\(languageName)", systemImage: "captions.bubble")
} else {
Label("status.action.translate", systemImage: "captions.bubble")

View file

@ -375,9 +375,9 @@ public struct StatusRowView: View {
if let translation = viewModel.translation, !viewModel.isLoadingTranslation {
GroupBox {
VStack(alignment: .leading, spacing: 4) {
Text(translation)
Text(translation.content.asSafeMarkdownAttributedString)
.font(.scaledBody)
Text("status.action.translated-label")
Text("status.action.translated-label-\(translation.provider)")
.font(.footnote)
.foregroundColor(.gray)
}

View file

@ -26,7 +26,7 @@ public class StatusRowViewModel: ObservableObject {
@Published var isFiltered: Bool = false
@Published var isLoadingRemoteContent: Bool = false
@Published var translation: String?
@Published var translation: StatusTranslation?
@Published var isLoadingTranslation: Bool = false
@Published var showDeleteAlert: Bool = false
@ -292,18 +292,28 @@ public class StatusRowViewModel: ObservableObject {
}
func translate(userLang: String) async {
let client = DeepLClient()
guard let client else { return }
do {
withAnimation {
isLoadingTranslation = true
}
let translation = try await client.request(target: userLang,
source: status.language,
text: status.reblog?.content.asRawText ?? status.content.asRawText)
// We first use instance translation API if available.
let translation: StatusTranslation = try await client.post(endpoint: Statuses.translate(id: status.reblog?.id ?? status.id,
lang: userLang))
withAnimation {
self.translation = translation
isLoadingTranslation = false
}
} catch {}
} catch {
// If not or fail we use Ice Cubes own DeepL client.
let deepLClient = DeepLClient()
let translation = try? await deepLClient.request(target: userLang,
source: status.language,
text: status.reblog?.content.asRawText ?? status.content.asRawText)
withAnimation {
self.translation = translation
isLoadingTranslation = false
}
}
}
}