feat(translation): add translation button, works with offline downloaded languages
This commit is contained in:
34
WorterBuch/TranslationService.swift
Normal file
34
WorterBuch/TranslationService.swift
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
//
|
||||||
|
// TranslationService.swift
|
||||||
|
// WorterBuch
|
||||||
|
//
|
||||||
|
// Created by Claude on 07.12.2025.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Translation
|
||||||
|
|
||||||
|
@available(iOS 18.0, *)
|
||||||
|
class TranslationService {
|
||||||
|
static func translate(text: String, from sourceLanguage: String = "de", to targetLanguage: String = "en") async -> String? {
|
||||||
|
do {
|
||||||
|
let sourceLocale = Locale.Language(identifier: sourceLanguage)
|
||||||
|
let targetLocale = Locale.Language(identifier: targetLanguage)
|
||||||
|
|
||||||
|
print("Attempting translation from \(sourceLanguage) to \(targetLanguage)")
|
||||||
|
|
||||||
|
// Create session - requires language pack to be installed
|
||||||
|
let session = TranslationSession(installedSource: sourceLocale, target: targetLocale)
|
||||||
|
|
||||||
|
let response = try await session.translate(text)
|
||||||
|
return response.targetText
|
||||||
|
} catch {
|
||||||
|
print("Translation error: \(error)")
|
||||||
|
print("To enable translation:")
|
||||||
|
print("1. Open the Translate app on your iPad")
|
||||||
|
print("2. Download German language pack")
|
||||||
|
print("3. Try the translate button again")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,8 @@ struct VocabularyFieldCell: View {
|
|||||||
let drawing: PKDrawing
|
let drawing: PKDrawing
|
||||||
let text: String
|
let text: String
|
||||||
let onTap: () -> Void
|
let onTap: () -> Void
|
||||||
|
var showTranslateButton: Bool = false
|
||||||
|
var onTranslate: (() -> Void)? = nil
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
@@ -41,6 +43,7 @@ struct VocabularyFieldCell: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Transcribed text - selectable but not editable
|
// Transcribed text - selectable but not editable
|
||||||
|
HStack(spacing: 8) {
|
||||||
if !text.isEmpty {
|
if !text.isEmpty {
|
||||||
SelectableTextView(
|
SelectableTextView(
|
||||||
text: text.lowercased(),
|
text: text.lowercased(),
|
||||||
@@ -55,6 +58,16 @@ struct VocabularyFieldCell: View {
|
|||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.frame(height: 40)
|
.frame(height: 40)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if showTranslateButton, let onTranslate = onTranslate {
|
||||||
|
Button(action: onTranslate) {
|
||||||
|
Image(systemName: "arrow.right.arrow.left.circle.fill")
|
||||||
|
.foregroundColor(.blue)
|
||||||
|
.font(.system(size: 20))
|
||||||
|
}
|
||||||
|
.frame(width: 40, height: 40)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.padding(8)
|
.padding(8)
|
||||||
.background(Color(.systemBackground))
|
.background(Color(.systemBackground))
|
||||||
|
|||||||
@@ -86,7 +86,8 @@ struct VocabularyGridView: View {
|
|||||||
entry: entry,
|
entry: entry,
|
||||||
onSelectField: { fieldType in
|
onSelectField: { fieldType in
|
||||||
openFieldEditor(for: entry, fieldType: fieldType)
|
openFieldEditor(for: entry, fieldType: fieldType)
|
||||||
}
|
},
|
||||||
|
onTranslate: translateEntry
|
||||||
)
|
)
|
||||||
.listRowInsets(EdgeInsets(top: 6, leading: 12, bottom: 6, trailing: 12))
|
.listRowInsets(EdgeInsets(top: 6, leading: 12, bottom: 6, trailing: 12))
|
||||||
.listRowSeparator(.hidden)
|
.listRowSeparator(.hidden)
|
||||||
@@ -211,11 +212,35 @@ struct VocabularyGridView: View {
|
|||||||
print("Error saving context: \(nsError), \(nsError.userInfo)")
|
print("Error saving context: \(nsError), \(nsError.userInfo)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func translateEntry(_ entry: VocabularyEntry) {
|
||||||
|
guard let germanText = entry.germanWordText, !germanText.isEmpty else { return }
|
||||||
|
guard entry.englishTranslationText?.isEmpty ?? true else { return }
|
||||||
|
|
||||||
|
Task {
|
||||||
|
if #available(iOS 18.0, *) {
|
||||||
|
if let translation = await TranslationService.translate(text: germanText) {
|
||||||
|
await MainActor.run {
|
||||||
|
entry.englishTranslationText = translation
|
||||||
|
saveContext()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
print("Translation requires iOS 18.0 or later")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct VocabularyEntryRow: View {
|
struct VocabularyEntryRow: View {
|
||||||
@ObservedObject var entry: VocabularyEntry
|
@ObservedObject var entry: VocabularyEntry
|
||||||
let onSelectField: (FieldType) -> Void
|
let onSelectField: (FieldType) -> Void
|
||||||
|
let onTranslate: (VocabularyEntry) -> Void
|
||||||
|
|
||||||
|
var shouldShowTranslateButton: Bool {
|
||||||
|
!(entry.germanWordText?.isEmpty ?? true) &&
|
||||||
|
(entry.englishTranslationText?.isEmpty ?? true)
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
@@ -239,7 +264,9 @@ struct VocabularyEntryRow: View {
|
|||||||
VocabularyFieldCell(
|
VocabularyFieldCell(
|
||||||
drawing: entry.englishTranslationPKDrawing,
|
drawing: entry.englishTranslationPKDrawing,
|
||||||
text: entry.englishTranslationText ?? "",
|
text: entry.englishTranslationText ?? "",
|
||||||
onTap: { onSelectField(.englishTranslation) }
|
onTap: { onSelectField(.englishTranslation) },
|
||||||
|
showTranslateButton: shouldShowTranslateButton,
|
||||||
|
onTranslate: { onTranslate(entry) }
|
||||||
)
|
)
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user