This commit is contained in:
2024-09-28 13:06:35 -04:00
parent e2cf6ffab2
commit f28b15e84a
8 changed files with 237 additions and 11 deletions

View File

@@ -33,6 +33,8 @@
3ACB685F297633BC00C46468 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3ACB685D297633BC00C46468 /* Localizable.strings */; };
3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */; };
3AE45AF6297BB2E700C1D842 /* LibreTranslateServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */; };
3AEEB1FD2CAA3F7B004653F8 /* OfflineTranslateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AEEB1FC2CAA3F7B004653F8 /* OfflineTranslateView.swift */; };
3AEEB1FE2CAA3F7B004653F8 /* OfflineTranslateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AEEB1FC2CAA3F7B004653F8 /* OfflineTranslateView.swift */; };
3AF9B5A82CA0B5CF0021A08E /* LanguageSortComparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF9B5A72CA0B5CF0021A08E /* LanguageSortComparator.swift */; };
3AF9B5A92CA0B5CF0021A08E /* LanguageSortComparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF9B5A72CA0B5CF0021A08E /* LanguageSortComparator.swift */; };
3AF9B5AB2CA0D2D90021A08E /* AppleTranslationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF9B5AA2CA0D2D90021A08E /* AppleTranslationSettingsView.swift */; };
@@ -1333,6 +1335,7 @@
3AEB8003297CCEA800713A25 /* tr-TR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "tr-TR"; path = "tr-TR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3AEB8004297CCEA800713A25 /* tr-TR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "tr-TR"; path = "tr-TR.lproj/Localizable.strings"; sourceTree = "<group>"; };
3AEB8005297CCEA900713A25 /* tr-TR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "tr-TR"; path = "tr-TR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3AEEB1FC2CAA3F7B004653F8 /* OfflineTranslateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfflineTranslateView.swift; sourceTree = "<group>"; };
3AF6336829884C6B0005672A /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3AF6336929884C6B0005672A /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/Localizable.strings"; sourceTree = "<group>"; };
3AF6336A29884C6B0005672A /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "pt-PT"; path = "pt-PT.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
@@ -3015,6 +3018,7 @@
4C28595F2A12A2BE004746F7 /* SupporterBadge.swift */,
5C6E1DAC2A193EC2008FC15A /* GradientButtonStyle.swift */,
5CC868DC2AA29B3200FB22BA /* NeutralButtonStyle.swift */,
3AEEB1FC2CAA3F7B004653F8 /* OfflineTranslateView.swift */,
);
path = Components;
sourceTree = "<group>";
@@ -3858,6 +3862,7 @@
4CB8838F296F781C00DC99E7 /* ReactionsView.swift in Sources */,
B5C60C202B530D5100C5ECA7 /* MuteItem.swift in Sources */,
4C75EFB328049D640006080F /* NostrEvent.swift in Sources */,
3AEEB1FE2CAA3F7B004653F8 /* OfflineTranslateView.swift in Sources */,
4C32B9582A9AD44700DC3548 /* VeriferOptions.swift in Sources */,
D74AAFC22B153395006CF0F4 /* HeadlessDamusState.swift in Sources */,
4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */,
@@ -4541,6 +4546,7 @@
D73E5F2B2C6A97F4007EB227 /* ReplyPart.swift in Sources */,
D73E5F2C2C6A97F4007EB227 /* ProxyView.swift in Sources */,
D73E5F2D2C6A97F4007EB227 /* SelectedEventView.swift in Sources */,
3AEEB1FD2CAA3F7B004653F8 /* OfflineTranslateView.swift in Sources */,
D73E5F2E2C6A97F4007EB227 /* EventBody.swift in Sources */,
D73E5F302C6A97F4007EB227 /* EventProfile.swift in Sources */,
D73E5F312C6A97F4007EB227 /* EventMenu.swift in Sources */,

View File

