import AppKit import Combine import Foundation @MainActor final class LanguageTranslatorViewModel: ObservableObject { static let maxWords = 10_000 @Published var sourceText: String = "" @Published var translatedText: String = "" @Published var sourceLanguage: TranslationLanguage = .defaultSource @Published var targetLanguage: TranslationLanguage = .defaultTarget @Published private(set) var isTranslating = false @Published private(set) var errorMessage: String? private let translationService: any TranslationServicing private let historyStorage: HistoryStoring private let textExtractionService: any TextExtracting init( translationService: any TranslationServicing = TranslationService(), historyStorage: HistoryStoring = HistoryStorageService.shared, textExtractionService: any TextExtracting = TextExtractionService() ) { self.translationService = translationService self.historyStorage = historyStorage self.textExtractionService = textExtractionService } var wordCountLimitLabel: String { "\(sourceText.wordCount)/\(Self.maxWords)" } var canTranslate: Bool { !sourceText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !isTranslating } var sourceLanguageOptions: [TranslationLanguage] { TranslationLanguage.all } var targetLanguageOptions: [TranslationLanguage] { TranslationLanguage.all.filter { $0 != sourceLanguage } } func pasteText() { guard let clipboard = NSPasteboard.general.string(forType: .string) else { return } sourceText = clipboard.truncated(toMaxWords: Self.maxWords) translatedText = "" errorMessage = nil } func uploadFile() { Task { do { guard let extracted = try await textExtractionService.pickAndExtractText() else { return } sourceText = extracted.truncated(toMaxWords: Self.maxWords) translatedText = "" errorMessage = nil } catch { errorMessage = error.localizedDescription } } } func clearText() { sourceText = "" translatedText = "" errorMessage = nil } func swapLanguages() { let previousSource = sourceLanguage sourceLanguage = targetLanguage targetLanguage = previousSource let previousSourceText = sourceText sourceText = translatedText translatedText = previousSourceText } func translate() { let trimmed = sourceText.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { return } isTranslating = true translatedText = "" errorMessage = nil Task { do { translatedText = try await translationService.translate( trimmed, from: sourceLanguage, to: targetLanguage ) saveToHistory(sourceText: trimmed, translatedText: translatedText) } catch { errorMessage = error.localizedDescription } isTranslating = false } } func restore(from entry: HistoryEntry) { guard case .languageTranslator(let data) = entry.payload else { return } sourceText = data.sourceText translatedText = data.translatedText sourceLanguage = TranslationLanguage.all.first { $0.id == data.sourceLanguageID } ?? .defaultSource targetLanguage = TranslationLanguage.all.first { $0.id == data.targetLanguageID } ?? .defaultTarget errorMessage = nil } private func saveToHistory(sourceText: String, translatedText: String) { let payload = HistoryPayload.languageTranslator( TranslationHistoryData( sourceText: sourceText, translatedText: translatedText, sourceLanguageID: sourceLanguage.id, targetLanguageID: targetLanguage.id ) ) let entry = HistoryEntry( tool: .languageTranslator, title: HistoryHelpers.title(from: sourceText, fallback: "Translation"), preview: HistoryHelpers.preview(from: translatedText), payload: payload ) historyStorage.save(entry) } func syncTargetLanguageIfNeeded() { guard targetLanguage == sourceLanguage else { return } targetLanguage = TranslationLanguage.all.first { $0 != sourceLanguage } ?? .defaultTarget } }