From 89bf5b7144bd752138831b5d5c7c5578a15f19fd Mon Sep 17 00:00:00 2001 From: Prince Yadav <66916296+prince-0408@users.noreply.github.com> Date: Thu, 2 Apr 2026 01:01:41 +0530 Subject: [PATCH] feat(keyboard): show closest word suggestion on invalid command #640 Add didYouMeanSuggestion(for:) helper in CommandVariables that progressively shortens the input word to find the closest prefix match in autocomplete_lexicon (minimum 2 chars). Update all 4 invalid command blocks in KeyboardViewController: - Return key: translate (uses wordToTranslate global) - Return key: conjugate (uses verbToConjugate global) - Translate key with selected text - Conjugate key with selected text - Plural key with selected text Each block shows 'Did you mean: X?' when a close match is found, falls back to existing invalidCommandMsg when no match exists. Existing alreadyPlural behavior is untouched. Closes #640 --- .../KeyboardViewController.swift | 26 ++++++++++++++++--- .../CommandVariables.swift | 22 ++++++++++++++++ 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/Keyboards/KeyboardsBase/KeyboardViewController.swift b/Keyboards/KeyboardsBase/KeyboardViewController.swift index 61dfee62..a2d2281c 100644 --- a/Keyboards/KeyboardsBase/KeyboardViewController.swift +++ b/Keyboards/KeyboardsBase/KeyboardViewController.swift @@ -2069,7 +2069,13 @@ class KeyboardViewController: UIInputViewController { autoCapAtStartOfProxy() if commandState == .invalid { - commandBar.text = commandPromptSpacing + invalidCommandMsgWikidata + // wordToTranslate is set by queryTranslation, verbToConjugate by triggerVerbConjugation. + let failedWord = wordToTranslate.isEmpty ? verbToConjugate : wordToTranslate + if let suggestion = didYouMeanSuggestion(for: failedWord) { + commandBar.text = commandPromptSpacing + suggestion + } else { + commandBar.text = commandPromptSpacing + invalidCommandMsgWikidata + } commandBar.isShowingInfoButton = true } else { commandBar.isShowingInfoButton = false @@ -2096,7 +2102,11 @@ class KeyboardViewController: UIInputViewController { loadKeys() proxy.insertText(selectedText) autoCapAtStartOfProxy() - commandBar.text = commandPromptSpacing + invalidCommandMsgWiktionary + if let suggestion = didYouMeanSuggestion(for: selectedText) { + commandBar.text = commandPromptSpacing + suggestion + } else { + commandBar.text = commandPromptSpacing + invalidCommandMsgWiktionary + } commandBar.isShowingInfoButton = true commandBar.textColor = keyCharColor return @@ -2130,7 +2140,11 @@ class KeyboardViewController: UIInputViewController { loadKeys() proxy.insertText(selectedText) autoCapAtStartOfProxy() - commandBar.text = commandPromptSpacing + invalidCommandMsgWikidata + if let suggestion = didYouMeanSuggestion(for: selectedText) { + commandBar.text = commandPromptSpacing + suggestion + } else { + commandBar.text = commandPromptSpacing + invalidCommandMsgWikidata + } commandBar.isShowingInfoButton = true commandBar.textColor = keyCharColor return @@ -2152,7 +2166,11 @@ class KeyboardViewController: UIInputViewController { if commandState == .invalid { proxy.insertText(selectedText) - commandBar.text = commandPromptSpacing + invalidCommandMsgWikidata + if let suggestion = didYouMeanSuggestion(for: selectedText) { + commandBar.text = commandPromptSpacing + suggestion + } else { + commandBar.text = commandPromptSpacing + invalidCommandMsgWikidata + } commandBar.isShowingInfoButton = true } else { commandBar.isShowingInfoButton = false diff --git a/Keyboards/KeyboardsBase/ScribeFunctionality/CommandVariables.swift b/Keyboards/KeyboardsBase/ScribeFunctionality/CommandVariables.swift index 43778e4f..6de2e0e4 100644 --- a/Keyboards/KeyboardsBase/ScribeFunctionality/CommandVariables.swift +++ b/Keyboards/KeyboardsBase/ScribeFunctionality/CommandVariables.swift @@ -182,3 +182,25 @@ var alreadyPluralMsg = "" /// The message shown on the download data button when no language data has been downloaded. var downloadDataMsg = "Please download language data" + +/// Returns a "Did you mean: X?" suggestion string for an invalid command input. +/// Progressively shortens the word to find the closest prefix match in the autocomplete lexicon. +/// +/// - Parameter word: the word that was not found. +/// - Returns: a suggestion string, or nil if no close match exists. +func didYouMeanSuggestion(for word: String) -> String? { + guard !word.isEmpty else { return nil } + + // Try progressively shorter prefixes (minimum 2 chars) to find a close match. + var prefix = word.lowercased() + while prefix.count >= 2 { + let suggestions = LanguageDBManager.shared.queryAutocompletions(word: prefix) + if let first = suggestions.first, !first.isEmpty { + // Only suggest if it's actually different from what was typed. + guard first.lowercased() != word.lowercased() else { return nil } + return "Did you mean: \(first)?" + } + prefix = String(prefix.dropLast()) + } + return nil +}