diff --git a/IceCubesApp/Resources/Localization/de.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/de.lproj/Localizable.strings index 7c469f1e..d98341af 100644 --- a/IceCubesApp/Resources/Localization/de.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/de.lproj/Localizable.strings @@ -243,6 +243,8 @@ "timeline.trending" = "Im Trend"; // MARK: Package: Status +"status.action.translate" = "Übersetzen"; +"status.action.translated-label" = "Übersetzt mit DeepL.com"; "status.action.bookmark" = "Lesezeichen"; "status.action.boost" = "Boosten"; "status.action.copy-text" = "Text kopieren"; diff --git a/IceCubesApp/Resources/Localization/en.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/en.lproj/Localizable.strings index 21ddaeb9..4c419c7c 100644 --- a/IceCubesApp/Resources/Localization/en.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/en.lproj/Localizable.strings @@ -243,6 +243,8 @@ "timeline.trending" = "Trending"; // MARK: Package: Status +"status.action.translate" = "Translate"; +"status.action.translated-label" = "Translated using DeepL.com"; "status.action.bookmark" = "Bookmark"; "status.action.boost" = "Boost"; "status.action.copy-text" = "Copy Text"; diff --git a/IceCubesApp/Resources/Localization/es.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/es.lproj/Localizable.strings index 0c896154..326167f2 100644 --- a/IceCubesApp/Resources/Localization/es.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/es.lproj/Localizable.strings @@ -242,6 +242,8 @@ "timeline.trending" = "Tendencia"; // MARK: Package: Status +"status.action.translate" = "Traducir"; +"status.action.translated-label" = "Traducido usando DeepL.com"; "status.action.bookmark" = "Añadir a marcadores"; "status.action.boost" = "Boostear"; "status.action.copy-text" = "Copiar texto"; diff --git a/IceCubesApp/Resources/Localization/nl.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/nl.lproj/Localizable.strings index 03c47304..8bfacb92 100644 --- a/IceCubesApp/Resources/Localization/nl.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/nl.lproj/Localizable.strings @@ -243,6 +243,8 @@ "timeline.trending" = "Trending"; // MARK: Package: Status +"status.action.translate" = "Vertalen"; +"status.action.translated-label" = "Vertaald met behulp van DeepL.com"; "status.action.bookmark" = "Bladwijzer"; "status.action.boost" = "Boosten"; "status.action.copy-text" = "Tekst Kopiëren"; diff --git a/IceCubesApp/Secret.plist b/IceCubesApp/Secret.plist index f39534a7..e05bb39c 100644 --- a/IceCubesApp/Secret.plist +++ b/IceCubesApp/Secret.plist @@ -4,5 +4,7 @@ OPENAI_SECRET NICE_TRY + DEEPL_SECRET + NICE_TRY_AGAIN diff --git a/Packages/Network/Sources/Network/DeepLClient.swift b/Packages/Network/Sources/Network/DeepLClient.swift new file mode 100644 index 00000000..05da3015 --- /dev/null +++ b/Packages/Network/Sources/Network/DeepLClient.swift @@ -0,0 +1,52 @@ +import Foundation + +public struct DeepLClient { + private let endpoint = "https://api-free.deepl.com/v2/translate" + + private var APIKey: String { + if let path = Bundle.main.path(forResource: "Secret", ofType: "plist") { + let secret = NSDictionary(contentsOfFile: path) + return secret?["DEEPL_SECRET"] as? String ?? "" + } + return "" + } + + private var authorizationHeaderValue: String { + "DeepL-Auth-Key \(APIKey)" + } + + public struct Response: Decodable { + public struct Translation: Decodable { + public let detectedSourceLanguage: String + public let text: String + } + public let translations: [Translation] + } + + private var decoder: JSONDecoder { + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + return decoder + } + + public init() {} + + public func request(target: String, source: String?, text: String) async throws -> String { + do { + var components = URLComponents(string: endpoint)! + var queryItems: [URLQueryItem] = [] + queryItems.append(.init(name: "text", value: text)) + queryItems.append(.init(name: "target_lang", value: target.uppercased())) + components.queryItems = queryItems + var request = URLRequest(url: components.url!) + request.httpMethod = "POST" + request.setValue(authorizationHeaderValue, forHTTPHeaderField: "Authorization") + 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 ?? "" + } catch { + throw error + } + } +} diff --git a/Packages/Status/Sources/Status/Row/StatusRowView.swift b/Packages/Status/Sources/Status/Row/StatusRowView.swift index ef60afc3..fa43fc30 100644 --- a/Packages/Status/Sources/Status/Row/StatusRowView.swift +++ b/Packages/Status/Sources/Status/Row/StatusRowView.swift @@ -199,45 +199,15 @@ public struct StatusRowView: View { }) Spacer() } - - if !reasons.contains(.placeholder) { - if !viewModel.isCompact, !viewModel.isEmbedLoading, let embed = viewModel.embeddedStatus { - StatusEmbeddedView(status: embed) - } else if viewModel.isEmbedLoading, !viewModel.isCompact { - StatusEmbeddedView(status: .placeholder()) - .redacted(reason: .placeholder) - .shimmering() - } - } + + makeTranslateView(status: status) if let poll = status.poll { StatusPollView(poll: poll) } - if !status.mediaAttachments.isEmpty { - if theme.statusDisplayStyle == .compact { - HStack { - StatusMediaPreviewView(attachments: status.mediaAttachments, - sensitive: status.sensitive, - isNotifications: viewModel.isCompact) - Spacer() - } - .padding(.vertical, 4) - } else { - StatusMediaPreviewView(attachments: status.mediaAttachments, - sensitive: status.sensitive, - isNotifications: viewModel.isCompact) - .padding(.vertical, 4) - } - } - if let card = status.card, - viewModel.embeddedStatus?.url != status.card?.url.absoluteString, - status.mediaAttachments.isEmpty, - !viewModel.isEmbedLoading, - theme.statusDisplayStyle == .large - { - StatusCardView(card: card) - } + makeMediasView(status: status) + makeCardView(status: status) } } } @@ -275,4 +245,68 @@ public struct StatusRowView: View { .foregroundColor(.gray) .contentShape(Rectangle()) } + + @ViewBuilder + private func makeTranslateView(status: AnyStatus) -> some View { + if let userLang = preferences.serverPreferences?.postLanguage, + status.language != nil, + userLang != status.language, + !status.content.asRawText.isEmpty, + viewModel.translation == nil { + Button { + Task { + await viewModel.translate(userLang: userLang) + } + } label: { + if viewModel.isLoadingTranslation { + ProgressView() + } else { + Text("status.action.translate") + } + } + } else if let translation = viewModel.translation { + GroupBox { + VStack(alignment: .leading, spacing: 4) { + Text(translation) + .font(.scaledBody) + Text("status.action.translated-label") + .font(.footnote) + .foregroundColor(.gray) + } + } + .fixedSize(horizontal: false, vertical: true) + } + } + + @ViewBuilder + private func makeMediasView(status: AnyStatus) -> some View { + if !status.mediaAttachments.isEmpty { + if theme.statusDisplayStyle == .compact { + HStack { + StatusMediaPreviewView(attachments: status.mediaAttachments, + sensitive: status.sensitive, + isNotifications: viewModel.isCompact) + Spacer() + } + .padding(.vertical, 4) + } else { + StatusMediaPreviewView(attachments: status.mediaAttachments, + sensitive: status.sensitive, + isNotifications: viewModel.isCompact) + .padding(.vertical, 4) + } + } + } + + @ViewBuilder + private func makeCardView(status: AnyStatus) -> some View { + if let card = status.card, + viewModel.embeddedStatus?.url != status.card?.url.absoluteString, + status.mediaAttachments.isEmpty, + !viewModel.isEmbedLoading, + theme.statusDisplayStyle == .large + { + StatusCardView(card: card) + } + } } diff --git a/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift b/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift index fbb611a1..de65e9bb 100644 --- a/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift +++ b/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift @@ -22,6 +22,9 @@ public class StatusRowViewModel: ObservableObject { @Published var displaySpoiler: Bool = false @Published var isEmbedLoading: Bool = true @Published var isFiltered: Bool = false + + @Published var translation: String? + @Published var isLoadingTranslation: Bool = false var filter: Filtered? { status.reblog?.filtered?.first ?? status.filtered?.first @@ -220,4 +223,18 @@ public class StatusRowViewModel: ObservableObject { reblogsCount = status.reblog?.reblogsCount ?? status.reblogsCount repliesCount = status.reblog?.repliesCount ?? status.repliesCount } + + func translate(userLang: String) async { + let client = DeepLClient() + do { + withAnimation { + isLoadingTranslation = true + } + let translation = try await client.request(target: userLang, source: status.language, text: status.content.asRawText) + withAnimation { + isLoadingTranslation = false + self.translation = translation + } + } catch {} + } } diff --git a/ci_scripts/ci_pre_xcodebuild.sh b/ci_scripts/ci_pre_xcodebuild.sh index 1432f6fb..e032e334 100755 --- a/ci_scripts/ci_pre_xcodebuild.sh +++ b/ci_scripts/ci_pre_xcodebuild.sh @@ -2,5 +2,6 @@ cd ../IceCubesApp/ plutil -replace OPENAI_SECRET -string $OPENAI_SECRET Secret.plist +plutil -replace DEEPL_SECRET -string $DEEPL_SECRET Secret.plist plutil -p Secret.plist exit 0