Skip to content

Commit 48a5b4d

Browse files
committed
Make locale failures and discovery first-class
Replace the unit Unavailable error with variants apps can match, fail closed on unknown locales, and expose locale() / available_locales(). Add examples and a README that states Spellkit wraps system backends instead of bundling dictionaries.
1 parent f5617ec commit 48a5b4d

13 files changed

Lines changed: 495 additions & 89 deletions

File tree

.github/workflows/ci.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ jobs:
3434
sudo apt-get install -y \
3535
libhunspell-dev \
3636
hunspell-en-us \
37+
hunspell-de-de \
38+
hunspell-fr \
3739
libclang-dev
3840
3941
- name: Test
@@ -46,10 +48,11 @@ jobs:
4648
- uses: actions/checkout@v4
4749
- uses: dtolnay/rust-toolchain@stable
4850
with:
49-
components: clippy
51+
components: rustfmt, clippy
5052
- uses: Swatinem/rust-cache@v2
5153
- name: Install hunspell
5254
run: |
5355
sudo apt-get update
5456
sudo apt-get install -y libhunspell-dev hunspell-en-us libclang-dev
57+
- run: cargo fmt --all -- --check
5558
- run: cargo clippy --all-targets -- -D warnings

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ This project follows [Semantic Versioning](https://semver.org/).
66

77
## [Unreleased]
88

9+
### Breaking
10+
- `Error` is now `InvalidLocale` / `UnsupportedLocale` / `DictionaryNotFound` / `InitializationFailed` (removed `Unavailable`)
11+
- macOS `with_locale` returns `UnsupportedLocale` if the language is not installed
12+
- Added `Checker::locale` and `Checker::available_locales`
13+
- Removed the crate binary (`src/main.rs`); use `examples/`
14+
915
### Changed
1016
- Document platform defaults for `Checker::new()`, locale failure, suggestions cap, and UTF-8 error ranges in rustdoc
1117
- README now states that macOS `Checker::new()` uses the system language

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "spellkit"
3-
description = "Bindings to your friendly neighborhood spellchecker."
4-
keywords = ["spellcheck", "spellchecker", "hunspell"]
3+
description = "Cross-platform native spell checking for Rust (NSSpellChecker, Windows Spell Checker, Hunspell)."
4+
keywords = ["spellcheck", "spellchecker", "hunspell", "nsspellchecker"]
55
categories = ["os", "text-processing"]
66
documentation = "https://docs.rs/spellkit"
77
repository = "https://github.com/rtmongold/spellkit"

README.md

Lines changed: 102 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,47 @@
1-
# spellkit
1+
# Spellkit
2+
3+
Cross-platform native spell checking for Rust.
24

35
[![On crates.io](https://img.shields.io/crates/v/spellkit.svg)](https://crates.io/crates/spellkit)
46
![Downloads](https://img.shields.io/crates/d/spellkit?style=flat-square)
57
[![CI](https://github.com/rtmongold/spellkit/actions/workflows/ci.yml/badge.svg)](https://github.com/rtmongold/spellkit/actions/workflows/ci.yml)
68
[![Docs](https://docs.rs/spellkit/badge.svg)](https://docs.rs/spellkit)
79

8-
Native spell checking with a small Rust API.
10+
## Why Spellkit?
911

10-
This project is **based on** [euclio/spellbound](https://github.com/euclio/spellbound)
11-
(last upstream commit 2020).
12+
Use the spell-checking facilities already on the user's system.
1213

13-
| Platform | API |
14-
| -------- | ------------------ |
14+
| Platform | Backend |
15+
| -------- | ------- |
1516
| macOS | [`NSSpellChecker`] |
16-
| Windows | [`ISpellChecker`] |
17-
| *nix | [`hunspell`] |
17+
| Windows | [`ISpellChecker`] (Windows Spell Checker) |
18+
| Linux / other Unix | [`Hunspell`] with system dictionaries |
1819

1920
[`ISpellChecker`]: https://docs.microsoft.com/en-us/windows/desktop/api/spellcheck/nn-spellcheck-ispellchecker
2021
[`NSSpellChecker`]: https://developer.apple.com/documentation/appkit/nsspellchecker
21-
[`hunspell`]: https://hunspell.github.io/
22+
[`Hunspell`]: https://hunspell.github.io/
23+
24+
Applications should not reimplement macOS, Windows, and Hunspell separately. Spellkit is one small API over those backends.
25+
26+
This project is **based on** [euclio/spellbound](https://github.com/euclio/spellbound) (last upstream commit 2020).
27+
28+
## What Spellkit is not
29+
30+
Spellkit does **not** bundle dictionaries or implement its own spelling algorithm. It wraps the platform backend and uses system / installed dictionaries. Behavior can differ across operating systems where the APIs differ.
2231

23-
## Example
32+
That is the distinction from crates that ship an engine and word lists (for example Spellbook).
33+
34+
## Quick start
35+
36+
cargo add spellkit
2437

2538
```rust
2639
use spellkit::Checker;
2740

2841
fn main() -> Result<(), spellkit::Error> {
2942
let checker = Checker::new()?;
30-
// Or: Checker::with_locale("en-US")?;
3143

32-
for err in checker.check("I beleeve I can fly") {
44+
for err in checker.check("I havv a spelling error.") {
3345
println!("{} @ {}..{}", err.text(), err.start(), err.end());
3446
for suggestion in checker.suggest(err.text()) {
3547
println!(" → {suggestion}");
@@ -39,30 +51,97 @@ fn main() -> Result<(), spellkit::Error> {
3951
}
4052
```
4153

42-
`Checker::new()` uses a platform default: system language on macOS, the user locale on Windows (falling back to `en-US`), and `LC_ALL` / `LC_MESSAGES` / `LANG` on Linux when a dictionary exists (otherwise `en_US` / `en_GB`). Use `with_locale` for another language.
54+
`Checker::locale()` is the language this instance is using. `Checker::available_locales()` lists what the OS can check.
4355

44-
Unknown or unsupported locales behave differently by platform:
56+
```rust
57+
use spellkit::Checker;
4558

46-
- **Linux:** missing dictionary / unknown locale → `Error::Unavailable`
47-
- **macOS:** empty locale → `Error::Unavailable`; unknown tags may still create a checker (system fallback)
48-
- **Windows:** unsupported language tag → `Error::Unavailable`
59+
fn main() -> Result<(), spellkit::Error> {
60+
println!("available: {:?}", Checker::available_locales());
61+
let checker = Checker::new()?;
62+
println!("using: {}", checker.locale());
63+
Ok(())
64+
}
65+
```
4966

50-
## Threading
67+
## Features
5168

52-
`Checker` is not `Send` or `Sync`. Do not share it across threads. macOS also serializes access to the shared `NSSpellChecker`.
69+
- Cross-platform: macOS, Windows, Linux
70+
- System dictionaries (no files shipped in the crate)
71+
- Suggestions (up to 10)
72+
- Locale via `with_locale` (`en_US` and `en-US` both work)
73+
- Temporary ignored words (`ignore` is per checker, not global)
74+
- UTF-8 byte ranges (`start` / `end` / `range`)
75+
- Small API
76+
77+
## Platform support
78+
79+
| | Linux | macOS | Windows |
80+
| --- | --- | --- | --- |
81+
| Backend | Hunspell | NSSpellChecker | ISpellChecker |
82+
| `Checker::new()` | `LC_ALL` / `LC_MESSAGES` / `LANG` if a dict exists, else `en_US` / `en_GB` | system language | user locale, else `en-US` |
83+
| Unknown `with_locale` | `Error::DictionaryNotFound` (paths searched) | `Error::UnsupportedLocale` | `Error::UnsupportedLocale` |
84+
| Empty locale | `Error::InvalidLocale` | `Error::InvalidLocale` | `Error::InvalidLocale` |
85+
| Suggestions | yes | yes | yes |
86+
| `ignore` | yes (this handle only) | yes (this document tag only) | yes (this checker only) |
87+
| `available_locales` | `*.dic` stems on disk (`DICPATH` then system dirs) | `availableLanguages` | `SupportedLanguages` |
88+
| `Send` / `Sync` | no | no | no |
89+
90+
Linux also honors `DICPATH` (colon-separated directories) before `/usr/share/hunspell` and the other built-in paths.
5391

54-
## Linux
92+
Word breaks are **not** identical: Linux tokenizes alphanumeric / `'` runs; macOS and Windows use the OS checker.
5593

56-
Needs a hunspell dictionary on disk (default search includes `/usr/share/hunspell`). Example:
94+
## How it works
95+
96+
`Checker` is a thin wrapper. On each OS it calls the native API, then converts misspelling ranges to UTF-8 byte offsets into the original `&str`.
97+
98+
## Spellkit vs other approaches
99+
100+
**Why not Spellbook?** Use Spellbook when you want a portable engine and bundled (or app-shipped) dictionaries. Use Spellkit when you want the OS dictionaries, native suggestions, and minimal integration.
101+
102+
**Why not Hunspell directly?** You would own dictionary discovery, FFI, and a second implementation for macOS and Windows. Spellkit is that integration.
103+
104+
**Why not ispell?** You would own an external process, its lifetime, and the command protocol. Spellkit stays in-process.
105+
106+
## Errors
107+
108+
- empty locale → `Error::InvalidLocale`
109+
- Linux missing `.aff`/`.dic``Error::DictionaryNotFound` (includes search paths)
110+
- macOS / Windows language not installed → `Error::UnsupportedLocale`
111+
- backend failed to start (null Hunspell handle, COM factory, empty macOS language) → `Error::InitializationFailed`
112+
113+
## Linux packages
57114

58115
- Arch: `pacman -S hunspell hunspell-en_us`
59116
- Debian/Ubuntu: `apt install libhunspell-dev hunspell-en-us`
117+
- Extra languages used in CI: `hunspell-de-de`, `hunspell-fr`
118+
119+
Without a dictionary, `Checker::new()` returns `Error::DictionaryNotFound`.
120+
121+
## Examples
60122

61-
Without a dictionary, `Checker::new()` returns `Error::Unavailable`.
123+
cargo run --example check -- "I havv a spelling error."
124+
cargo run --example suggestions -- "I beleeve I can fly"
125+
cargo run --example locale
126+
cargo run --example highlight
127+
128+
## Threading
129+
130+
`Checker` is not `Send` or `Sync`. Do not share it across threads. macOS also serializes access to the shared `NSSpellChecker`.
131+
132+
## Documentation
133+
134+
- [docs.rs/spellkit](https://docs.rs/spellkit)
135+
- [CHANGELOG.md](CHANGELOG.md)
136+
137+
## Contributing
138+
139+
Issues and PRs: [github.com/rtmongold/spellkit](https://github.com/rtmongold/spellkit)
62140

63141
## License
142+
64143
MIT OR Apache-2.0
65144

66145
## Credits
67146

68-
Originally by [Andy Russell](https://github.com/euclio). Maintained as `spellkit` by Robert Mongold.
147+
Originally by [Andy Russell](https://github.com/euclio). Maintained as `spellkit` by Robert Mongold.

examples/check.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
use spellkit::Checker;
2+
use std::env;
3+
4+
fn main() -> Result<(), spellkit::Error> {
5+
let text = env::args().skip(1).collect::<Vec<_>>().join(" ");
6+
let checker = Checker::new()?;
7+
for error in checker.check(&text) {
8+
println!("{}", error.text());
9+
}
10+
Ok(())
11+
}

examples/highlight.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
use spellkit::Checker;
2+
3+
fn main() -> Result<(), spellkit::Error> {
4+
let text = "I beleeve I can fly";
5+
let checker = Checker::new()?;
6+
for err in checker.check(text) {
7+
let range = err.range();
8+
println!("{} @ {range:?}", err.text());
9+
println!(" slice: {}", &text[range]);
10+
}
11+
Ok(())
12+
}

examples/locale.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
fn main() -> Result<(), spellkit::Error> {
2+
println!("available: {:?}", spellkit::Checker::available_locales());
3+
let c = spellkit::Checker::new()?;
4+
println!("locale: {}", c.locale());
5+
Ok(())
6+
}

examples/suggestions.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
use spellkit::Checker;
2+
use std::env;
3+
4+
fn main() -> Result<(), spellkit::Error> {
5+
let text = env::args().skip(1).collect::<Vec<_>>().join(" ");
6+
let checker = Checker::new()?;
7+
for error in checker.check(&text) {
8+
println!("{}", error.text());
9+
for s in checker.suggest(error.text()) {
10+
println!(" {s}");
11+
}
12+
}
13+
Ok(())
14+
}

0 commit comments

Comments
 (0)