mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-11-22 08:20:59 +00:00
Implement Apple Translate (#2065)
* 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.
* 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".
* 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.
* Localize Labels
The labels for the buttons and options are now localized. "DeepL API Key" is written consistently (with uppercase Key)
* 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.
* Fix storage
The selected value for preferredTranslationType wasn't stored, the
synchronization between UserPreferences and Storage is now in place.
* 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.
* 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.
* 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.
* 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.
* Swift Format
* Revert "Run all the strings through localization"
This reverts commit 86c5099662
.
# Conflicts:
# Packages/Env/Sources/Env/TranslationType.swift
* Remove the DeepL fallback
The DeepL fallback for the instance translation service is removed,
error messages are shown if a translation fails.
* 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
* Make the localization keys clear strings
* 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.
This commit is contained in:
parent
a8039df22d
commit
48faddebea
15 changed files with 517 additions and 354 deletions
|
@ -87,7 +87,6 @@
|
||||||
9F7D93942980063100EE6B7A /* AppAccount in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7D93932980063100EE6B7A /* AppAccount */; };
|
9F7D93942980063100EE6B7A /* AppAccount in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7D93932980063100EE6B7A /* AppAccount */; };
|
||||||
9F7D939A29805DBD00EE6B7A /* AccountSettingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7D939929805DBD00EE6B7A /* AccountSettingView.swift */; };
|
9F7D939A29805DBD00EE6B7A /* AccountSettingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7D939929805DBD00EE6B7A /* AccountSettingView.swift */; };
|
||||||
9FA6FD6229C04A8800E2312C /* TranslationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA6FD6129C04A8800E2312C /* TranslationSettingsView.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 */; };
|
9FAD858B29743F7400496AB1 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FAD858A29743F7400496AB1 /* ShareViewController.swift */; };
|
||||||
9FAD858E29743F7400496AB1 /* (null) in Resources */ = {isa = PBXBuildFile; };
|
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, ); }; };
|
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 = "<group>"; };
|
9F7D939529800B0300EE6B7A /* IceCubesApp-release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "IceCubesApp-release.xcconfig"; sourceTree = "<group>"; };
|
||||||
9F7D939929805DBD00EE6B7A /* AccountSettingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingView.swift; sourceTree = "<group>"; };
|
9F7D939929805DBD00EE6B7A /* AccountSettingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingView.swift; sourceTree = "<group>"; };
|
||||||
9FA6FD6129C04A8800E2312C /* TranslationSettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TranslationSettingsView.swift; sourceTree = "<group>"; };
|
9FA6FD6129C04A8800E2312C /* TranslationSettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TranslationSettingsView.swift; sourceTree = "<group>"; };
|
||||||
9FAD85822971BF7200496AB1 /* Secret.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Secret.plist; sourceTree = "<group>"; };
|
|
||||||
9FAD858829743F7400496AB1 /* IceCubesShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = IceCubesShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
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 = "<group>"; };
|
9FAD858A29743F7400496AB1 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
|
||||||
9FAD858F29743F7400496AB1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
9FAD858F29743F7400496AB1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
@ -602,7 +600,6 @@
|
||||||
9FAE4AC8293774FF00772766 /* Info.plist */,
|
9FAE4AC8293774FF00772766 /* Info.plist */,
|
||||||
9F398AB429360A5800A889F2 /* App */,
|
9F398AB429360A5800A889F2 /* App */,
|
||||||
9F398AB529360A6100A889F2 /* Resources */,
|
9F398AB529360A6100A889F2 /* Resources */,
|
||||||
9FAD85822971BF7200496AB1 /* Secret.plist */,
|
|
||||||
);
|
);
|
||||||
path = IceCubesApp;
|
path = IceCubesApp;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -960,7 +957,6 @@
|
||||||
9F18801829AE477F00D85459 /* favorite.wav in Resources */,
|
9F18801829AE477F00D85459 /* favorite.wav in Resources */,
|
||||||
9F24EEB829360C330042359D /* Preview Assets.xcassets in Resources */,
|
9F24EEB829360C330042359D /* Preview Assets.xcassets in Resources */,
|
||||||
069709A8298C87B5006E4CB5 /* OpenDyslexic-Regular.otf in Resources */,
|
069709A8298C87B5006E4CB5 /* OpenDyslexic-Regular.otf in Resources */,
|
||||||
9FAD85832971BF7200496AB1 /* Secret.plist in Resources */,
|
|
||||||
9F18801229AE477F00D85459 /* tabSelection.wav in Resources */,
|
9F18801229AE477F00D85459 /* tabSelection.wav in Resources */,
|
||||||
9F18801429AE477F00D85459 /* bookmark.wav in Resources */,
|
9F18801429AE477F00D85459 /* bookmark.wav in Resources */,
|
||||||
9F18801629AE477F00D85459 /* refresh.wav in Resources */,
|
9F18801629AE477F00D85459 /* refresh.wav in Resources */,
|
||||||
|
|
|
@ -32,6 +32,8 @@ struct SettingsTabs: View {
|
||||||
|
|
||||||
let isModal: Bool
|
let isModal: Bool
|
||||||
|
|
||||||
|
@State private var startingPoint: SettingsStartingPoint? = nil
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack(path: $routerPath.path) {
|
NavigationStack(path: $routerPath.path) {
|
||||||
Form {
|
Form {
|
||||||
|
@ -64,6 +66,32 @@ struct SettingsTabs: View {
|
||||||
}
|
}
|
||||||
.withAppRouter()
|
.withAppRouter()
|
||||||
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
|
.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 {
|
.onAppear {
|
||||||
routerPath.client = client
|
routerPath.client = client
|
||||||
|
|
|
@ -11,16 +11,13 @@ struct TranslationSettingsView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
deepLToggle
|
translationSelector
|
||||||
if preferences.alwaysUseDeepl {
|
if preferences.preferredTranslationType == .useDeepl {
|
||||||
Section("settings.translation.user-api-key") {
|
Section("settings.translation.user-api-key") {
|
||||||
deepLPicker
|
deepLPicker
|
||||||
SecureField("settings.translation.user-api-key", text: $apiKey)
|
SecureField("settings.translation.user-api-key", text: $apiKey)
|
||||||
.textContentType(.password)
|
.textContentType(.password)
|
||||||
}
|
}
|
||||||
.onAppear {
|
|
||||||
readValue()
|
|
||||||
}
|
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
|
@ -37,6 +34,7 @@ struct TranslationSettingsView: View {
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
backgroundAPIKey
|
||||||
autoDetectSection
|
autoDetectSection
|
||||||
}
|
}
|
||||||
.navigationTitle("settings.translation.navigation-title")
|
.navigationTitle("settings.translation.navigation-title")
|
||||||
|
@ -48,19 +46,39 @@ struct TranslationSettingsView: View {
|
||||||
writeNewValue()
|
writeNewValue()
|
||||||
}
|
}
|
||||||
.onAppear(perform: updatePrefs)
|
.onAppear(perform: updatePrefs)
|
||||||
|
.onAppear(perform: readValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var deepLToggle: some View {
|
private var translationSelector: some View {
|
||||||
@Bindable var preferences = preferences
|
@Bindable var preferences = preferences
|
||||||
Toggle(isOn: $preferences.alwaysUseDeepl) {
|
Picker("settings.translation.preferred-translation-type", selection: $preferences.preferredTranslationType) {
|
||||||
Text("settings.translation.always-deepl")
|
ForEach(allTTCases, id: \.self) { type in
|
||||||
|
Text(type.description).tag(type)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
#endif
|
#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
|
@ViewBuilder
|
||||||
private var deepLPicker: some View {
|
private var deepLPicker: some View {
|
||||||
@Bindable var preferences = preferences
|
@Bindable var preferences = preferences
|
||||||
|
@ -80,6 +98,34 @@ struct TranslationSettingsView: View {
|
||||||
} footer: {
|
} footer: {
|
||||||
Text("settings.translation.auto-detect-post-language-footer")
|
Text("settings.translation.auto-detect-post-language-footer")
|
||||||
}
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var backgroundAPIKey: some View {
|
||||||
|
if preferences.preferredTranslationType != .useDeepl,
|
||||||
|
!apiKey.isEmpty
|
||||||
|
{
|
||||||
|
Section {
|
||||||
|
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.")
|
||||||
|
}
|
||||||
|
Button(role: .destructive) {
|
||||||
|
withAnimation {
|
||||||
|
writeNewValue(value: "")
|
||||||
|
readValue()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Text("action.delete")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func writeNewValue() {
|
private func writeNewValue() {
|
||||||
|
@ -91,11 +137,7 @@ struct TranslationSettingsView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func readValue() {
|
private func readValue() {
|
||||||
if let apiKey = DeepLUserAPIHandler.readIfAllowed() {
|
apiKey = DeepLUserAPIHandler.readKey()
|
||||||
self.apiKey = apiKey
|
|
||||||
} else {
|
|
||||||
apiKey = ""
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updatePrefs() {
|
private func updatePrefs() {
|
||||||
|
|
|
@ -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" : {
|
"action.delete" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
@ -22024,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" : {
|
"design.tag.n-posts-from-n-participants %lld %lld" : {
|
||||||
"comment" : "MARK: Package: DesignSystem",
|
"comment" : "MARK: Package: DesignSystem",
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
|
@ -30807,6 +30944,17 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Instance" : {
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Instanz"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"instance.info.domains" : {
|
"instance.info.domains" : {
|
||||||
"comment" : "MARK: Instances",
|
"comment" : "MARK: Instances",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
@ -32342,6 +32490,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" : {
|
"list.edit.isExclusive" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
@ -59866,124 +60028,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings.translation.always-deepl" : {
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"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 翻譯"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"settings.translation.api-key-type" : {
|
"settings.translation.api-key-type" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"be" : {
|
"be" : {
|
||||||
|
@ -60460,120 +60504,136 @@
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"be" : {
|
"be" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "This feature requires a DeepL API key"
|
"value" : "This feature requires a DeepL API key"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ca" : {
|
"ca" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "This feature requires a DeepL API key"
|
"value" : "This feature requires a DeepL API key"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Für diese Funktion ist ein DeepL-API-Schlüssel erforderlich."
|
"value" : "Für diese Funktion ist ein DeepL-API-Schlüssel erforderlich."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "This feature requires a DeepL API key"
|
"value" : "This feature requires a DeepL API Key"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"en-GB" : {
|
"en-GB" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "This feature requires a DeepL API key"
|
"value" : "This feature requires a DeepL API key"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"es" : {
|
"es" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Esta funcionalidad requiere una clave API de DeepL"
|
"value" : "Esta funcionalidad requiere una clave API de DeepL"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"eu" : {
|
"eu" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Ezaugarri honek DeepL API gako bat behar du"
|
"value" : "Ezaugarri honek DeepL API gako bat behar du"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fr" : {
|
"fr" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "This feature requires a DeepL API key"
|
"value" : "This feature requires a DeepL API key"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"it" : {
|
"it" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Questa funzione richiede una chiave API DeepL"
|
"value" : "Questa funzione richiede una chiave API DeepL"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ja" : {
|
"ja" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "この機能には DeepL APIキーが必要です"
|
"value" : "この機能には DeepL APIキーが必要です"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ko" : {
|
"ko" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "이 기능을 사용하려면 DeepL에서 발급받은 API 키가 있어야 합니다."
|
"value" : "이 기능을 사용하려면 DeepL에서 발급받은 API 키가 있어야 합니다."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nb" : {
|
"nb" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Denne funksjonen krever en DeepL API-nøkkel"
|
"value" : "Denne funksjonen krever en DeepL API-nøkkel"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nl" : {
|
"nl" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Deze functionaliteit vereist een DeepL API-sleutel"
|
"value" : "Deze functionaliteit vereist een DeepL API-sleutel"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pl" : {
|
"pl" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Ta funkcja wymaga klucza DeepL API"
|
"value" : "Ta funkcja wymaga klucza DeepL API"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Esta funcionalidade requer uma chave de API DeepL"
|
"value" : "Esta funcionalidade requer uma chave de API DeepL"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tr" : {
|
"tr" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Bu özellik bir DeepL API anahtarı gerektirir"
|
"value" : "Bu özellik bir DeepL API anahtarı gerektirir"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"uk" : {
|
"uk" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Ця функція потребує ключа DeepL API key"
|
"value" : "Ця функція потребує ключа DeepL API key"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"zh-Hans" : {
|
"zh-Hans" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "该功能需要 DeepL API 密钥"
|
"value" : "该功能需要 DeepL API 密钥"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"zh-Hant" : {
|
"zh-Hant" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "本功能需有 DeepL API 密鑰"
|
"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" : {
|
"settings.translation.user-api-key" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"be" : {
|
"be" : {
|
||||||
|
@ -76826,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" : {
|
"timeline-new-posts %lld" : {
|
||||||
"comment" : "Turkish does not have \"-ler\" on words in the sentence, so it follows the singular word.",
|
"comment" : "Turkish does not have \"-ler\" on words in the sentence, so it follows the singular word.",
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
|
@ -81149,126 +81231,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" : {
|
"Visibility" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>DEEPL_SECRET</key>
|
|
||||||
<string>NICE_TRY_AGAIN</string>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
|
@ -280,7 +280,7 @@ import SwiftUI
|
||||||
isLoadingTranslation = true
|
isLoadingTranslation = true
|
||||||
}
|
}
|
||||||
|
|
||||||
let userAPIKey = DeepLUserAPIHandler.readIfAllowed()
|
let userAPIKey = DeepLUserAPIHandler.readKeyIfAllowed()
|
||||||
let userAPIFree = UserPreferences.shared.userDeeplAPIFree
|
let userAPIFree = UserPreferences.shared.userDeeplAPIFree
|
||||||
let deeplClient = DeepLClient(userAPIKey: userAPIKey, userAPIFree: userAPIFree)
|
let deeplClient = DeepLClient(userAPIKey: userAPIKey, userAPIFree: userAPIFree)
|
||||||
|
|
||||||
|
|
|
@ -23,22 +23,30 @@ public enum DeepLUserAPIHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func readIfAllowed() -> String? {
|
public static func readKeyIfAllowed() -> String? {
|
||||||
guard UserPreferences.shared.alwaysUseDeepl else { return nil }
|
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
|
keychain.synchronizable = true
|
||||||
return keychain.get(key)
|
return keychain.get(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func deactivateToggleIfNoKey() {
|
public static func deactivateToggleIfNoKey() {
|
||||||
UserPreferences.shared.alwaysUseDeepl = shouldAlwaysUseDeepl
|
if UserPreferences.shared.preferredTranslationType == .useDeepl {
|
||||||
|
if readKeyInternal() == nil {
|
||||||
|
UserPreferences.shared.preferredTranslationType = .useServerIfPossible
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static var shouldAlwaysUseDeepl: Bool {
|
public static var shouldAlwaysUseDeepl: Bool {
|
||||||
readIfAllowed() != nil
|
readKeyIfAllowed() != nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
@MainActor
|
||||||
@Observable public class RouterPath {
|
@Observable public class RouterPath {
|
||||||
public var client: Client?
|
public var client: Client?
|
||||||
|
@ -123,6 +135,8 @@ public enum SheetDestination: Identifiable, Hashable {
|
||||||
public var path: [RouterDestination] = []
|
public var path: [RouterDestination] = []
|
||||||
public var presentedSheet: SheetDestination?
|
public var presentedSheet: SheetDestination?
|
||||||
|
|
||||||
|
public static var settingsStartingPoint: SettingsStartingPoint? = nil
|
||||||
|
|
||||||
public init() {}
|
public init() {}
|
||||||
|
|
||||||
public func navigate(to: RouterDestination) {
|
public func navigate(to: RouterDestination) {
|
||||||
|
|
18
Packages/Env/Sources/Env/TranslationType.swift
Normal file
18
Packages/Env/Sources/Env/TranslationType.swift
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
public enum TranslationType: String, CaseIterable {
|
||||||
|
case useServerIfPossible
|
||||||
|
case useDeepl
|
||||||
|
case useApple
|
||||||
|
|
||||||
|
public var description: LocalizedStringKey {
|
||||||
|
switch self {
|
||||||
|
case .useServerIfPossible:
|
||||||
|
"Instance"
|
||||||
|
case .useDeepl:
|
||||||
|
"DeepL"
|
||||||
|
case .useApple:
|
||||||
|
"Apple Translate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,7 +25,7 @@ import SwiftUI
|
||||||
@AppStorage("app_require_alt_text") public var appRequireAltText = false
|
@AppStorage("app_require_alt_text") public var appRequireAltText = false
|
||||||
@AppStorage("autoplay_video") public var autoPlayVideo = true
|
@AppStorage("autoplay_video") public var autoPlayVideo = true
|
||||||
@AppStorage("mute_video") public var muteVideo = 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("user_deepl_api_free") public var userDeeplAPIFree = true
|
||||||
@AppStorage("auto_detect_post_language") public var autoDetectPostLanguage = true
|
@AppStorage("auto_detect_post_language") public var autoDetectPostLanguage = true
|
||||||
|
|
||||||
|
@ -63,7 +63,30 @@ import SwiftUI
|
||||||
|
|
||||||
@AppStorage("sidebar_expanded") public var isSidebarExpanded: Bool = false
|
@AppStorage("sidebar_expanded") public var isSidebarExpanded: Bool = false
|
||||||
|
|
||||||
init() {}
|
init() {
|
||||||
|
prepareTranslationType()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func prepareTranslationType() {
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
#if canImport(_Translation_SwiftUI)
|
||||||
|
if #unavailable(iOS 17.4),
|
||||||
|
preferredTranslationType == .useApple
|
||||||
|
{
|
||||||
|
preferredTranslationType = .useServerIfPossible
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
if preferredTranslationType == .useApple {
|
||||||
|
preferredTranslationType = .useServerIfPossible
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static let sharedDefault = UserDefaults(suiteName: "group.com.thomasricouard.IceCubesApp")
|
public static let sharedDefault = UserDefaults(suiteName: "group.com.thomasricouard.IceCubesApp")
|
||||||
|
@ -185,9 +208,9 @@ import SwiftUI
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var alwaysUseDeepl: Bool {
|
public var preferredTranslationType: TranslationType {
|
||||||
didSet {
|
didSet {
|
||||||
storage.alwaysUseDeepl = alwaysUseDeepl
|
storage.preferredTranslationType = preferredTranslationType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -482,7 +505,7 @@ import SwiftUI
|
||||||
appDefaultPostsSensitive = storage.appDefaultPostsSensitive
|
appDefaultPostsSensitive = storage.appDefaultPostsSensitive
|
||||||
appRequireAltText = storage.appRequireAltText
|
appRequireAltText = storage.appRequireAltText
|
||||||
autoPlayVideo = storage.autoPlayVideo
|
autoPlayVideo = storage.autoPlayVideo
|
||||||
alwaysUseDeepl = storage.alwaysUseDeepl
|
preferredTranslationType = storage.preferredTranslationType
|
||||||
userDeeplAPIFree = storage.userDeeplAPIFree
|
userDeeplAPIFree = storage.userDeeplAPIFree
|
||||||
autoDetectPostLanguage = storage.autoDetectPostLanguage
|
autoDetectPostLanguage = storage.autoDetectPostLanguage
|
||||||
inAppBrowserReaderView = storage.inAppBrowserReaderView
|
inAppBrowserReaderView = storage.inAppBrowserReaderView
|
||||||
|
|
|
@ -12,20 +12,8 @@ public struct DeepLClient: Sendable {
|
||||||
"https://api\(deeplUserAPIFree && (deeplUserAPIKey != nil) ? "-free" : "").deepl.com/v2/translate"
|
"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 {
|
private var authorizationHeaderValue: String {
|
||||||
"DeepL-Auth-Key \(APIKey)"
|
"DeepL-Auth-Key \(deeplUserAPIKey ?? "")"
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct Response: Decodable {
|
public struct Response: Decodable {
|
||||||
|
@ -49,26 +37,22 @@ public struct DeepLClient: Sendable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public func request(target: String, text: String) async throws -> Translation {
|
public func request(target: String, text: String) async throws -> Translation {
|
||||||
do {
|
var components = URLComponents(string: endpoint)!
|
||||||
var components = URLComponents(string: endpoint)!
|
var queryItems: [URLQueryItem] = []
|
||||||
var queryItems: [URLQueryItem] = []
|
queryItems.append(.init(name: "text", value: text))
|
||||||
queryItems.append(.init(name: "text", value: text))
|
queryItems.append(.init(name: "target_lang", value: target.uppercased()))
|
||||||
queryItems.append(.init(name: "target_lang", value: target.uppercased()))
|
components.queryItems = queryItems
|
||||||
components.queryItems = queryItems
|
var request = URLRequest(url: components.url!)
|
||||||
var request = URLRequest(url: components.url!)
|
request.httpMethod = "POST"
|
||||||
request.httpMethod = "POST"
|
request.setValue(authorizationHeaderValue, forHTTPHeaderField: "Authorization")
|
||||||
request.setValue(authorizationHeaderValue, forHTTPHeaderField: "Authorization")
|
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
|
||||||
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
|
let (result, _) = try await URLSession.shared.data(for: request)
|
||||||
let (result, _) = try await URLSession.shared.data(for: request)
|
let response = try decoder.decode(Response.self, from: result)
|
||||||
let response = try decoder.decode(Response.self, from: result)
|
if let translation = response.translations.first {
|
||||||
if let translation = response.translations.first {
|
return .init(content: translation.text.removingPercentEncoding ?? "",
|
||||||
return .init(content: translation.text.removingPercentEncoding ?? "",
|
detectedSourceLanguage: translation.detectedSourceLanguage,
|
||||||
detectedSourceLanguage: translation.detectedSourceLanguage,
|
provider: "DeepL.com")
|
||||||
provider: "DeepL.com")
|
|
||||||
}
|
|
||||||
throw DeepLError.notFound
|
|
||||||
} catch {
|
|
||||||
throw error
|
|
||||||
}
|
}
|
||||||
|
throw DeepLError.notFound
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -161,7 +161,7 @@ extension StatusEditor {
|
||||||
|
|
||||||
private func translateDescription() async -> String? {
|
private func translateDescription() async -> String? {
|
||||||
isTranslating = true
|
isTranslating = true
|
||||||
let userAPIKey = DeepLUserAPIHandler.readIfAllowed()
|
let userAPIKey = DeepLUserAPIHandler.readKeyIfAllowed()
|
||||||
let userAPIFree = UserPreferences.shared.userDeeplAPIFree
|
let userAPIFree = UserPreferences.shared.userDeeplAPIFree
|
||||||
let deeplClient = DeepLClient(userAPIKey: userAPIKey, userAPIFree: userAPIFree)
|
let deeplClient = DeepLClient(userAPIKey: userAPIKey, userAPIFree: userAPIFree)
|
||||||
let lang = preferences.serverPreferences?.postLanguage ?? Locale.current.language.languageCode?.identifier
|
let lang = preferences.serverPreferences?.postLanguage ?? Locale.current.language.languageCode?.identifier
|
||||||
|
|
|
@ -5,6 +5,19 @@ import Foundation
|
||||||
import Models
|
import Models
|
||||||
import Network
|
import Network
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
#if canImport(_Translation_SwiftUI)
|
||||||
|
import Translation
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
func addTranslateView(isPresented: Binding<Bool>, text: String) -> some View {
|
||||||
|
if #available(iOS 17.4, *) {
|
||||||
|
return self.translationPresentation(isPresented: isPresented, text: text)
|
||||||
|
} else {
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
public struct StatusRowView: View {
|
public struct StatusRowView: View {
|
||||||
|
@ -15,6 +28,7 @@ public struct StatusRowView: View {
|
||||||
@Environment(\.accessibilityVoiceOverEnabled) private var accessibilityVoiceOverEnabled
|
@Environment(\.accessibilityVoiceOverEnabled) private var accessibilityVoiceOverEnabled
|
||||||
@Environment(\.isStatusFocused) private var isFocused
|
@Environment(\.isStatusFocused) private var isFocused
|
||||||
@Environment(\.indentationLevel) private var indentationLevel
|
@Environment(\.indentationLevel) private var indentationLevel
|
||||||
|
@Environment(RouterPath.self) private var routerPath: RouterPath
|
||||||
|
|
||||||
@Environment(QuickLook.self) private var quickLook
|
@Environment(QuickLook.self) private var quickLook
|
||||||
@Environment(Theme.self) private var theme
|
@Environment(Theme.self) private var theme
|
||||||
|
@ -219,6 +233,23 @@ public struct StatusRowView: View {
|
||||||
StatusDataControllerProvider.shared.dataController(for: viewModel.finalStatus,
|
StatusDataControllerProvider.shared.dataController(for: viewModel.finalStatus,
|
||||||
client: viewModel.client)
|
client: viewModel.client)
|
||||||
)
|
)
|
||||||
|
.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("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
|
||||||
|
routerPath.presentedSheet = .settings
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#if canImport(_Translation_SwiftUI)
|
||||||
|
.addTranslateView(isPresented: $viewModel.showAppleTranslation, text: viewModel.finalStatus.content.asRawText)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
|
|
|
@ -31,6 +31,18 @@ import SwiftUI
|
||||||
var translation: Translation?
|
var translation: Translation?
|
||||||
var isLoadingTranslation: Bool = false
|
var isLoadingTranslation: Bool = false
|
||||||
var showDeleteAlert: Bool = false
|
var showDeleteAlert: Bool = false
|
||||||
|
var showAppleTranslation = false
|
||||||
|
var preferredTranslationType = TranslationType.useServerIfPossible {
|
||||||
|
didSet {
|
||||||
|
if oldValue != preferredTranslationType {
|
||||||
|
translation = nil
|
||||||
|
showAppleTranslation = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var deeplTranslationError = false
|
||||||
|
var instanceTranslationError = false
|
||||||
|
|
||||||
private(set) var actionsAccountsFetched: Bool = false
|
private(set) var actionsAccountsFetched: Bool = false
|
||||||
var favoriters: [Account] = []
|
var favoriters: [Account] = []
|
||||||
|
@ -297,25 +309,43 @@ import SwiftUI
|
||||||
}
|
}
|
||||||
|
|
||||||
func translate(userLang: String) async {
|
func translate(userLang: String) async {
|
||||||
withAnimation {
|
updatePreferredTranslation()
|
||||||
isLoadingTranslation = true
|
if preferredTranslationType == .useApple {
|
||||||
}
|
showAppleTranslation = true
|
||||||
if !alwaysTranslateWithDeepl {
|
return
|
||||||
do {
|
|
||||||
// We first use instance translation API if available.
|
|
||||||
let translation: Translation = try await client.post(endpoint: Statuses.translate(id: finalStatus.id,
|
|
||||||
lang: userLang))
|
|
||||||
withAnimation {
|
|
||||||
self.translation = translation
|
|
||||||
isLoadingTranslation = false
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
} catch {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If not or fail we use Ice Cubes own DeepL client.
|
if preferredTranslationType != .useDeepl {
|
||||||
await translateWithDeepL(userLang: userLang)
|
await translateWithInstance(userLang: userLang)
|
||||||
|
|
||||||
|
if translation == nil {
|
||||||
|
await translateWithDeepL(userLang: userLang)
|
||||||
|
}
|
||||||
|
} 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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func translateWithDeepL(userLang: String) async {
|
func translateWithDeepL(userLang: String) async {
|
||||||
|
@ -332,6 +362,20 @@ import SwiftUI
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
private func getDeepLClient() -> DeepLClient {
|
||||||
let userAPIfree = UserPreferences.shared.userDeeplAPIFree
|
let userAPIfree = UserPreferences.shared.userDeeplAPIFree
|
||||||
|
|
||||||
|
@ -339,11 +383,17 @@ import SwiftUI
|
||||||
}
|
}
|
||||||
|
|
||||||
private var userAPIKey: String? {
|
private var userAPIKey: String? {
|
||||||
DeepLUserAPIHandler.readIfAllowed()
|
DeepLUserAPIHandler.readKey()
|
||||||
}
|
}
|
||||||
|
|
||||||
var alwaysTranslateWithDeepl: Bool {
|
func updatePreferredTranslation() {
|
||||||
DeepLUserAPIHandler.shouldAlwaysUseDeepl
|
if DeepLUserAPIHandler.shouldAlwaysUseDeepl {
|
||||||
|
preferredTranslationType = .useDeepl
|
||||||
|
} else if UserPreferences.shared.preferredTranslationType == .useApple {
|
||||||
|
preferredTranslationType = .useApple
|
||||||
|
} else {
|
||||||
|
preferredTranslationType = .useServerIfPossible
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchRemoteStatus() async -> Bool {
|
func fetchRemoteStatus() async -> Bool {
|
||||||
|
|
|
@ -35,7 +35,8 @@ struct StatusRowTranslateView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
@ViewBuilder
|
||||||
|
var translateButton: some View {
|
||||||
if !isInCaptureMode,
|
if !isInCaptureMode,
|
||||||
!isCompact,
|
!isCompact,
|
||||||
let userLang = preferences.serverPreferences?.postLanguage,
|
let userLang = preferences.serverPreferences?.postLanguage,
|
||||||
|
@ -54,8 +55,22 @@ struct StatusRowTranslateView: View {
|
||||||
}
|
}
|
||||||
.buttonStyle(.borderless)
|
.buttonStyle(.borderless)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let translation = viewModel.translation, !viewModel.isLoadingTranslation {
|
@ViewBuilder
|
||||||
|
var generalTranslateButton: some View {
|
||||||
|
translateButton
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
generalTranslateButton
|
||||||
|
.onChange(of: preferences.preferredTranslationType) { _, _ in
|
||||||
|
withAnimation {
|
||||||
|
_ = viewModel.updatePreferredTranslation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let translation = viewModel.translation, !viewModel.isLoadingTranslation, preferences.preferredTranslationType != .useApple {
|
||||||
GroupBox {
|
GroupBox {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text(translation.content.asSafeMarkdownAttributedString)
|
Text(translation.content.asSafeMarkdownAttributedString)
|
||||||
|
|
Loading…
Reference in a new issue