From 9613cbb9944d5620c4da9c392ec572ee52a84cb7 Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Sun, 18 Oct 2026 10:08:33 +0200 Subject: [PATCH] feat: swap translation languages with Return on empty input --- README.md | 4 ++-- Sources/Conjure/TranslationController.swift | 11 ++++++++-- .../TranslationExecutionTests.swift | 20 +++++++++++++++++++ 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a1d26cc..7c835d3 100644 --- a/README.md +++ b/README.md @@ -136,9 +136,9 @@ Notes autosave locally as RTFD packages, including embedded images, under `~/Lib ## Translation -Search for **Conjure Translate**, identified by its centered 🌏 icon, and open it. Choose a provider in the pill-shaped header selector, enter text in the search bar, then press Return or click **Translate**. Typing alone never sends text. When the translation is available, the action changes to **Copy**: press Return or click it to copy the full translation and close Conjure. Command-C still copies selected output. Editing the input restores **Translate**. +Search for **Conjure Translate**, identified by its centered 🌏 icon, and open it. With empty or whitespace-only input, Return or **Swap languages** reverses the source and target without submitting a request. Enter text to change the action to **Translate**; press Return or click it to submit. Typing alone never sends text. When the translation is available, the action changes to **Copy**: press Return or click it to copy the full translation and close Conjure. Command-C still copies selected output. Editing the input clears the result and restores **Translate**, or **Swap languages** if empty. The action is disabled while translating. -The footer places **Translate/Copy** and its Return keycap before **Actions**, whose Command and K keys use separate keycaps, matching the launcher. +The footer places the current primary action and its Return keycap before **Actions**, whose Command and K keys use separate keycaps, matching the launcher. Press Command-P or click the provider selector to expand a searchable action-style popup from its top edge downwards, hiding the indicator until the popup closes. A checkmark marks the current provider. Use Up/Down and Return or click a provider to select it; Escape, Back, or clicking outside closes the popup. Changing providers clears the previous result without submitting a request. diff --git a/Sources/Conjure/TranslationController.swift b/Sources/Conjure/TranslationController.swift index 80a2b98..d1da995 100644 --- a/Sources/Conjure/TranslationController.swift +++ b/Sources/Conjure/TranslationController.swift @@ -239,8 +239,11 @@ final class TranslationController: NSViewController { func inputChanged() { cancelRequest() output.string = "" - translateButton.title = "Translate" - translateButton.toolTip = "Translate the input (Return). Escape dismisses Conjure." + let isEmpty = (input?() ?? "").trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + translateButton.title = isEmpty ? "Swap languages" : "Translate" + translateButton.toolTip = isEmpty + ? "Swap source and target languages (Return)." + : "Translate the input (Return). Escape dismisses Conjure." settingsButton.isHidden = true languageButton.title = "\(languageName(sourceLanguage)) → \(languageName(targetLanguage))" output.setAccessibilityLabel("\(languageName(targetLanguage)) translation") @@ -269,6 +272,10 @@ final class TranslationController: NSViewController { @objc func performPrimaryAction() { guard task == nil else { return } + if (input?() ?? "").trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + swapLanguages() + return + } guard !output.string.isEmpty else { translate(); return } pasteboard.clearContents() if pasteboard.setString(output.string, forType: .string) { diff --git a/Tests/ConjureKitTests/TranslationExecutionTests.swift b/Tests/ConjureKitTests/TranslationExecutionTests.swift index 202c3cb..1ede6f7 100644 --- a/Tests/ConjureKitTests/TranslationExecutionTests.swift +++ b/Tests/ConjureKitTests/TranslationExecutionTests.swift @@ -58,6 +58,26 @@ private func descendants(_ view: NSView) -> [NSView] { [view] + view.subviews.fl @Suite(.serialized) struct TranslationExecutionTests { +@Test @MainActor +func emptyPrimaryActionSwapsLanguagesWithoutSubmitting() { + _ = NSApplication.shared + let suite = "TranslateEmpty.\(UUID().uuidString)" + let defaults = UserDefaults(suiteName: suite)! + defer { defaults.removePersistentDomain(forName: suite) } + let tool = TranslationController(defaults: defaults, + myMemory: { _, _, _ in Issue.record("Empty input must not submit"); return "unexpected" }) + tool.input = { " \n\t" } + _ = tool.view + let button = descendants(tool.view).compactMap { $0 as? NSButton } + .first { $0.action == NSSelectorFromString("performPrimaryAction") }! + button.performClick(nil) + #expect(tool.sourceLanguage == "ja") + #expect(tool.targetLanguage == "en") + tool.performPrimaryAction() + #expect(tool.sourceLanguage == "en") + #expect(tool.targetLanguage == "ja") +} + @Test @MainActor func languageActionsInvalidateOldResultsAndApplyTheSelectedPair() async throws { _ = NSApplication.shared