@@ -0,0 +1,168 @@
//
// OfflineTranslateView.swift
// damus
//
// Created by Terry Yiu on 9/29/24.
//
import SwiftUI
import SwiftUI
import NaturalLanguage
import Translation
fileprivate let MIN_UNIQUE_CHARS = 2
@available(iOS 18.0, macOS 15.0, *)
@available(macCatalyst, unavailable)
struct OfflineTranslateView: View {
let damus_state: DamusState
let event: NostrEvent
let size: EventViewKind
@ObservedObject var translations_model: TranslationModel
@State private var translationConfiguration: TranslationSession.Configuration?
// @State private var languageStatus: LanguageAvailability.Status?
init(damus_state: DamusState, event: NostrEvent, size: EventViewKind) {
self.damus_state = damus_state
self.event = event
self.size = size
self._translations_model = ObservedObject(wrappedValue: damus_state.events.get_cache_data(event.id).translations_model)
}
var TranslateButton: some View {
Button(NSLocalizedString("Translate Note", comment: "Button to translate note from different language.")) {
translate()
}
.translate_button_style()
}
func TranslatedView(lang: String?, artifacts: NoteArtifactsSeparated, font_size: Double) -> some View {
return VStack(alignment: .leading) {
let translatedFromLanguageString = String(format: NSLocalizedString("Translated from %@", comment: "Button to indicate that the note has been translated from a different language."), lang ?? "ja")
Text(translatedFromLanguageString)
.foregroundColor(.gray)
.font(.footnote)
.padding([.top, .bottom], 10)
if self.size == .selected {
SelectableText(damus_state: damus_state, event: event, attributedString: artifacts.content.attributed, size: self.size)
} else {
artifacts.content.text
.font(eventviewsize_to_font(self.size, font_size: font_size))
}
}
}
func translate() {
guard /*let languageStatus, */translations_model.state == .havent_tried && damus_state.settings.translation_service == .none && damus_state.settings.translate_offline/* && languageStatus != .unsupported*/, let note_language = translations_model.note_language else {
return
}
guard translationConfiguration == nil else {
translationConfiguration?.invalidate()
return
}
translationConfiguration = TranslationSession.Configuration(
source: Locale.Language(identifier: note_language))
}
// func setLanguageStatus() async {
// guard languageStatus == nil else {
// return
// }
//
// guard let note_language = translations_model.note_language else {
// languageStatus = .unsupported
// return
// }
//
// let languageAvailability = LanguageAvailability()
// let language = Locale.Language(identifier: note_language)
// languageStatus = await languageAvailability.status(from: language, to: nil)
// }
var body: some View {
if let note_lang = translations_model.note_language, damus_state.settings.translation_service == .none && damus_state.settings.translate_offline && should_translate(event: event, our_keypair: damus_state.keypair, note_lang: note_lang) {
Group {
switch self.translations_model.state {
case .havent_tried:
if damus_state.settings.auto_translate/* && languageStatus == .installed*/ {
Text("")
} else {
TranslateButton
}
case .translating:
Text("")
case .translated(let translated):
let languageName = Locale.current.localizedString(forLanguageCode: translated.language)
TranslatedView(lang: languageName, artifacts: translated.artifacts, font_size: damus_state.settings.font_size)
case .not_needed:
Text("")
}
}
.onAppear {
// Task { @MainActor in
// await setLanguageStatus()
// }
translate()
}
.translationTask(translationConfiguration) { translationSession in
Task { @MainActor in
do {
guard let note_language = translations_model.note_language, translations_model.state == .havent_tried/*, languageStatus != .unsupported*/ else {
return
}
translations_model.state = .translating
let originalContent = event.get_content(damus_state.keypair)
let response = try await translationSession.translate(originalContent)
let translated_note = response.targetText
guard originalContent != translated_note else {
// if its the same, give up and don't retry
translations_model.state = .not_needed
return
}
guard translationMeetsStringDistanceRequirements(original: originalContent, translated: translated_note) else {
translations_model.state = .not_needed
return
}
// Render translated note
let translated_blocks = parse_note_content(content: .content(translated_note, event.tags))
let artifacts = render_blocks(blocks: translated_blocks, profiles: damus_state.profiles)
// and cache it
translations_model.state = .translated(Translated(artifacts: artifacts, language: note_language))
} catch {
// code to handle error
print("Error translating note: \(error.localizedDescription)")
translations_model.state = .not_needed
}
}
}
} else {
Text("")
}
}
func translationMeetsStringDistanceRequirements(original: String, translated: String) -> Bool {
return levenshteinDistanceIsGreaterThanOrEqualTo(from: original, to: translated, threshold: MIN_UNIQUE_CHARS)
}
}
@available(iOS 18.0, macOS 15.0, *)
@available(macCatalyst, unavailable)
struct OfflineTranslateView_Previews: PreviewProvider {
static var previews: some View {
let ds = test_damus_state
OfflineTranslateView(damus_state: ds, event: test_note, size: .normal)
}
}

View File

