-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Expand file tree
/
Copy pathTranslationViewModel.swift
More file actions
133 lines (115 loc) · 4.86 KB
/
Copy pathTranslationViewModel.swift
File metadata and controls
133 lines (115 loc) · 4.86 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
import Foundation
import SwiftUI
import WebKit
import Translation
import NaturalLanguage
import Combine
import WordPressShared
@available(iOS 26, *)
@MainActor
public final class TranslationViewModel: ObservableObject {
@Published var configuration: TranslationSession.Configuration?
private var content: [String] = []
private var continuation: CheckedContinuation<[String], Error>?
public init() {}
public func translate(_ content: String, to targetLanguage: Locale.Language) async throws -> String {
let content = try await translate([content], to: targetLanguage)
guard let first = content.first else {
throw URLError(.unknown) // Should never happen
}
return first
}
/// Translate content to the specified target language.
///
/// This method detects the source language automatically and translates each string
/// in the content array independently.
public func translate(
_ content: [String],
from source: Locale.Language? = nil,
to target: Locale.Language = Locale.current.language
) async throws -> [String] {
wpAssert(continuation == nil, "Translation in progress")
self.content = content
return try await withCheckedThrowingContinuation { continuation in
self.continuation = continuation
// This will trigger the .translationTask in TranslationHostView
if self.configuration != nil {
// Yes, this is how you restart translation with the existing configuration
// in the Translation framework.
self.configuration?.invalidate()
} else {
self.configuration = TranslationSession.Configuration(source: source, target: target)
}
}
}
/// Check if translation is available for the given content.
public func checkAvailability(for content: String, to targetLanguage: Locale.Language = Locale.current.language) async -> TranslationAvailability {
// Important. The `Translation` framework is effective at translating
// HTML, but the `status(...)` method and `NLLanguageRecognizer`
// incorrectly identify dominant language as English if a post has a
// significant amount of HTML tags and/or CSS styles.
let content = (try? ContentExtractor.extractRelevantText(from: content)) ?? content
guard let identifier = IntelligenceService.detectLanguage(from: content) else {
return .unavailable
}
let sourceLanguage = Locale.Language(identifier: identifier)
let availability = LanguageAvailability()
let status = await availability.status(from: sourceLanguage, to: targetLanguage)
guard status == .installed || status == .supported else {
return .unavailable
}
return .available(sourceLanguage: sourceLanguage, targetLanguage: targetLanguage)
}
fileprivate func performTranslation(session: TranslationSession) async {
do {
var output: [String] = []
for string in content {
try Task.checkCancellation()
let result = try await session.translate(string)
output.append(result.targetText)
}
finish(with: .success(output))
} catch {
if (error as NSError).domain == NSCocoaErrorDomain && (error as NSError).code == NSUserCancelledError {
finish(with: .failure(CancellationError()))
} else {
finish(with: .failure(error))
}
}
}
private func finish(with result: Result<[String], Error>) {
content = []
if let continuation {
self.continuation = nil
continuation.resume(with: result)
}
}
}
public enum TranslationAvailability {
case unavailable
case available(sourceLanguage: Locale.Language, targetLanguage: Locale.Language)
}
// MARK: - TranslationHostView (SwiftUI)
/// SwiftUI view that hosts translation functionality using .translationTask()
///
/// This view manages the translation session lifecycle. It observes the view model's
/// configuration and triggers translation when it changes.
///
/// **IMPORTANT**: The `session` object must NEVER leave the `.translationTask` closure.
/// Capturing or storing the session causes crashes.
@available(iOS 26, *)
public struct TranslationHostView: View {
@ObservedObject var viewModel: TranslationViewModel
public init(viewModel: TranslationViewModel) {
self.viewModel = viewModel
}
public var body: some View {
Color.clear
.frame(width: 0, height: 0)
.translationTask(viewModel.configuration) { session in
await viewModel.performTranslation(session: session)
}
}
}
@available(iOS 18.0, *)
extension TranslationSession: @retroactive @unchecked(Sendable) {}