From 022f2f9a491626dd69be7ca33efcfbd084d71f75 Mon Sep 17 00:00:00 2001 From: Paul Schuetz Date: Tue, 7 May 2024 01:08:20 +0200 Subject: [PATCH 01/16] Implement a first version of Apple's Translation The user can now choose between his instance's server, DeepL (with API key) and Apple's Translation framework. A translation is cleared if the translation type is changed. The strings aren't yet written, but the translations settings view's inconsistent background is now fixed. --- .../Settings/TranslationSettingsView.swift | 15 +++++++---- .../Localization/Localizable.xcstrings | 4 +++ .../Env/Sources/Env/DeepLUserAPIHandler.swift | 8 ++++-- .../Env/Sources/Env/TranslationType.swift | 5 ++++ .../Env/Sources/Env/UserPreferences.swift | 8 ++---- .../StatusKit/Row/StatusRowViewModel.swift | 27 ++++++++++++++++--- .../Row/Subviews/StatusRowTranslateView.swift | 24 +++++++++++++++-- 7 files changed, 73 insertions(+), 18 deletions(-) create mode 100644 Packages/Env/Sources/Env/TranslationType.swift diff --git a/IceCubesApp/App/Tabs/Settings/TranslationSettingsView.swift b/IceCubesApp/App/Tabs/Settings/TranslationSettingsView.swift index e844cc01f..768fbd2be 100644 --- a/IceCubesApp/App/Tabs/Settings/TranslationSettingsView.swift +++ b/IceCubesApp/App/Tabs/Settings/TranslationSettingsView.swift @@ -11,8 +11,8 @@ struct TranslationSettingsView: View { var body: some View { Form { - deepLToggle - if preferences.alwaysUseDeepl { + translationSelector + if preferences.preferredTranslationType == .useDeepl { Section("settings.translation.user-api-key") { deepLPicker SecureField("settings.translation.user-api-key", text: $apiKey) @@ -51,10 +51,12 @@ struct TranslationSettingsView: View { } @ViewBuilder - private var deepLToggle: some View { + private var translationSelector: some View { @Bindable var preferences = preferences - Toggle(isOn: $preferences.alwaysUseDeepl) { - Text("settings.translation.always-deepl") + Picker("settings.translation.preferred-translation-type", selection: $preferences.preferredTranslationType) { + ForEach(TranslationType.allCases, id: \.self) { type in + Text(type.rawValue).tag(type) + } } #if !os(visionOS) .listRowBackground(theme.primaryBackgroundColor) @@ -80,6 +82,9 @@ struct TranslationSettingsView: View { } footer: { Text("settings.translation.auto-detect-post-language-footer") } + #if !os(visionOS) + .listRowBackground(theme.primaryBackgroundColor) + #endif } private func writeNewValue() { diff --git a/IceCubesApp/Resources/Localization/Localizable.xcstrings b/IceCubesApp/Resources/Localization/Localizable.xcstrings index 46e0bc2a5..68912a015 100644 --- a/IceCubesApp/Resources/Localization/Localizable.xcstrings +++ b/IceCubesApp/Resources/Localization/Localizable.xcstrings @@ -59867,6 +59867,7 @@ } }, "settings.translation.always-deepl" : { + "extractionState" : "stale", "localizations" : { "be" : { "stringUnit" : { @@ -60573,6 +60574,9 @@ } } } + }, + "settings.translation.preferred-translation-type" : { + }, "settings.translation.user-api-key" : { "localizations" : { diff --git a/Packages/Env/Sources/Env/DeepLUserAPIHandler.swift b/Packages/Env/Sources/Env/DeepLUserAPIHandler.swift index 410265f1b..681e2c446 100644 --- a/Packages/Env/Sources/Env/DeepLUserAPIHandler.swift +++ b/Packages/Env/Sources/Env/DeepLUserAPIHandler.swift @@ -24,7 +24,7 @@ public enum DeepLUserAPIHandler { } public static func readIfAllowed() -> String? { - guard UserPreferences.shared.alwaysUseDeepl else { return nil } + guard UserPreferences.shared.preferredTranslationType == .useDeepl else { return nil } return readValue() } @@ -35,7 +35,11 @@ public enum DeepLUserAPIHandler { } public static func deactivateToggleIfNoKey() { - UserPreferences.shared.alwaysUseDeepl = shouldAlwaysUseDeepl + if UserPreferences.shared.preferredTranslationType == .useDeepl { + if readValue() == nil { + UserPreferences.shared.preferredTranslationType = .useServerIfPossible + } + } } public static var shouldAlwaysUseDeepl: Bool { diff --git a/Packages/Env/Sources/Env/TranslationType.swift b/Packages/Env/Sources/Env/TranslationType.swift new file mode 100644 index 000000000..54d47f937 --- /dev/null +++ b/Packages/Env/Sources/Env/TranslationType.swift @@ -0,0 +1,5 @@ +public enum TranslationType: String, CaseIterable { + case useServerIfPossible + case useDeepl + case useApple +} diff --git a/Packages/Env/Sources/Env/UserPreferences.swift b/Packages/Env/Sources/Env/UserPreferences.swift index b7a55ef45..e2d6e2fe5 100644 --- a/Packages/Env/Sources/Env/UserPreferences.swift +++ b/Packages/Env/Sources/Env/UserPreferences.swift @@ -185,11 +185,7 @@ import SwiftUI } } - public var alwaysUseDeepl: Bool { - didSet { - storage.alwaysUseDeepl = alwaysUseDeepl - } - } + public var preferredTranslationType: TranslationType public var userDeeplAPIFree: Bool { didSet { @@ -482,7 +478,7 @@ import SwiftUI appDefaultPostsSensitive = storage.appDefaultPostsSensitive appRequireAltText = storage.appRequireAltText autoPlayVideo = storage.autoPlayVideo - alwaysUseDeepl = storage.alwaysUseDeepl + preferredTranslationType = .useServerIfPossible userDeeplAPIFree = storage.userDeeplAPIFree autoDetectPostLanguage = storage.autoDetectPostLanguage inAppBrowserReaderView = storage.inAppBrowserReaderView diff --git a/Packages/StatusKit/Sources/StatusKit/Row/StatusRowViewModel.swift b/Packages/StatusKit/Sources/StatusKit/Row/StatusRowViewModel.swift index ad47c332f..b759cc74a 100644 --- a/Packages/StatusKit/Sources/StatusKit/Row/StatusRowViewModel.swift +++ b/Packages/StatusKit/Sources/StatusKit/Row/StatusRowViewModel.swift @@ -31,6 +31,15 @@ import SwiftUI var translation: Translation? var isLoadingTranslation: Bool = false var showDeleteAlert: Bool = false + var showAppleTranslation = false + var preferredTranslationType = TranslationType.useServerIfPossible { + didSet { + if oldValue != preferredTranslationType { + translation = nil + showAppleTranslation = false + } + } + } private(set) var actionsAccountsFetched: Bool = false var favoriters: [Account] = [] @@ -297,10 +306,16 @@ import SwiftUI } func translate(userLang: String) async { + updatePreferredTranslation() + if preferredTranslationType == .useApple { + showAppleTranslation = true + return + } + withAnimation { isLoadingTranslation = true } - if !alwaysTranslateWithDeepl { + if preferredTranslationType != .useDeepl { do { // We first use instance translation API if available. let translation: Translation = try await client.post(endpoint: Statuses.translate(id: finalStatus.id, @@ -342,8 +357,14 @@ import SwiftUI DeepLUserAPIHandler.readIfAllowed() } - var alwaysTranslateWithDeepl: Bool { - DeepLUserAPIHandler.shouldAlwaysUseDeepl + func updatePreferredTranslation() { + if DeepLUserAPIHandler.shouldAlwaysUseDeepl { + preferredTranslationType = .useDeepl + } else if UserPreferences.shared.preferredTranslationType == .useApple { + preferredTranslationType = .useApple + } else { + preferredTranslationType = .useServerIfPossible + } } func fetchRemoteStatus() async -> Bool { diff --git a/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowTranslateView.swift b/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowTranslateView.swift index a1a2fed4f..84fde2ba2 100644 --- a/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowTranslateView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowTranslateView.swift @@ -2,6 +2,7 @@ import DesignSystem import Env import Models import SwiftUI +import Translation @MainActor struct StatusRowTranslateView: View { @@ -35,7 +36,8 @@ struct StatusRowTranslateView: View { } } - var body: some View { + @ViewBuilder + var translateButton: some View { if !isInCaptureMode, !isCompact, let userLang = preferences.serverPreferences?.postLanguage, @@ -54,8 +56,26 @@ struct StatusRowTranslateView: View { } .buttonStyle(.borderless) } + } + + @ViewBuilder + var generalTranslateButton: some View { + if #available(iOS 17.4, *) { + @Bindable var viewModel = viewModel + translateButton + .translationPresentation(isPresented: $viewModel.showAppleTranslation, text: viewModel.finalStatus.content.asRawText) + } else { + translateButton + } + } + + var body: some View { + generalTranslateButton + .onChange(of: preferences.preferredTranslationType) { _, _ in + _ = viewModel.updatePreferredTranslation() + } - if let translation = viewModel.translation, !viewModel.isLoadingTranslation { + if let translation = viewModel.translation, !viewModel.isLoadingTranslation, preferences.preferredTranslationType != .useApple { GroupBox { VStack(alignment: .leading, spacing: 4) { Text(translation.content.asSafeMarkdownAttributedString) From 1940fbd1cb205490675e870e6902c1419665f13f Mon Sep 17 00:00:00 2001 From: Paul Schuetz Date: Wed, 8 May 2024 17:31:58 +0200 Subject: [PATCH 02/16] Transfer the old "always_use_deepl" setting The "always_use_deepl"-setting is now deleted, but its content is transferred to the equivalent value in "preferred_translation_type". --- Packages/Env/Sources/Env/UserPreferences.swift | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Packages/Env/Sources/Env/UserPreferences.swift b/Packages/Env/Sources/Env/UserPreferences.swift index e2d6e2fe5..dda77508a 100644 --- a/Packages/Env/Sources/Env/UserPreferences.swift +++ b/Packages/Env/Sources/Env/UserPreferences.swift @@ -25,7 +25,7 @@ import SwiftUI @AppStorage("app_require_alt_text") public var appRequireAltText = false @AppStorage("autoplay_video") public var autoPlayVideo = true @AppStorage("mute_video") public var muteVideo = true - @AppStorage("always_use_deepl") public var alwaysUseDeepl = false + @AppStorage("preferred_translation_type") public var preferredTranslationType = TranslationType.useServerIfPossible @AppStorage("user_deepl_api_free") public var userDeeplAPIFree = true @AppStorage("auto_detect_post_language") public var autoDetectPostLanguage = true @@ -63,7 +63,15 @@ import SwiftUI @AppStorage("sidebar_expanded") public var isSidebarExpanded: Bool = false - init() {} + init() { + let sharedDefault = UserDefaults.standard + if let alwaysUseDeepl = (sharedDefault.object(forKey: "always_use_deepl") as? Bool) { + if alwaysUseDeepl { + preferredTranslationType = .useDeepl + } + sharedDefault.removeObject(forKey: "always_use_deepl") + } + } } public static let sharedDefault = UserDefaults(suiteName: "group.com.thomasricouard.IceCubesApp") From 5e8c453aeb4269cd86e4655541a0b88aca11323a Mon Sep 17 00:00:00 2001 From: Paul Schuetz Date: Thu, 9 May 2024 00:48:13 +0200 Subject: [PATCH 03/16] Show the user if the DeepL-API key is still stored The user is now shown a prompt if they've switched away from .useDeepl, but there's still an API key stored. The API key is not deleted if the user doesn't instruct the app to do so, so this change makes it more transparent, since a user might not expect the key to be stored and might not want this to be the case. --- .../Settings/TranslationSettingsView.swift | 32 ++++++++++++++----- .../Account/AccountDetailViewModel.swift | 2 +- .../Env/Sources/Env/DeepLUserAPIHandler.swift | 14 +++++--- .../Editor/Components/MediaEditView.swift | 2 +- .../StatusKit/Row/StatusRowViewModel.swift | 2 +- 5 files changed, 36 insertions(+), 16 deletions(-) diff --git a/IceCubesApp/App/Tabs/Settings/TranslationSettingsView.swift b/IceCubesApp/App/Tabs/Settings/TranslationSettingsView.swift index 768fbd2be..0fa599947 100644 --- a/IceCubesApp/App/Tabs/Settings/TranslationSettingsView.swift +++ b/IceCubesApp/App/Tabs/Settings/TranslationSettingsView.swift @@ -18,9 +18,6 @@ struct TranslationSettingsView: View { SecureField("settings.translation.user-api-key", text: $apiKey) .textContentType(.password) } - .onAppear { - readValue() - } #if !os(visionOS) .listRowBackground(theme.primaryBackgroundColor) #endif @@ -37,6 +34,7 @@ struct TranslationSettingsView: View { #endif } } + backgroundAPIKey autoDetectSection } .navigationTitle("settings.translation.navigation-title") @@ -48,6 +46,7 @@ struct TranslationSettingsView: View { writeNewValue() } .onAppear(perform: updatePrefs) + .onAppear(perform: readValue) } @ViewBuilder @@ -86,6 +85,27 @@ struct TranslationSettingsView: View { .listRowBackground(theme.primaryBackgroundColor) #endif } + + @ViewBuilder + private var backgroundAPIKey: some View { + if preferences.preferredTranslationType != .useDeepl, + !apiKey.isEmpty { + Section { + Text("settings.translation.api-key-still-stored") + Button(role: .destructive) { + withAnimation { + writeNewValue(value: "") + readValue() + } + } label: { + Text("action.delete") + } + } + #if !os(visionOS) + .listRowBackground(theme.primaryBackgroundColor) + #endif + } + } private func writeNewValue() { writeNewValue(value: apiKey) @@ -96,11 +116,7 @@ struct TranslationSettingsView: View { } private func readValue() { - if let apiKey = DeepLUserAPIHandler.readIfAllowed() { - self.apiKey = apiKey - } else { - apiKey = "" - } + apiKey = DeepLUserAPIHandler.readKey() } private func updatePrefs() { diff --git a/Packages/Account/Sources/Account/AccountDetailViewModel.swift b/Packages/Account/Sources/Account/AccountDetailViewModel.swift index 81c855a45..d5bb7b089 100644 --- a/Packages/Account/Sources/Account/AccountDetailViewModel.swift +++ b/Packages/Account/Sources/Account/AccountDetailViewModel.swift @@ -280,7 +280,7 @@ import SwiftUI isLoadingTranslation = true } - let userAPIKey = DeepLUserAPIHandler.readIfAllowed() + let userAPIKey = DeepLUserAPIHandler.readKeyIfAllowed() let userAPIFree = UserPreferences.shared.userDeeplAPIFree let deeplClient = DeepLClient(userAPIKey: userAPIKey, userAPIFree: userAPIFree) diff --git a/Packages/Env/Sources/Env/DeepLUserAPIHandler.swift b/Packages/Env/Sources/Env/DeepLUserAPIHandler.swift index 681e2c446..0cb2fc387 100644 --- a/Packages/Env/Sources/Env/DeepLUserAPIHandler.swift +++ b/Packages/Env/Sources/Env/DeepLUserAPIHandler.swift @@ -23,26 +23,30 @@ public enum DeepLUserAPIHandler { } } - public static func readIfAllowed() -> String? { + public static func readKeyIfAllowed() -> String? { guard UserPreferences.shared.preferredTranslationType == .useDeepl else { return nil } - return readValue() + return readKeyInternal() } - private static func readValue() -> String? { + public static func readKey() -> String { + return readKeyInternal() ?? "" + } + + private static func readKeyInternal() -> String? { keychain.synchronizable = true return keychain.get(key) } public static func deactivateToggleIfNoKey() { if UserPreferences.shared.preferredTranslationType == .useDeepl { - if readValue() == nil { + if readKeyInternal() == nil { UserPreferences.shared.preferredTranslationType = .useServerIfPossible } } } public static var shouldAlwaysUseDeepl: Bool { - readIfAllowed() != nil + readKeyIfAllowed() != nil } } diff --git a/Packages/StatusKit/Sources/StatusKit/Editor/Components/MediaEditView.swift b/Packages/StatusKit/Sources/StatusKit/Editor/Components/MediaEditView.swift index 0e68ac287..fabb31796 100644 --- a/Packages/StatusKit/Sources/StatusKit/Editor/Components/MediaEditView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Editor/Components/MediaEditView.swift @@ -161,7 +161,7 @@ extension StatusEditor { private func translateDescription() async -> String? { isTranslating = true - let userAPIKey = DeepLUserAPIHandler.readIfAllowed() + let userAPIKey = DeepLUserAPIHandler.readKeyIfAllowed() let userAPIFree = UserPreferences.shared.userDeeplAPIFree let deeplClient = DeepLClient(userAPIKey: userAPIKey, userAPIFree: userAPIFree) let lang = preferences.serverPreferences?.postLanguage ?? Locale.current.language.languageCode?.identifier diff --git a/Packages/StatusKit/Sources/StatusKit/Row/StatusRowViewModel.swift b/Packages/StatusKit/Sources/StatusKit/Row/StatusRowViewModel.swift index b759cc74a..d139e98ac 100644 --- a/Packages/StatusKit/Sources/StatusKit/Row/StatusRowViewModel.swift +++ b/Packages/StatusKit/Sources/StatusKit/Row/StatusRowViewModel.swift @@ -354,7 +354,7 @@ import SwiftUI } private var userAPIKey: String? { - DeepLUserAPIHandler.readIfAllowed() + DeepLUserAPIHandler.readKeyIfAllowed() } func updatePreferredTranslation() { From a8201e24559d71e09698504d6d506bbefce41d22 Mon Sep 17 00:00:00 2001 From: Paul Schuetz Date: Thu, 9 May 2024 01:24:50 +0200 Subject: [PATCH 04/16] Localize Labels The labels for the buttons and options are now localized. "DeepL API Key" is written consistently (with uppercase Key) --- .../Settings/TranslationSettingsView.swift | 2 +- .../Localization/Localizable.xcstrings | 181 ++++++------------ .../Env/Sources/Env/TranslationType.swift | 13 ++ 3 files changed, 68 insertions(+), 128 deletions(-) diff --git a/IceCubesApp/App/Tabs/Settings/TranslationSettingsView.swift b/IceCubesApp/App/Tabs/Settings/TranslationSettingsView.swift index 0fa599947..ec381aea2 100644 --- a/IceCubesApp/App/Tabs/Settings/TranslationSettingsView.swift +++ b/IceCubesApp/App/Tabs/Settings/TranslationSettingsView.swift @@ -54,7 +54,7 @@ struct TranslationSettingsView: View { @Bindable var preferences = preferences Picker("settings.translation.preferred-translation-type", selection: $preferences.preferredTranslationType) { ForEach(TranslationType.allCases, id: \.self) { type in - Text(type.rawValue).tag(type) + Text(type.description).tag(type) } } #if !os(visionOS) diff --git a/IceCubesApp/Resources/Localization/Localizable.xcstrings b/IceCubesApp/Resources/Localization/Localizable.xcstrings index 68912a015..dbe15db1e 100644 --- a/IceCubesApp/Resources/Localization/Localizable.xcstrings +++ b/IceCubesApp/Resources/Localization/Localizable.xcstrings @@ -26160,6 +26160,23 @@ } } }, + "enum.translation-type.use-server-if-possible" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Instanz" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Instance" + } + } + } + }, "env.poll-vote-frequency.multiple" : { "extractionState" : "manual", "localizations" : { @@ -59866,121 +59883,18 @@ } } }, - "settings.translation.always-deepl" : { - "extractionState" : "stale", + "settings.translation.api-key-still-stored" : { "localizations" : { - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "Always Translate using DeepL" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Always Translate using DeepL" - } - }, "de" : { "stringUnit" : { - "state" : "translated", - "value" : "Immer mit DeepL übersetzen" + "state" : "needs_review", + "value" : "Der DeepL-API-Schlüssel ist noch gespeichert!" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Always Translate using DeepL" - } - }, - "en-GB" : { - "stringUnit" : { - "state" : "translated", - "value" : "Always Translate using DeepL" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Traducir siempre con DeepL" - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Itzuli beti DeepL erabiliz" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Always Translate using DeepL" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Traduci sempre con DeepL" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "DeepLを使用して常に翻訳する" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "항상 DeepL을 통해 번역" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Oversett alltid med DeepL" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vertaal altijd met DeepL" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zawsze tłumacz przy pomocy DeepL" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sempre traduzir utilizando DeepL" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Her Zaman DeepL kullanarak Çevir" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Завжди перекладати за допомогою DeepL" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "总是使用 DeepL 翻译" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "一概用 DeepL 翻譯" + "value" : "The DeepL API Key is still stored!" } } } @@ -60461,122 +60375,135 @@ "localizations" : { "be" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "This feature requires a DeepL API key" } }, "ca" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "This feature requires a DeepL API key" } }, "de" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Für diese Funktion ist ein DeepL-API-Schlüssel erforderlich." } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "This feature requires a DeepL API key" + "value" : "This feature requires a DeepL API Key" } }, "en-GB" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "This feature requires a DeepL API key" } }, "es" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Esta funcionalidad requiere una clave API de DeepL" } }, "eu" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Ezaugarri honek DeepL API gako bat behar du" } }, "fr" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "This feature requires a DeepL API key" } }, "it" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Questa funzione richiede una chiave API DeepL" } }, "ja" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "この機能には DeepL APIキーが必要です" } }, "ko" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "이 기능을 사용하려면 DeepL에서 발급받은 API 키가 있어야 합니다." } }, "nb" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Denne funksjonen krever en DeepL API-nøkkel" } }, "nl" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Deze functionaliteit vereist een DeepL API-sleutel" } }, "pl" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Ta funkcja wymaga klucza DeepL API" } }, "pt-BR" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Esta funcionalidade requer uma chave de API DeepL" } }, "tr" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Bu özellik bir DeepL API anahtarı gerektirir" } }, "uk" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Ця функція потребує ключа DeepL API key" } }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "该功能需要 DeepL API 密钥" } }, "zh-Hant" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "本功能需有 DeepL API 密鑰" } } } }, "settings.translation.preferred-translation-type" : { - + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Übersetzungs-Service" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Translation Service" + } + } + } }, "settings.translation.user-api-key" : { "localizations" : { diff --git a/Packages/Env/Sources/Env/TranslationType.swift b/Packages/Env/Sources/Env/TranslationType.swift index 54d47f937..1939dc082 100644 --- a/Packages/Env/Sources/Env/TranslationType.swift +++ b/Packages/Env/Sources/Env/TranslationType.swift @@ -1,5 +1,18 @@ +import SwiftUI + public enum TranslationType: String, CaseIterable { case useServerIfPossible case useDeepl case useApple + + public var description: LocalizedStringKey { + switch self { + case .useServerIfPossible: + "enum.translation-type.use-server-if-possible" + case .useDeepl: + "DeepL" + case .useApple: + "Apple Translate" + } + } } From 86c5099662add18eaf6326c815de36344602c82d Mon Sep 17 00:00:00 2001 From: Paul Schuetz Date: Thu, 9 May 2024 14:14:13 +0200 Subject: [PATCH 05/16] Run all the strings through localization The strings "DeepL" and "Apple Translate" are now also saved in localizable.strings and addressed through keys. They were taken directly previously, which was inconsistent. --- .../Localization/Localizable.xcstrings | 22 +++++++++++++++++++ .../Env/Sources/Env/TranslationType.swift | 4 ++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/IceCubesApp/Resources/Localization/Localizable.xcstrings b/IceCubesApp/Resources/Localization/Localizable.xcstrings index dbe15db1e..5e8489d15 100644 --- a/IceCubesApp/Resources/Localization/Localizable.xcstrings +++ b/IceCubesApp/Resources/Localization/Localizable.xcstrings @@ -26160,6 +26160,28 @@ } } }, + "enum.translation-type.apple" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apple Translate" + } + } + } + }, + "enum.translation-type.deepl" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "DeepL" + } + } + } + }, "enum.translation-type.use-server-if-possible" : { "extractionState" : "manual", "localizations" : { diff --git a/Packages/Env/Sources/Env/TranslationType.swift b/Packages/Env/Sources/Env/TranslationType.swift index 1939dc082..eae8a6469 100644 --- a/Packages/Env/Sources/Env/TranslationType.swift +++ b/Packages/Env/Sources/Env/TranslationType.swift @@ -10,9 +10,9 @@ public enum TranslationType: String, CaseIterable { case .useServerIfPossible: "enum.translation-type.use-server-if-possible" case .useDeepl: - "DeepL" + "enum.translation-type.deepl" case .useApple: - "Apple Translate" + "enum.translation-type.apple" } } } From 40a11061b7bfe01eb9f1ed37f84f7e66840546be Mon Sep 17 00:00:00 2001 From: Paul Schuetz Date: Thu, 9 May 2024 14:51:55 +0200 Subject: [PATCH 06/16] Fix storage The selected value for preferredTranslationType wasn't stored, the synchronization between UserPreferences and Storage is now in place. --- Packages/Env/Sources/Env/UserPreferences.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Packages/Env/Sources/Env/UserPreferences.swift b/Packages/Env/Sources/Env/UserPreferences.swift index dda77508a..8a4b89a48 100644 --- a/Packages/Env/Sources/Env/UserPreferences.swift +++ b/Packages/Env/Sources/Env/UserPreferences.swift @@ -193,7 +193,11 @@ import SwiftUI } } - public var preferredTranslationType: TranslationType + public var preferredTranslationType: TranslationType { + didSet { + storage.preferredTranslationType = preferredTranslationType + } + } public var userDeeplAPIFree: Bool { didSet { @@ -486,7 +490,7 @@ import SwiftUI appDefaultPostsSensitive = storage.appDefaultPostsSensitive appRequireAltText = storage.appRequireAltText autoPlayVideo = storage.autoPlayVideo - preferredTranslationType = .useServerIfPossible + preferredTranslationType = storage.preferredTranslationType userDeeplAPIFree = storage.userDeeplAPIFree autoDetectPostLanguage = storage.autoDetectPostLanguage inAppBrowserReaderView = storage.inAppBrowserReaderView From b228c1e36b043e7dbb0c8300fdc2b97d19bb33d6 Mon Sep 17 00:00:00 2001 From: Paul Schuetz Date: Thu, 9 May 2024 15:03:07 +0200 Subject: [PATCH 07/16] Hide Apple Translate if not yet on iOS 17.4 The Apple Translate option is hidden if the user hasn't updated their phone to at least iOS 17.4. If the Apple Translate option is selected but the user has downgraded to before iOS 17.4, the standard instance option is selected. --- IceCubesApp/App/Tabs/Settings/TranslationSettingsView.swift | 6 +++++- Packages/Env/Sources/Env/UserPreferences.swift | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/IceCubesApp/App/Tabs/Settings/TranslationSettingsView.swift b/IceCubesApp/App/Tabs/Settings/TranslationSettingsView.swift index ec381aea2..cb87e9e96 100644 --- a/IceCubesApp/App/Tabs/Settings/TranslationSettingsView.swift +++ b/IceCubesApp/App/Tabs/Settings/TranslationSettingsView.swift @@ -54,7 +54,11 @@ struct TranslationSettingsView: View { @Bindable var preferences = preferences Picker("settings.translation.preferred-translation-type", selection: $preferences.preferredTranslationType) { ForEach(TranslationType.allCases, id: \.self) { type in - Text(type.description).tag(type) + if #available(iOS 17.4, *) { + Text(type.description).tag(type) + } else if type != .useApple { + Text(type.description).tag(type) + } } } #if !os(visionOS) diff --git a/Packages/Env/Sources/Env/UserPreferences.swift b/Packages/Env/Sources/Env/UserPreferences.swift index 8a4b89a48..0acb62a76 100644 --- a/Packages/Env/Sources/Env/UserPreferences.swift +++ b/Packages/Env/Sources/Env/UserPreferences.swift @@ -71,6 +71,10 @@ import SwiftUI } sharedDefault.removeObject(forKey: "always_use_deepl") } + if #unavailable(iOS 17.4), + preferredTranslationType == .useApple { + preferredTranslationType = .useServerIfPossible + } } } From e651f5c12c745c4a91f4400def9c08690ccad72a Mon Sep 17 00:00:00 2001 From: Paul Schuetz Date: Thu, 9 May 2024 22:09:46 +0200 Subject: [PATCH 08/16] Consistently show Apple Translate Apple Translate was previously only shown if the standard translate button was visible, that is now fixed. It's now attached to the StatusRowView, which is always present. --- .../Sources/StatusKit/Row/StatusRowView.swift | 12 ++++++++++++ .../Row/Subviews/StatusRowTranslateView.swift | 9 +-------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Packages/StatusKit/Sources/StatusKit/Row/StatusRowView.swift b/Packages/StatusKit/Sources/StatusKit/Row/StatusRowView.swift index 791b541e1..6f0503e65 100644 --- a/Packages/StatusKit/Sources/StatusKit/Row/StatusRowView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Row/StatusRowView.swift @@ -5,6 +5,7 @@ import Foundation import Models import Network import SwiftUI +import Translation @MainActor public struct StatusRowView: View { @@ -219,6 +220,7 @@ public struct StatusRowView: View { StatusDataControllerProvider.shared.dataController(for: viewModel.finalStatus, client: viewModel.client) ) + .addTranslateView(isPresented: $viewModel.showAppleTranslation, text: viewModel.finalStatus.content.asRawText) } @ViewBuilder @@ -355,3 +357,13 @@ public struct StatusRowView: View { .withPreviewsEnv() .environment(Theme.shared) } + +extension View { + func addTranslateView(isPresented: Binding, text: String) -> some View { + if #available(iOS 17.4, *) { + return self.translationPresentation(isPresented: isPresented, text: text) + } else { + return self + } + } +} diff --git a/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowTranslateView.swift b/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowTranslateView.swift index 84fde2ba2..47defae1b 100644 --- a/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowTranslateView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowTranslateView.swift @@ -2,7 +2,6 @@ import DesignSystem import Env import Models import SwiftUI -import Translation @MainActor struct StatusRowTranslateView: View { @@ -60,13 +59,7 @@ struct StatusRowTranslateView: View { @ViewBuilder var generalTranslateButton: some View { - if #available(iOS 17.4, *) { - @Bindable var viewModel = viewModel - translateButton - .translationPresentation(isPresented: $viewModel.showAppleTranslation, text: viewModel.finalStatus.content.asRawText) - } else { - translateButton - } + translateButton } var body: some View { From 7f3bfb6a7b2c400ff5e318edf67000356a3ff0ad Mon Sep 17 00:00:00 2001 From: Paul Schuetz Date: Thu, 9 May 2024 22:21:36 +0200 Subject: [PATCH 09/16] Animate the removal of translations The reset of a translation when the translation type is changed is now animated, which is important for iPad users if they've translated a post in the sidebar. --- .../StatusKit/Row/Subviews/StatusRowTranslateView.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowTranslateView.swift b/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowTranslateView.swift index 47defae1b..222f17698 100644 --- a/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowTranslateView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowTranslateView.swift @@ -65,7 +65,9 @@ struct StatusRowTranslateView: View { var body: some View { generalTranslateButton .onChange(of: preferences.preferredTranslationType) { _, _ in - _ = viewModel.updatePreferredTranslation() + withAnimation { + _ = viewModel.updatePreferredTranslation() + } } if let translation = viewModel.translation, !viewModel.isLoadingTranslation, preferences.preferredTranslationType != .useApple { From bb81bc43e4897478c81867e9d8a111869c6b9d85 Mon Sep 17 00:00:00 2001 From: Paul Schuetz Date: Thu, 9 May 2024 23:13:41 +0200 Subject: [PATCH 10/16] Add support for the Mac Catalyst build The Mac Catalyst Version doesn't allow the import of the api, so compiler flags now check if the import isn't allowed and then remove all references to Apple Translate. --- .../Settings/TranslationSettingsView.swift | 25 ++++++++++++++----- .../Env/Sources/Env/UserPreferences.swift | 10 ++++++++ .../Sources/StatusKit/Row/StatusRowView.swift | 24 ++++++++++-------- 3 files changed, 43 insertions(+), 16 deletions(-) diff --git a/IceCubesApp/App/Tabs/Settings/TranslationSettingsView.swift b/IceCubesApp/App/Tabs/Settings/TranslationSettingsView.swift index cb87e9e96..600884a6d 100644 --- a/IceCubesApp/App/Tabs/Settings/TranslationSettingsView.swift +++ b/IceCubesApp/App/Tabs/Settings/TranslationSettingsView.swift @@ -53,12 +53,8 @@ struct TranslationSettingsView: View { private var translationSelector: some View { @Bindable var preferences = preferences Picker("settings.translation.preferred-translation-type", selection: $preferences.preferredTranslationType) { - ForEach(TranslationType.allCases, id: \.self) { type in - if #available(iOS 17.4, *) { - Text(type.description).tag(type) - } else if type != .useApple { - Text(type.description).tag(type) - } + ForEach(allTTCases, id: \.self) { type in + Text(type.description).tag(type) } } #if !os(visionOS) @@ -66,6 +62,23 @@ struct TranslationSettingsView: View { #endif } + var allTTCases: [TranslationType] { + TranslationType.allCases.filter { type in + if type != .useApple { + return true + } + #if canImport(_Translation_SwiftUI) + if #available(iOS 17.4, *) { + return true + } else { + return false + } + #else + return false + #endif + } + } + @ViewBuilder private var deepLPicker: some View { @Bindable var preferences = preferences diff --git a/Packages/Env/Sources/Env/UserPreferences.swift b/Packages/Env/Sources/Env/UserPreferences.swift index 0acb62a76..af2664a59 100644 --- a/Packages/Env/Sources/Env/UserPreferences.swift +++ b/Packages/Env/Sources/Env/UserPreferences.swift @@ -64,6 +64,10 @@ import SwiftUI @AppStorage("sidebar_expanded") public var isSidebarExpanded: Bool = false init() { + prepareTranslationType() + } + + private func prepareTranslationType() { let sharedDefault = UserDefaults.standard if let alwaysUseDeepl = (sharedDefault.object(forKey: "always_use_deepl") as? Bool) { if alwaysUseDeepl { @@ -71,10 +75,16 @@ import SwiftUI } sharedDefault.removeObject(forKey: "always_use_deepl") } +#if canImport(_Translation_SwiftUI) if #unavailable(iOS 17.4), preferredTranslationType == .useApple { preferredTranslationType = .useServerIfPossible } +#else + if preferredTranslationType == .useApple { + preferredTranslationType = .useServerIfPossible + } +#endif } } diff --git a/Packages/StatusKit/Sources/StatusKit/Row/StatusRowView.swift b/Packages/StatusKit/Sources/StatusKit/Row/StatusRowView.swift index 6f0503e65..e7edc968c 100644 --- a/Packages/StatusKit/Sources/StatusKit/Row/StatusRowView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Row/StatusRowView.swift @@ -5,8 +5,20 @@ import Foundation import Models import Network import SwiftUI +#if canImport(_Translation_SwiftUI) import Translation +extension View { + func addTranslateView(isPresented: Binding, text: String) -> some View { + if #available(iOS 17.4, *) { + return self.translationPresentation(isPresented: isPresented, text: text) + } else { + return self + } + } +} +#endif + @MainActor public struct StatusRowView: View { @Environment(\.openWindow) private var openWindow @@ -220,7 +232,9 @@ public struct StatusRowView: View { StatusDataControllerProvider.shared.dataController(for: viewModel.finalStatus, client: viewModel.client) ) +#if canImport(_Translation_SwiftUI) .addTranslateView(isPresented: $viewModel.showAppleTranslation, text: viewModel.finalStatus.content.asRawText) +#endif } @ViewBuilder @@ -357,13 +371,3 @@ public struct StatusRowView: View { .withPreviewsEnv() .environment(Theme.shared) } - -extension View { - func addTranslateView(isPresented: Binding, text: String) -> some View { - if #available(iOS 17.4, *) { - return self.translationPresentation(isPresented: isPresented, text: text) - } else { - return self - } - } -} From 2f2e00d3b7dc57c5676acd1e80b4bae5a25a0870 Mon Sep 17 00:00:00 2001 From: Paul Schuetz Date: Fri, 10 May 2024 09:49:15 +0200 Subject: [PATCH 11/16] Swift Format --- .../Settings/TranslationSettingsView.swift | 19 ++++++------- .../Env/Sources/Env/DeepLUserAPIHandler.swift | 2 +- .../Env/Sources/Env/TranslationType.swift | 12 ++++----- .../Env/Sources/Env/UserPreferences.swift | 27 ++++++++++--------- .../Sources/StatusKit/Row/StatusRowView.swift | 20 +++++++------- 5 files changed, 41 insertions(+), 39 deletions(-) diff --git a/IceCubesApp/App/Tabs/Settings/TranslationSettingsView.swift b/IceCubesApp/App/Tabs/Settings/TranslationSettingsView.swift index 600884a6d..83b6260ff 100644 --- a/IceCubesApp/App/Tabs/Settings/TranslationSettingsView.swift +++ b/IceCubesApp/App/Tabs/Settings/TranslationSettingsView.swift @@ -68,17 +68,17 @@ struct TranslationSettingsView: View { return true } #if canImport(_Translation_SwiftUI) - if #available(iOS 17.4, *) { - return true - } else { - return false - } + if #available(iOS 17.4, *) { + return true + } else { + return false + } #else - return false + return false #endif } } - + @ViewBuilder private var deepLPicker: some View { @Bindable var preferences = preferences @@ -102,11 +102,12 @@ struct TranslationSettingsView: View { .listRowBackground(theme.primaryBackgroundColor) #endif } - + @ViewBuilder private var backgroundAPIKey: some View { if preferences.preferredTranslationType != .useDeepl, - !apiKey.isEmpty { + !apiKey.isEmpty + { Section { Text("settings.translation.api-key-still-stored") Button(role: .destructive) { diff --git a/Packages/Env/Sources/Env/DeepLUserAPIHandler.swift b/Packages/Env/Sources/Env/DeepLUserAPIHandler.swift index 0cb2fc387..7225bf257 100644 --- a/Packages/Env/Sources/Env/DeepLUserAPIHandler.swift +++ b/Packages/Env/Sources/Env/DeepLUserAPIHandler.swift @@ -32,7 +32,7 @@ public enum DeepLUserAPIHandler { public static func readKey() -> String { return readKeyInternal() ?? "" } - + private static func readKeyInternal() -> String? { keychain.synchronizable = true return keychain.get(key) diff --git a/Packages/Env/Sources/Env/TranslationType.swift b/Packages/Env/Sources/Env/TranslationType.swift index eae8a6469..e3ffb6057 100644 --- a/Packages/Env/Sources/Env/TranslationType.swift +++ b/Packages/Env/Sources/Env/TranslationType.swift @@ -7,12 +7,12 @@ public enum TranslationType: String, CaseIterable { public var description: LocalizedStringKey { switch self { - case .useServerIfPossible: - "enum.translation-type.use-server-if-possible" - case .useDeepl: - "enum.translation-type.deepl" - case .useApple: - "enum.translation-type.apple" + case .useServerIfPossible: + "enum.translation-type.use-server-if-possible" + case .useDeepl: + "enum.translation-type.deepl" + case .useApple: + "enum.translation-type.apple" } } } diff --git a/Packages/Env/Sources/Env/UserPreferences.swift b/Packages/Env/Sources/Env/UserPreferences.swift index af2664a59..9233e26af 100644 --- a/Packages/Env/Sources/Env/UserPreferences.swift +++ b/Packages/Env/Sources/Env/UserPreferences.swift @@ -60,13 +60,13 @@ import SwiftUI @AppStorage("show_reply_indentation") public var showReplyIndentation: Bool = true @AppStorage("show_account_popover") public var showAccountPopover: Bool = true - + @AppStorage("sidebar_expanded") public var isSidebarExpanded: Bool = false init() { prepareTranslationType() } - + private func prepareTranslationType() { let sharedDefault = UserDefaults.standard if let alwaysUseDeepl = (sharedDefault.object(forKey: "always_use_deepl") as? Bool) { @@ -75,16 +75,17 @@ import SwiftUI } sharedDefault.removeObject(forKey: "always_use_deepl") } -#if canImport(_Translation_SwiftUI) - if #unavailable(iOS 17.4), - preferredTranslationType == .useApple { - preferredTranslationType = .useServerIfPossible - } -#else - if preferredTranslationType == .useApple { - preferredTranslationType = .useServerIfPossible - } -#endif + #if canImport(_Translation_SwiftUI) + if #unavailable(iOS 17.4), + preferredTranslationType == .useApple + { + preferredTranslationType = .useServerIfPossible + } + #else + if preferredTranslationType == .useApple { + preferredTranslationType = .useServerIfPossible + } + #endif } } @@ -350,7 +351,7 @@ import SwiftUI storage.showAccountPopover = showAccountPopover } } - + public var isSidebarExpanded: Bool { didSet { storage.isSidebarExpanded = isSidebarExpanded diff --git a/Packages/StatusKit/Sources/StatusKit/Row/StatusRowView.swift b/Packages/StatusKit/Sources/StatusKit/Row/StatusRowView.swift index e7edc968c..370f4ca00 100644 --- a/Packages/StatusKit/Sources/StatusKit/Row/StatusRowView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Row/StatusRowView.swift @@ -6,17 +6,17 @@ import Models import Network import SwiftUI #if canImport(_Translation_SwiftUI) -import Translation + import Translation -extension View { - func addTranslateView(isPresented: Binding, text: String) -> some View { - if #available(iOS 17.4, *) { - return self.translationPresentation(isPresented: isPresented, text: text) - } else { - return self + extension View { + func addTranslateView(isPresented: Binding, text: String) -> some View { + if #available(iOS 17.4, *) { + return self.translationPresentation(isPresented: isPresented, text: text) + } else { + return self + } } } -} #endif @MainActor @@ -232,9 +232,9 @@ public struct StatusRowView: View { StatusDataControllerProvider.shared.dataController(for: viewModel.finalStatus, client: viewModel.client) ) -#if canImport(_Translation_SwiftUI) + #if canImport(_Translation_SwiftUI) .addTranslateView(isPresented: $viewModel.showAppleTranslation, text: viewModel.finalStatus.content.asRawText) -#endif + #endif } @ViewBuilder From 37b14bfb0df5f640641659dc2bb03de0216a6836 Mon Sep 17 00:00:00 2001 From: Paul Schuetz Date: Mon, 13 May 2024 09:35:12 +0200 Subject: [PATCH 12/16] Revert "Run all the strings through localization" This reverts commit 86c5099662add18eaf6326c815de36344602c82d. # Conflicts: # Packages/Env/Sources/Env/TranslationType.swift --- .../Localization/Localizable.xcstrings | 22 ------------------- .../Env/Sources/Env/TranslationType.swift | 4 ++-- 2 files changed, 2 insertions(+), 24 deletions(-) diff --git a/IceCubesApp/Resources/Localization/Localizable.xcstrings b/IceCubesApp/Resources/Localization/Localizable.xcstrings index 5e8489d15..dbe15db1e 100644 --- a/IceCubesApp/Resources/Localization/Localizable.xcstrings +++ b/IceCubesApp/Resources/Localization/Localizable.xcstrings @@ -26160,28 +26160,6 @@ } } }, - "enum.translation-type.apple" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Apple Translate" - } - } - } - }, - "enum.translation-type.deepl" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "DeepL" - } - } - } - }, "enum.translation-type.use-server-if-possible" : { "extractionState" : "manual", "localizations" : { diff --git a/Packages/Env/Sources/Env/TranslationType.swift b/Packages/Env/Sources/Env/TranslationType.swift index e3ffb6057..ff58795cb 100644 --- a/Packages/Env/Sources/Env/TranslationType.swift +++ b/Packages/Env/Sources/Env/TranslationType.swift @@ -10,9 +10,9 @@ public enum TranslationType: String, CaseIterable { case .useServerIfPossible: "enum.translation-type.use-server-if-possible" case .useDeepl: - "enum.translation-type.deepl" + "DeepL" case .useApple: - "enum.translation-type.apple" + "Apple Translate" } } } From b4a1b0986464a14bebfcef8e4d078c8928cce47e Mon Sep 17 00:00:00 2001 From: Paul Schuetz Date: Mon, 13 May 2024 11:34:17 +0200 Subject: [PATCH 13/16] Remove the DeepL fallback The DeepL fallback for the instance translation service is removed, error messages are shown if a translation fails. --- .../App/Tabs/Settings/SettingsTab.swift | 30 +- .../Localization/Localizable.xcstrings | 276 ++++++++++-------- IceCubesApp/Secret.plist | 8 - Packages/Env/Sources/Env/Router.swift | 14 + .../Network/Sources/Network/DeepLClient.swift | 50 ++-- .../Sources/StatusKit/Row/StatusRowView.swift | 15 + .../StatusKit/Row/StatusRowViewModel.swift | 24 +- 7 files changed, 243 insertions(+), 174 deletions(-) delete mode 100644 IceCubesApp/Secret.plist diff --git a/IceCubesApp/App/Tabs/Settings/SettingsTab.swift b/IceCubesApp/App/Tabs/Settings/SettingsTab.swift index 4724eb6bb..592f8285e 100644 --- a/IceCubesApp/App/Tabs/Settings/SettingsTab.swift +++ b/IceCubesApp/App/Tabs/Settings/SettingsTab.swift @@ -31,7 +31,9 @@ struct SettingsTabs: View { @Binding var popToRootTab: Tab let isModal: Bool - + + @State private var startingPoint: SettingsStartingPoint? = nil + var body: some View { NavigationStack(path: $routerPath.path) { Form { @@ -64,6 +66,32 @@ struct SettingsTabs: View { } .withAppRouter() .withSheetDestinations(sheetDestinations: $routerPath.presentedSheet) + .onAppear { + startingPoint = RouterPath.settingsStartingPoint + RouterPath.settingsStartingPoint = nil + } + .navigationDestination(item: $startingPoint) { targetView in + switch targetView { + case .display: + DisplaySettingsView() + case .haptic: + HapticSettingsView() + case .remoteTimelines: + RemoteTimelinesSettingView() + case .tagGroups: + TagsGroupSettingView() + case .recentTags: + RecenTagsSettingView() + case .content: + ContentSettingsView() + case .swipeActions: + SwipeActionsSettingsView() + case .tabAndSidebarEntries: + EmptyView() + case .translation: + TranslationSettingsView() + } + } } .onAppear { routerPath.client = client diff --git a/IceCubesApp/Resources/Localization/Localizable.xcstrings b/IceCubesApp/Resources/Localization/Localizable.xcstrings index dbe15db1e..57b350f21 100644 --- a/IceCubesApp/Resources/Localization/Localizable.xcstrings +++ b/IceCubesApp/Resources/Localization/Localizable.xcstrings @@ -18412,6 +18412,126 @@ } } }, + "action.cancel" : { + "comment" : "MARK: Common strings", + "extractionState" : "stale", + "localizations" : { + "be" : { + "stringUnit" : { + "state" : "translated", + "value" : "Скасаваць" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancel·la" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abbrechen" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancel" + } + }, + "en-GB" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancel" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancelar" + } + }, + "eu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Utzi" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Annuler" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Annulla" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "キャンセル" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "취소" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avbryt" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Annuleer" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Anuluj" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancelar" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İptal Et" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Відміна" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "取消" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "取消" + } + } + } + }, "action.delete" : { "extractionState" : "manual", "localizations" : { @@ -20304,6 +20424,40 @@ } } }, + "alert.translation-error.deepl" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "DeepL konnte nicht erreicht werden!\nIst der API-Schlüssel korrekt?" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "DeepL couldn't be reached!\nIs the API Key correct?" + } + } + } + }, + "alert.translation-error.instance" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Der Übersetzung-Service deiner Instanz konnte nicht erreicht werden!" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "The Translation Service of your Instance couldn't be reached!" + } + } + } + }, "An error occured while posting to Mastodon, please try again." : { "localizations" : { "de" : { @@ -81080,126 +81234,6 @@ } } } - }, - "action.cancel" : { - "comment" : "MARK: Common strings", - "extractionState" : "stale", - "localizations" : { - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "Скасаваць" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cancel·la" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Abbrechen" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cancel" - } - }, - "en-GB" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cancel" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cancelar" - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Utzi" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Annuler" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Annulla" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "キャンセル" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "취소" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Avbryt" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Annuleer" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Anuluj" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cancelar" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "İptal Et" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Відміна" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "取消" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "取消" - } - } - } }, "Visibility" : { "localizations" : { @@ -81223,4 +81257,4 @@ } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/IceCubesApp/Secret.plist b/IceCubesApp/Secret.plist deleted file mode 100644 index 0a3d92585..000000000 --- a/IceCubesApp/Secret.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - DEEPL_SECRET - NICE_TRY_AGAIN - - diff --git a/Packages/Env/Sources/Env/Router.swift b/Packages/Env/Sources/Env/Router.swift index b0e325131..6827d2952 100644 --- a/Packages/Env/Sources/Env/Router.swift +++ b/Packages/Env/Sources/Env/Router.swift @@ -115,6 +115,18 @@ public enum SheetDestination: Identifiable, Hashable { } } +public enum SettingsStartingPoint { + case display + case haptic + case remoteTimelines + case tagGroups + case recentTags + case content + case swipeActions + case tabAndSidebarEntries + case translation +} + @MainActor @Observable public class RouterPath { public var client: Client? @@ -123,6 +135,8 @@ public enum SheetDestination: Identifiable, Hashable { public var path: [RouterDestination] = [] public var presentedSheet: SheetDestination? + public static var settingsStartingPoint: SettingsStartingPoint? = nil + public init() {} public func navigate(to: RouterDestination) { diff --git a/Packages/Network/Sources/Network/DeepLClient.swift b/Packages/Network/Sources/Network/DeepLClient.swift index 9a2c0394d..d3ba47a83 100644 --- a/Packages/Network/Sources/Network/DeepLClient.swift +++ b/Packages/Network/Sources/Network/DeepLClient.swift @@ -12,20 +12,8 @@ public struct DeepLClient: Sendable { "https://api\(deeplUserAPIFree && (deeplUserAPIKey != nil) ? "-free" : "").deepl.com/v2/translate" } - private var APIKey: String { - if let deeplUserAPIKey { - return deeplUserAPIKey - } - - 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)" + "DeepL-Auth-Key \(deeplUserAPIKey ?? "")" } public struct Response: Decodable { @@ -49,26 +37,22 @@ public struct DeepLClient: Sendable { } public func request(target: String, text: String) async throws -> Translation { - 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) - if let translation = response.translations.first { - return .init(content: translation.text.removingPercentEncoding ?? "", - detectedSourceLanguage: translation.detectedSourceLanguage, - provider: "DeepL.com") - } - throw DeepLError.notFound - } catch { - throw error + 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) + if let translation = response.translations.first { + return .init(content: translation.text.removingPercentEncoding ?? "", + detectedSourceLanguage: translation.detectedSourceLanguage, + provider: "DeepL.com") } + throw DeepLError.notFound } } diff --git a/Packages/StatusKit/Sources/StatusKit/Row/StatusRowView.swift b/Packages/StatusKit/Sources/StatusKit/Row/StatusRowView.swift index 370f4ca00..4d1bbb41d 100644 --- a/Packages/StatusKit/Sources/StatusKit/Row/StatusRowView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Row/StatusRowView.swift @@ -28,6 +28,7 @@ public struct StatusRowView: View { @Environment(\.accessibilityVoiceOverEnabled) private var accessibilityVoiceOverEnabled @Environment(\.isStatusFocused) private var isFocused @Environment(\.indentationLevel) private var indentationLevel + @Environment(RouterPath.self) private var routerPath: RouterPath @Environment(QuickLook.self) private var quickLook @Environment(Theme.self) private var theme @@ -232,6 +233,20 @@ public struct StatusRowView: View { StatusDataControllerProvider.shared.dataController(for: viewModel.finalStatus, client: viewModel.client) ) + .alert("alert.translation-error.deepl", isPresented: $viewModel.deeplTranslationError) { + Button("alert.button.ok", role: .cancel) {} + Button("settings.general.translate") { + RouterPath.settingsStartingPoint = .translation + routerPath.presentedSheet = .settings + } + } + .alert("alert.translation-error.instance", isPresented: $viewModel.instanceTranslationError) { + Button("alert.button.ok", role: .cancel) {} + Button("settings.general.translate") { + RouterPath.settingsStartingPoint = .translation + routerPath.presentedSheet = .settings + } + } #if canImport(_Translation_SwiftUI) .addTranslateView(isPresented: $viewModel.showAppleTranslation, text: viewModel.finalStatus.content.asRawText) #endif diff --git a/Packages/StatusKit/Sources/StatusKit/Row/StatusRowViewModel.swift b/Packages/StatusKit/Sources/StatusKit/Row/StatusRowViewModel.swift index d139e98ac..1555ea935 100644 --- a/Packages/StatusKit/Sources/StatusKit/Row/StatusRowViewModel.swift +++ b/Packages/StatusKit/Sources/StatusKit/Row/StatusRowViewModel.swift @@ -40,6 +40,8 @@ import SwiftUI } } } + var deeplTranslationError = false + var instanceTranslationError = false private(set) var actionsAccountsFetched: Bool = false var favoriters: [Account] = [] @@ -316,21 +318,21 @@ import SwiftUI isLoadingTranslation = true } if preferredTranslationType != .useDeepl { - do { - // We first use instance translation API if available. - let translation: Translation = 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 - isLoadingTranslation = false + self.translation = translation + isLoadingTranslation = false } - - return - } catch {} + if translation == nil { + instanceTranslationError = true + } + } else { + await translateWithDeepL(userLang: userLang) + if translation == nil { + deeplTranslationError = true + } } - - // If not or fail we use Ice Cubes own DeepL client. - await translateWithDeepL(userLang: userLang) } func translateWithDeepL(userLang: String) async { From 643ad7550682c8bea7dd7c2b34ea054b92a4d4a0 Mon Sep 17 00:00:00 2001 From: Paul Schuetz Date: Mon, 13 May 2024 11:59:32 +0200 Subject: [PATCH 14/16] Allow for the use of an User API Key as fallback The DeepL fallback is reinstated if the user has put in their own API Key --- IceCubesApp.xcodeproj/project.pbxproj | 4 ---- .../Tabs/Settings/TranslationSettingsView.swift | 3 +++ .../Localization/Localizable.xcstrings | 14 ++++++++++++++ .../StatusKit/Row/StatusRowViewModel.swift | 17 ++++++++++++----- 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/IceCubesApp.xcodeproj/project.pbxproj b/IceCubesApp.xcodeproj/project.pbxproj index e4717abfd..18f7f273d 100644 --- a/IceCubesApp.xcodeproj/project.pbxproj +++ b/IceCubesApp.xcodeproj/project.pbxproj @@ -87,7 +87,6 @@ 9F7D93942980063100EE6B7A /* AppAccount in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7D93932980063100EE6B7A /* AppAccount */; }; 9F7D939A29805DBD00EE6B7A /* AccountSettingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7D939929805DBD00EE6B7A /* AccountSettingView.swift */; }; 9FA6FD6229C04A8800E2312C /* TranslationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA6FD6129C04A8800E2312C /* TranslationSettingsView.swift */; }; - 9FAD85832971BF7200496AB1 /* Secret.plist in Resources */ = {isa = PBXBuildFile; fileRef = 9FAD85822971BF7200496AB1 /* Secret.plist */; }; 9FAD858B29743F7400496AB1 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FAD858A29743F7400496AB1 /* ShareViewController.swift */; }; 9FAD858E29743F7400496AB1 /* (null) in Resources */ = {isa = PBXBuildFile; }; 9FAD859229743F7400496AB1 /* IceCubesShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 9FAD858829743F7400496AB1 /* IceCubesShareExtension.appex */; platformFilters = (ios, maccatalyst, ); settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; @@ -271,7 +270,6 @@ 9F7D939529800B0300EE6B7A /* IceCubesApp-release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "IceCubesApp-release.xcconfig"; sourceTree = ""; }; 9F7D939929805DBD00EE6B7A /* AccountSettingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingView.swift; sourceTree = ""; }; 9FA6FD6129C04A8800E2312C /* TranslationSettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TranslationSettingsView.swift; sourceTree = ""; }; - 9FAD85822971BF7200496AB1 /* Secret.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Secret.plist; sourceTree = ""; }; 9FAD858829743F7400496AB1 /* IceCubesShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = IceCubesShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 9FAD858A29743F7400496AB1 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; }; 9FAD858F29743F7400496AB1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -602,7 +600,6 @@ 9FAE4AC8293774FF00772766 /* Info.plist */, 9F398AB429360A5800A889F2 /* App */, 9F398AB529360A6100A889F2 /* Resources */, - 9FAD85822971BF7200496AB1 /* Secret.plist */, ); path = IceCubesApp; sourceTree = ""; @@ -960,7 +957,6 @@ 9F18801829AE477F00D85459 /* favorite.wav in Resources */, 9F24EEB829360C330042359D /* Preview Assets.xcassets in Resources */, 069709A8298C87B5006E4CB5 /* OpenDyslexic-Regular.otf in Resources */, - 9FAD85832971BF7200496AB1 /* Secret.plist in Resources */, 9F18801229AE477F00D85459 /* tabSelection.wav in Resources */, 9F18801429AE477F00D85459 /* bookmark.wav in Resources */, 9F18801629AE477F00D85459 /* refresh.wav in Resources */, diff --git a/IceCubesApp/App/Tabs/Settings/TranslationSettingsView.swift b/IceCubesApp/App/Tabs/Settings/TranslationSettingsView.swift index 83b6260ff..a65b4c5bf 100644 --- a/IceCubesApp/App/Tabs/Settings/TranslationSettingsView.swift +++ b/IceCubesApp/App/Tabs/Settings/TranslationSettingsView.swift @@ -110,6 +110,9 @@ struct TranslationSettingsView: View { { Section { Text("settings.translation.api-key-still-stored") + if preferences.preferredTranslationType == .useServerIfPossible { + Text("It can however still be used as a fallback for your instance's translation service.") + } Button(role: .destructive) { withAnimation { writeNewValue(value: "") diff --git a/IceCubesApp/Resources/Localization/Localizable.xcstrings b/IceCubesApp/Resources/Localization/Localizable.xcstrings index 57b350f21..55010ee37 100644 --- a/IceCubesApp/Resources/Localization/Localizable.xcstrings +++ b/IceCubesApp/Resources/Localization/Localizable.xcstrings @@ -32513,6 +32513,20 @@ } } }, + "It can however still be used as a fallback for your instance's translation service." : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Er kann aber auch als Fallback für den Übersetzungsservice deiner Instanz dienen." + } + } + } + }, + "Key" : { + "extractionState" : "manual" + }, "list.edit.isExclusive" : { "extractionState" : "manual", "localizations" : { diff --git a/Packages/StatusKit/Sources/StatusKit/Row/StatusRowViewModel.swift b/Packages/StatusKit/Sources/StatusKit/Row/StatusRowViewModel.swift index 1555ea935..91e5d096c 100644 --- a/Packages/StatusKit/Sources/StatusKit/Row/StatusRowViewModel.swift +++ b/Packages/StatusKit/Sources/StatusKit/Row/StatusRowViewModel.swift @@ -40,6 +40,7 @@ import SwiftUI } } } + var deeplTranslationError = false var instanceTranslationError = false @@ -318,13 +319,19 @@ import SwiftUI isLoadingTranslation = true } if preferredTranslationType != .useDeepl { - let translation: Translation? = 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)) + + if translation == nil { + await translateWithDeepL(userLang: userLang) + } else { withAnimation { - self.translation = translation - isLoadingTranslation = false + self.translation = translation + isLoadingTranslation = false } - if translation == nil { + } + + if self.translation == nil { instanceTranslationError = true } } else { @@ -356,7 +363,7 @@ import SwiftUI } private var userAPIKey: String? { - DeepLUserAPIHandler.readKeyIfAllowed() + DeepLUserAPIHandler.readKey() } func updatePreferredTranslation() { From 7de6a0f0070908a05174f8d72d238565f6d85789 Mon Sep 17 00:00:00 2001 From: Paul Schuetz Date: Mon, 13 May 2024 12:26:40 +0200 Subject: [PATCH 15/16] Make the localization keys clear strings --- .../Settings/TranslationSettingsView.swift | 2 +- .../Localization/Localizable.xcstrings | 117 ++++++++---------- .../Env/Sources/Env/TranslationType.swift | 2 +- .../Sources/StatusKit/Row/StatusRowView.swift | 4 +- 4 files changed, 54 insertions(+), 71 deletions(-) diff --git a/IceCubesApp/App/Tabs/Settings/TranslationSettingsView.swift b/IceCubesApp/App/Tabs/Settings/TranslationSettingsView.swift index a65b4c5bf..4a0c246f6 100644 --- a/IceCubesApp/App/Tabs/Settings/TranslationSettingsView.swift +++ b/IceCubesApp/App/Tabs/Settings/TranslationSettingsView.swift @@ -109,7 +109,7 @@ struct TranslationSettingsView: View { !apiKey.isEmpty { Section { - Text("settings.translation.api-key-still-stored") + Text("The DeepL API Key is still stored!") if preferences.preferredTranslationType == .useServerIfPossible { Text("It can however still be used as a fallback for your instance's translation service.") } diff --git a/IceCubesApp/Resources/Localization/Localizable.xcstrings b/IceCubesApp/Resources/Localization/Localizable.xcstrings index 55010ee37..f1c094fd0 100644 --- a/IceCubesApp/Resources/Localization/Localizable.xcstrings +++ b/IceCubesApp/Resources/Localization/Localizable.xcstrings @@ -20424,40 +20424,6 @@ } } }, - "alert.translation-error.deepl" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "DeepL konnte nicht erreicht werden!\nIst der API-Schlüssel korrekt?" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "DeepL couldn't be reached!\nIs the API Key correct?" - } - } - } - }, - "alert.translation-error.instance" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Der Übersetzung-Service deiner Instanz konnte nicht erreicht werden!" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The Translation Service of your Instance couldn't be reached!" - } - } - } - }, "An error occured while posting to Mastodon, please try again." : { "localizations" : { "de" : { @@ -22178,6 +22144,23 @@ } } }, + "DeepL couldn't be reached!\nIs the API Key correct?" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "DeepL konnte nicht erreicht werden! Ist der API-Schlüssel korrekt?" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "DeepL couldn't be reached!\nIs the API Key correct?" + } + } + } + }, "design.tag.n-posts-from-n-participants %lld %lld" : { "comment" : "MARK: Package: DesignSystem", "extractionState" : "manual", @@ -26314,23 +26297,6 @@ } } }, - "enum.translation-type.use-server-if-possible" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Instanz" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Instance" - } - } - } - }, "env.poll-vote-frequency.multiple" : { "extractionState" : "manual", "localizations" : { @@ -30978,6 +30944,17 @@ } } }, + "Instance" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Instanz" + } + } + } + }, "instance.info.domains" : { "comment" : "MARK: Instances", "localizations" : { @@ -60051,22 +60028,6 @@ } } }, - "settings.translation.api-key-still-stored" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Der DeepL-API-Schlüssel ist noch gespeichert!" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The DeepL API Key is still stored!" - } - } - } - }, "settings.translation.api-key-type" : { "localizations" : { "be" : { @@ -76925,6 +76886,28 @@ } } }, + "The DeepL API Key is still stored!" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Der DeepL-API-Schlüssel ist noch gespeichert!" + } + } + } + }, + "The Translation Service of your Instance couldn't be reached!" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Der Übersetzung-Service deiner Instanz konnte nicht erreicht werden!" + } + } + } + }, "timeline-new-posts %lld" : { "comment" : "Turkish does not have \"-ler\" on words in the sentence, so it follows the singular word.", "extractionState" : "manual", diff --git a/Packages/Env/Sources/Env/TranslationType.swift b/Packages/Env/Sources/Env/TranslationType.swift index ff58795cb..70631c1e1 100644 --- a/Packages/Env/Sources/Env/TranslationType.swift +++ b/Packages/Env/Sources/Env/TranslationType.swift @@ -8,7 +8,7 @@ public enum TranslationType: String, CaseIterable { public var description: LocalizedStringKey { switch self { case .useServerIfPossible: - "enum.translation-type.use-server-if-possible" + "Instance" case .useDeepl: "DeepL" case .useApple: diff --git a/Packages/StatusKit/Sources/StatusKit/Row/StatusRowView.swift b/Packages/StatusKit/Sources/StatusKit/Row/StatusRowView.swift index 4d1bbb41d..056aae635 100644 --- a/Packages/StatusKit/Sources/StatusKit/Row/StatusRowView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Row/StatusRowView.swift @@ -233,14 +233,14 @@ public struct StatusRowView: View { StatusDataControllerProvider.shared.dataController(for: viewModel.finalStatus, client: viewModel.client) ) - .alert("alert.translation-error.deepl", isPresented: $viewModel.deeplTranslationError) { + .alert("DeepL couldn't be reached!\nIs the API Key correct?", isPresented: $viewModel.deeplTranslationError) { Button("alert.button.ok", role: .cancel) {} Button("settings.general.translate") { RouterPath.settingsStartingPoint = .translation routerPath.presentedSheet = .settings } } - .alert("alert.translation-error.instance", isPresented: $viewModel.instanceTranslationError) { + .alert("The Translation Service of your Instance couldn't be reached!", isPresented: $viewModel.instanceTranslationError) { Button("alert.button.ok", role: .cancel) {} Button("settings.general.translate") { RouterPath.settingsStartingPoint = .translation From 218553d1cf96068ba8799f52c6821cbc8e1fd114 Mon Sep 17 00:00:00 2001 From: Paul Schuetz Date: Mon, 13 May 2024 12:47:00 +0200 Subject: [PATCH 16/16] Make Apple and the instance a fallback Apple Translate is now a fallback for both other translation types, the instance service is a fallback for DeepL. --- .../StatusKit/Row/StatusRowViewModel.swift | 50 +++++++++++++------ 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/Packages/StatusKit/Sources/StatusKit/Row/StatusRowViewModel.swift b/Packages/StatusKit/Sources/StatusKit/Row/StatusRowViewModel.swift index 91e5d096c..03946b98c 100644 --- a/Packages/StatusKit/Sources/StatusKit/Row/StatusRowViewModel.swift +++ b/Packages/StatusKit/Sources/StatusKit/Row/StatusRowViewModel.swift @@ -315,29 +315,35 @@ import SwiftUI return } - withAnimation { - isLoadingTranslation = true - } if preferredTranslationType != .useDeepl { - let translation: Translation? = try? await client.post(endpoint: Statuses.translate(id: finalStatus.id, - lang: userLang)) - + await translateWithInstance(userLang: userLang) + if translation == nil { await translateWithDeepL(userLang: userLang) - } else { - withAnimation { - self.translation = translation - isLoadingTranslation = false - } - } - - if self.translation == nil { - instanceTranslationError = true } } else { await translateWithDeepL(userLang: userLang) + if translation == nil { + await translateWithInstance(userLang: userLang) + } + } + + var hasShown = false +#if canImport(_Translation_SwiftUI) + if translation == nil, + #available(iOS 17.4, *) { + showAppleTranslation = true + hasShown = true + } +#endif + + if !hasShown, + translation == nil { + if preferredTranslationType == .useDeepl { deeplTranslationError = true + } else { + instanceTranslationError = true } } } @@ -355,6 +361,20 @@ import SwiftUI isLoadingTranslation = false } } + + func translateWithInstance(userLang: String) async { + withAnimation { + isLoadingTranslation = true + } + + let translation: Translation? = try? await client.post(endpoint: Statuses.translate(id: finalStatus.id, + lang: userLang)) + + withAnimation { + self.translation = translation + isLoadingTranslation = false + } + } private func getDeepLClient() -> DeepLClient { let userAPIfree = UserPreferences.shared.userDeeplAPIFree