@@ -85,7 +85,7 @@ struct TranslateView: View {
return false
}
if TranslationService.isAppleTranslationPopoverSupported {
if TranslationService.isAppleTranslationSupported {
return damus_state.settings.translation_service == .none || damus_state.settings.can_translate
} else {
return damus_state.settings.can_translate

View File

@@ -39,7 +39,7 @@ enum TranslationService: String, CaseIterable, Identifiable, StringCodable {
switch self {
case .none:
let displayName: String
if TranslationService.isAppleTranslationPopoverSupported {
if TranslationService.isAppleTranslationSupported {
displayName = NSLocalizedString("apple_translation_service", value: "Apple", comment: "Dropdown option for selecting Apple as a translation service.")
} else {
displayName = NSLocalizedString("none_translation_service", value: "None", comment: "Dropdown option for selecting no translation service.")
@@ -58,11 +58,15 @@ enum TranslationService: String, CaseIterable, Identifiable, StringCodable {
}
}
static var isAppleTranslationPopoverSupported: Bool {
static var isAppleTranslationSupported: Bool {
#if targetEnvironment(macCatalyst)
return false
#else
if #available(iOS 17.4, macOS 14.4, *) {
return true
} else {
return false
}
#endif
}
}

View File

@@ -180,6 +180,9 @@ class UserSettingsStore: ObservableObject {
@Setting(key: "auto_translate", default_value: true)
var auto_translate: Bool
@Setting(key: "translate_offline", default_value: true)
var translate_offline: Bool
@Setting(key: "show_general_statuses", default_value: true)
var show_general_statuses: Bool

View File

@@ -99,7 +99,17 @@ struct NoteContentView: View {
}
var translateView: some View {
#if targetEnvironment(macCatalyst)
TranslateView(damus_state: damus_state, event: event, size: self.size, isAppleTranslationPopoverPresented: $isAppleTranslationPopoverPresented)
#else
if #available(iOS 18.0, macOS 15.0, *), damus_state.settings.translate_offline {
AnyView(OfflineTranslateView(damus_state: damus_state, event: event, size: self.size))
} else {
AnyView(
TranslateView(damus_state: damus_state, event: event, size: self.size, isAppleTranslationPopoverPresented: $isAppleTranslationPopoverPresented)
)
}
#endif
}
func previewView(links: [URL]) -> some View {
@@ -148,7 +158,7 @@ struct NoteContentView: View {
}
}
if !options.contains(.no_translate) && (size == .selected || TranslationService.isAppleTranslationPopoverSupported || damus_state.settings.auto_translate) {
if !options.contains(.no_translate) && (size == .selected || TranslationService.isAppleTranslationSupported || damus_state.settings.auto_translate) {
if with_padding {
translateView
.padding(.horizontal)
@@ -301,9 +311,13 @@ struct NoteContentView: View {
Markdown(md.markdown)
.padding([.leading, .trailing, .top])
case .separated(let separated):
if #available(iOS 17.4, macOS 14.4, *) {
if #available(iOS 18.0, macOS 15.0, *), damus_state.settings.auto_translate {
MainContent(artifacts: separated)
} else if #available(iOS 17.4, macOS 14.4, *) {
MainContent(artifacts: separated)
#if !targetEnvironment(macCatalyst)
.translationPresentation(isPresented: $isAppleTranslationPopoverPresented, text: event.get_content(damus_state.keypair))
#endif
} else {
MainContent(artifacts: separated)
}

View File

@@ -8,7 +8,8 @@
import SwiftUI
import Translation
@available(iOS 18.0, *)
@available(iOS 18.0, macOS 15.0, *)
@available(macCatalyst, unavailable)
struct AppleTranslationSettingsView: View {
@ObservedObject var settings: UserSettingsStore

View File

@@ -12,7 +12,7 @@ struct TranslationSettingsView: View {
@ObservedObject var settings: UserSettingsStore
var damus_state: DamusState
@Environment(\.dismiss) var dismiss
@Environment(\.dismiss) var dismiss
var body: some View {
Form {
@@ -101,20 +101,50 @@ struct TranslationSettingsView: View {
Toggle(NSLocalizedString("Translate DMs", comment: "Toggle to translate direct messages."), isOn: $settings.translate_dms)
.toggleStyle(.switch)
*/
} else if #available(iOS 18.0, *) {
Toggle(NSLocalizedString("Automatically translate notes", comment: "Toggle to automatically translate notes."), isOn: $settings.auto_translate)
.toggleStyle(.switch)
}
}
if #available(iOS 18.0, *) {
#if !targetEnvironment(macCatalyst)
if #available(iOS 18.0, macOS 15.0, *), settings.translation_service == .none {
Toggle(NSLocalizedString("Automatically translate notes", comment: "Toggle to automatically translate notes."), isOn: $settings.auto_translate)
.toggleStyle(.switch)
Section (
content: {
Toggle(NSLocalizedString("On-Device Mode", comment: "Toggle to always translate offline using downloaded languages."), isOn: $settings.translate_offline)
}, footer: {
Text("Always translate offline using downloaded languages. Offline translations may not be as accurate as online translations. Apple may collect usage metrics, but this data does not include the original or translated content.", comment: "Section footer explaining the implications of enabling offline translations.")
}
)
AppleTranslationSettingsView(settings: settings)
}
#endif
}
.navigationTitle(NSLocalizedString("Translation", comment: "Navigation title for translation settings."))
.onReceive(handle_notify(.switched_timeline)) { _ in
dismiss()
}
.onChange(of: settings.auto_translate) { newValue in
// Apple automatic translation can occur only if offline translations are enabled.
if settings.translation_service == .none && newValue && !settings.translate_offline {
settings.translate_offline = true
}
}
.onChange(of: settings.translate_offline) { newValue in
// Apple automatic translation can occur only if offline translations are enabled.
if settings.translation_service == .none && !newValue && settings.auto_translate {
settings.auto_translate = false
}
}
.onChange(of: settings.translation_service) { newValue in
// Apple automatic translation can occur only if offline translations are enabled.
// If automatic translations are enabled for a non-Apple translation service,
// and then the translation service is switched to Apple, offline translations need to be enabled.
if newValue == .none && settings.auto_translate && !settings.translate_offline {
settings.translate_offline = true
}
}
}
}