From da0b92e13d2688766ebdff4834d52aeba9514661 Mon Sep 17 00:00:00 2001 From: Paul Schuetz Date: Sun, 19 Mar 2023 16:18:13 +0100 Subject: [PATCH] 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 --- .../Account/AccountDetailContextMenu.swift | 11 ++++++++++ .../Account/AccountDetailHeaderView.swift | 22 +++++++++++++++++++ .../Account/AccountDetailViewModel.swift | 21 ++++++++++++++++++ ...tusTranslation.swift => Translation.swift} | 4 ++-- .../Network/Sources/Network/DeepLClient.swift | 2 +- .../Status/Row/StatusRowViewModel.swift | 4 ++-- 6 files changed, 59 insertions(+), 5 deletions(-) rename Packages/Models/Sources/Models/{StatusTranslation.swift => Translation.swift} (80%) diff --git a/Packages/Account/Sources/Account/AccountDetailContextMenu.swift b/Packages/Account/Sources/Account/AccountDetailContextMenu.swift index 7f5f0a12..86bb6b3b 100644 --- a/Packages/Account/Sources/Account/AccountDetailContextMenu.swift +++ b/Packages/Account/Sources/Account/AccountDetailContextMenu.swift @@ -150,6 +150,17 @@ public struct AccountDetailContextMenu: View { 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 { Button { routerPath.presentedSheet = .listAddAccount(account: account) diff --git a/Packages/Account/Sources/Account/AccountDetailHeaderView.swift b/Packages/Account/Sources/Account/AccountDetailHeaderView.swift index e69ebe4b..2203f22b 100644 --- a/Packages/Account/Sources/Account/AccountDetailHeaderView.swift +++ b/Packages/Account/Sources/Account/AccountDetailHeaderView.swift @@ -202,12 +202,34 @@ struct AccountDetailHeaderView: View { 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 } .padding(.horizontal, .layoutPadding) .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 { VStack { Text(count, format: .number.notation(.compactName)) diff --git a/Packages/Account/Sources/Account/AccountDetailViewModel.swift b/Packages/Account/Sources/Account/AccountDetailViewModel.swift index 3786711f..a8d80453 100644 --- a/Packages/Account/Sources/Account/AccountDetailViewModel.swift +++ b/Packages/Account/Sources/Account/AccountDetailViewModel.swift @@ -83,6 +83,9 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher { } } + @Published var translation: Translation? + @Published var isLoadingTranslation = false + private(set) var account: Account? private var tabTask: Task? @@ -263,4 +266,22 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher { func statusDidAppear(status _: Models.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 + } + } } diff --git a/Packages/Models/Sources/Models/StatusTranslation.swift b/Packages/Models/Sources/Models/Translation.swift similarity index 80% rename from Packages/Models/Sources/Models/StatusTranslation.swift rename to Packages/Models/Sources/Models/Translation.swift index 8b6390a8..2b16e5ff 100644 --- a/Packages/Models/Sources/Models/StatusTranslation.swift +++ b/Packages/Models/Sources/Models/Translation.swift @@ -1,6 +1,6 @@ import Foundation -public struct StatusTranslation: Decodable { +public struct Translation: Decodable { public let content: HTMLString public let detectedSourceLanguage: String public let provider: String @@ -12,4 +12,4 @@ public struct StatusTranslation: Decodable { } } -extension StatusTranslation: Sendable {} +extension Translation: Sendable {} diff --git a/Packages/Network/Sources/Network/DeepLClient.swift b/Packages/Network/Sources/Network/DeepLClient.swift index ea91d040..427db35d 100644 --- a/Packages/Network/Sources/Network/DeepLClient.swift +++ b/Packages/Network/Sources/Network/DeepLClient.swift @@ -48,7 +48,7 @@ public struct DeepLClient { deeplUserAPIFree = userAPIFree } - public func request(target: String, text: String) async throws -> StatusTranslation { + public func request(target: String, text: String) async throws -> Translation { do { var components = URLComponents(string: endpoint)! var queryItems: [URLQueryItem] = [] diff --git a/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift b/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift index 28f68a67..4eff791b 100644 --- a/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift +++ b/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift @@ -21,7 +21,7 @@ public class StatusRowViewModel: ObservableObject { @Published var isEmbedLoading: Bool = false @Published var isFiltered: Bool = false - @Published var translation: StatusTranslation? + @Published var translation: Translation? @Published var isLoadingTranslation: Bool = false @Published var showDeleteAlert: Bool = false @@ -294,7 +294,7 @@ public class StatusRowViewModel: ObservableObject { if !alwaysTranslateWithDeepl { do { // 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)) withAnimation { self.translation = translation