feat: add selectable multilingual spelling dictionaries#657
Conversation
| VStack(alignment: .leading, spacing: 8) { | ||
| ForEach(SpellingDictionaryLanguage.allCases) { language in | ||
| Toggle( | ||
| language.settingsLabel, | ||
| isOn: Binding( | ||
| get: { | ||
| suggestionSettings.isSpellingDictionaryEnabled(language) | ||
| }, | ||
| set: { | ||
| suggestionSettings.setSpellingDictionary( | ||
| language, | ||
| enabled: $0 | ||
| ) | ||
| } | ||
| ) | ||
| ) | ||
| .toggleStyle(.checkbox) | ||
| } | ||
| } |
There was a problem hiding this comment.
The dictionary picker is always interactive regardless of whether typo corrections are actually being offered.
SymSpellCorrector is only consulted when offerTypoCorrections is true (gated via TypoGate in handleTypoGate), so a user who has "Offer Corrections on Typo" turned off can tick every dictionary checkbox with no observable effect. A .disabled(!suggestionSettings.offerTypoCorrections) modifier on the VStack makes this dependency explicit in the UI, matching how the "Offer Corrections on Typo" toggle itself is already disabled when its own parent toggle is off.
| VStack(alignment: .leading, spacing: 8) { | |
| ForEach(SpellingDictionaryLanguage.allCases) { language in | |
| Toggle( | |
| language.settingsLabel, | |
| isOn: Binding( | |
| get: { | |
| suggestionSettings.isSpellingDictionaryEnabled(language) | |
| }, | |
| set: { | |
| suggestionSettings.setSpellingDictionary( | |
| language, | |
| enabled: $0 | |
| ) | |
| } | |
| ) | |
| ) | |
| .toggleStyle(.checkbox) | |
| } | |
| } | |
| VStack(alignment: .leading, spacing: 8) { | |
| ForEach(SpellingDictionaryLanguage.allCases) { language in | |
| Toggle( | |
| language.settingsLabel, | |
| isOn: Binding( | |
| get: { | |
| suggestionSettings.isSpellingDictionaryEnabled(language) | |
| }, | |
| set: { | |
| suggestionSettings.setSpellingDictionary( | |
| language, | |
| enabled: $0 | |
| ) | |
| } | |
| ) | |
| ) | |
| .toggleStyle(.checkbox) | |
| } | |
| } | |
| .disabled(!suggestionSettings.offerTypoCorrections) |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| switch self { | ||
| case .english: return "frequency_dictionary_en_82_765" | ||
| case .german: return "de-100k" | ||
| case .spanish: return "es-100l" |
There was a problem hiding this comment.
The Spanish resource name
es-100l (letter l) diverges from the *-100k pattern every other language follows (de-100k, fr-100k, he-100k, it-100k, ru-100k). The actual file on disk is also es-100l.txt, so this works correctly at runtime, but anyone expecting es-100k when browsing the bundle or the resource directory will look for the wrong file. A comment on the case would prevent future confusion.
| case .spanish: return "es-100l" | |
| // Upstream SymSpell file uses "l" (not "k") — matches the filename es-100l.txt in the bundle. | |
| case .spanish: return "es-100l" |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Summary
Validation
swiftlint lint --config .swiftlint.yml --quietxcodebuild ... build-for-testing ... CODE_SIGNING_ALLOWED=NOScreenshotContextGeneratorTests.test_generateContext_allNoiseOCRReturnsUnavailable; an independent main-worktree run was stalled at the same test.Linked issues
None.
Risk / rollout notes
Greptile Summary
This PR bundles six new SymSpell frequency dictionaries (German, Spanish, French, Hebrew, Italian, Russian) alongside their license files, and adds a settings UI for enabling them. A new
SpellingLanguageResolveruses Apple'sNLLanguageRecognizerto pick the right index from surrounding text when multiple dictionaries are enabled;SymSpellCorrectorloads indexes lazily and evicts the least-recently-used one when a two-entry cache limit is reached.SpellingLanguageResolver,SymSpellCorrector) is clean and well-tested: lazy loading, LRU eviction, per-language cache isolation, and NSSpellChecker fallback all work correctly.SpellingDictionaryCatalog,SuggestionSettingsStore,SuggestionSettingsModel) correctly normalizes, persists, and migrates the newenabledSpellingDictionaryCodespreference alongside the existing typo-correction toggles.es-100l(letter ℓ) while every other dictionary follows the*-100kconvention — the mismatch is inherited from upstream but stands out to maintainers reading the bundle.Confidence Score: 4/5
Safe to merge; all new language-routing, lazy-loading, and LRU-eviction logic is correct and well-tested.
The core SymSpell cache, language resolver, settings persistence, and NSSpellChecker fallback are all implemented correctly. The dictionary picker is always interactive even when the parent "Offer Corrections on Typo" toggle is off, which can mislead users into enabling dictionaries that have no runtime effect. The Spanish resource name inconsistency is inherited from upstream but worth a one-line comment.
Cotabby/UI/SpellingDictionaryPicker.swift — picker interactivity should be gated on offerTypoCorrections; Cotabby/Models/SpellingDictionaryCatalog.swift — Spanish resource name anomaly.
Important Files Changed
es-100ldiverges from the*-100knaming pattern used by every other languagebestCorrectionrouting and NSSpellChecker fallback logic are clean and correctly integrated with TypoGatespellingDictionaryCodesDefaultsKeykey andsaveEnabledSpellingDictionaryCodesare correctly wired; normalize-on-save is slightly redundant but harmlessSequence Diagram
sequenceDiagram participant SC as SuggestionCoordinator participant TG as TypoGate participant SLR as SpellingLanguageResolver participant SSC as SymSpellCorrector participant NSS as NSSpellChecker SC->>TG: handleTypoGate(rawContext, workID) TG->>SC: bestCorrection(word, precedingText) SC->>SLR: resolve(precedingText, word, enabledLanguages) alt single language enabled SLR-->>SC: language (explicit selection) else multiple languages, confident NL detection SLR-->>SC: language (from NLLanguageRecognizer) else ambiguous / empty SLR-->>SC: nil SC->>NSS: bestCorrection(word) NSS-->>SC: correction or nil end alt language resolved SC->>SSC: bestCorrection(word, language) alt index cached SSC-->>SC: correction or nil else index loading / not ready SSC-->>SC: nil (triggers background load) SC->>NSS: bestCorrection(word) NSS-->>SC: correction or nil end end TG->>SC: .correct(word, correctedWord) or .suppress or .proceedReviews (1): Last reviewed commit: "feat: add multilingual spelling dictiona..." | Re-trigger Greptile