Allow translation of an account bio/note (#1276)

The bio (note) of an account can now be translated via DeepL. If the user has
put in his own DeepL API key, that is used, otherwise, the standard one is
used. See #1267

Signed-off-by: Paul Schuetz <pa.schuetz@web.de>
This commit is contained in:
Paul Schuetz 2023-03-19 16:18:13 +01:00 committed by GitHub
parent a08587643d
commit da0b92e13d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 59 additions and 5 deletions

View file

@ -150,6 +150,17 @@ public struct AccountDetailContextMenu: View {
Divider() Divider()
} }
if let lang = preferences.serverPreferences?.postLanguage ?? Locale.current.language.languageCode?.identifier
{
Button {
Task {
await viewModel.translate(userLang: lang)
}
} label: {
Label("status.action.translate", systemImage: "captions.bubble")
}
}
if viewModel.relationship?.following == true { if viewModel.relationship?.following == true {
Button { Button {
routerPath.presentedSheet = .listAddAccount(account: account) routerPath.presentedSheet = .listAddAccount(account: account)

View file

@ -202,12 +202,34 @@ struct AccountDetailHeaderView: View {
routerPath.handle(url: url) routerPath.handle(url: url)
}) })
if let translation = viewModel.translation, !viewModel.isLoadingTranslation {
GroupBox {
VStack(alignment: .leading, spacing: 4) {
Text(translation.content.asSafeMarkdownAttributedString)
.font(.scaledBody)
Text(getLocalizedStringLabel(langCode: translation.detectedSourceLanguage, provider: translation.provider))
.font(.footnote)
.foregroundColor(.gray)
}
}
.fixedSize(horizontal: false, vertical: true)
}
fieldsView fieldsView
} }
.padding(.horizontal, .layoutPadding) .padding(.horizontal, .layoutPadding)
.offset(y: -40) .offset(y: -40)
} }
private func getLocalizedStringLabel(langCode: String, provider: String) -> String {
if let localizedLanguage = Locale.current.localizedString(forLanguageCode: langCode) {
let format = NSLocalizedString("status.action.translated-label-from-%@-%@", comment: "")
return String.localizedStringWithFormat(format, localizedLanguage, provider)
} else {
return "status.action.translated-label-\(provider)"
}
}
private func makeCustomInfoLabel(title: LocalizedStringKey, count: Int, needsBadge: Bool = false) -> some View { private func makeCustomInfoLabel(title: LocalizedStringKey, count: Int, needsBadge: Bool = false) -> some View {
VStack { VStack {
Text(count, format: .number.notation(.compactName)) Text(count, format: .number.notation(.compactName))

View file

@ -83,6 +83,9 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
} }
} }
@Published var translation: Translation?
@Published var isLoadingTranslation = false
private(set) var account: Account? private(set) var account: Account?
private var tabTask: Task<Void, Never>? private var tabTask: Task<Void, Never>?
@ -263,4 +266,22 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
func statusDidAppear(status _: Models.Status) {} func statusDidAppear(status _: Models.Status) {}
func statusDidDisappear(status _: Status) {} func statusDidDisappear(status _: Status) {}
func translate(userLang: String) async {
guard let account else { return }
withAnimation {
isLoadingTranslation = true
}
let userAPIKey = DeepLUserAPIHandler.readIfAllowed()
let userAPIFree = UserPreferences.shared.userDeeplAPIFree
let deeplClient = DeepLClient(userAPIKey: userAPIKey, userAPIFree: userAPIFree)
let translation = try? await deeplClient.request(target: userLang, text: account.note.asRawText)
withAnimation {
self.translation = translation
isLoadingTranslation = false
}
}
} }

View file

@ -1,6 +1,6 @@
import Foundation import Foundation
public struct StatusTranslation: Decodable { public struct Translation: Decodable {
public let content: HTMLString public let content: HTMLString
public let detectedSourceLanguage: String public let detectedSourceLanguage: String
public let provider: String public let provider: String
@ -12,4 +12,4 @@ public struct StatusTranslation: Decodable {
} }
} }
extension StatusTranslation: Sendable {} extension Translation: Sendable {}

View file

@ -48,7 +48,7 @@ public struct DeepLClient {
deeplUserAPIFree = userAPIFree deeplUserAPIFree = userAPIFree
} }
public func request(target: String, text: String) async throws -> StatusTranslation { public func request(target: String, text: String) async throws -> Translation {
do { do {
var components = URLComponents(string: endpoint)! var components = URLComponents(string: endpoint)!
var queryItems: [URLQueryItem] = [] var queryItems: [URLQueryItem] = []

View file

@ -21,7 +21,7 @@ public class StatusRowViewModel: ObservableObject {
@Published var isEmbedLoading: Bool = false @Published var isEmbedLoading: Bool = false
@Published var isFiltered: Bool = false @Published var isFiltered: Bool = false
@Published var translation: StatusTranslation? @Published var translation: Translation?
@Published var isLoadingTranslation: Bool = false @Published var isLoadingTranslation: Bool = false
@Published var showDeleteAlert: Bool = false @Published var showDeleteAlert: Bool = false
@ -294,7 +294,7 @@ public class StatusRowViewModel: ObservableObject {
if !alwaysTranslateWithDeepl { if !alwaysTranslateWithDeepl {
do { do {
// We first use instance translation API if available. // We first use instance translation API if available.
let translation: StatusTranslation = try await client.post(endpoint: Statuses.translate(id: finalStatus.id, let translation: Translation = try await client.post(endpoint: Statuses.translate(id: finalStatus.id,
lang: userLang)) lang: userLang))
withAnimation { withAnimation {
self.translation = translation self.translation = translation