I Built a Privacy-First Open-Source Spell Checker in Flutter — Here’s What I Learned
Writing software that looks simple on the surface can become surprisingly interesting once you start asking harder questions.
How should spelling errors be detected?
How should correction suggestions be ranked?
What happens when the text contains Unicode characters?
How do you safely modify text without accidentally applying a correction to the wrong location?
How can a writing assistant remain completely local instead of sending someone's text to a remote service?
And how do you make the same project work across Android, iOS, Windows, Linux, macOS, and the Web?
These questions are some of the reasons I built SpellChecker, an open-source, privacy-first Flutter spelling utility and deterministic writing assistant.
The project is available on GitHub:
GitHub Repository — sanskarIN/SpellChecker
This article is a deep look at the project, the engineering decisions behind it, the architecture, the privacy model, the challenges I encountered, and where I want to take it next.
What is SpellChecker?
SpellChecker is an open-source Flutter application and reusable Dart library for local spelling and writing analysis.
The core idea is simple:
Your text should be able to stay on your device while the application analyzes it.
Instead of requiring a remote spelling API, a cloud grammar service, an online account, or a generative rewriting backend, SpellChecker performs its core analysis locally.
The current project includes:
- 13 offline spelling language packs
- 10 built-in writing rules
- Local personal dictionaries
- Ranked spelling suggestions
- Source-range-safe corrections
- Unicode-aware tokenization
- Keyboard-first review workflows
- Writing analysis
- Portable settings
- Reusable Dart APIs
- Cross-platform Flutter targets
- Automated testing and CI
- MIT licensing
The current package version documented by the repository is 3.2.0+25.
The built-in spelling languages include:
- English (US)
- English (UK)
- Hindi
- Spanish
- French
- German
- Portuguese (Brazil)
- Italian
- Bengali
- Marathi
- Tamil
- Telugu
- Russian
That multilingual aspect is particularly important to me because a writing tool should not be designed around only one language or one region.
Why build another spell checker?
There are already many spell-checking tools.
So why build one?
For me, the interesting problem wasn't simply:
"Can I detect a misspelled word?"
The more interesting question was:
"Can I build a complete, explainable, offline-first writing tool where correction behavior is deterministic and the architecture can be reused by other Dart applications?"
That changes the engineering problem significantly.
A basic spell checker can be implemented with a dictionary lookup:
word -> dictionary lookup -> valid/invalid
But a useful writing assistant needs to handle much more:
raw text
↓
Unicode-aware tokenization
↓
language-aware dictionary lookup
↓
candidate generation
↓
suggestion ranking
↓
issue representation
↓
source-range validation
↓
safe correction
↓
updated editor state
Once you build the complete pipeline, seemingly small decisions become architecture decisions.
The privacy-first design
One of the main principles of SpellChecker is that the user's text should remain local.
The bundled application does not require:
- a remote spelling API
- a remote grammar API
- an AI rewriting service
- document uploads
- user accounts
- cloud synchronization for editor analysis
- telemetry for editor analysis
This is important because writing data can be extremely personal.
A user might paste:
- a private email
- a personal journal
- source code
- a business document
- an assignment
- financial notes
- unpublished content
- customer information
A writing assistant does not necessarily need that text to leave the user's device.
So I designed the core system around local processing.
The application uses local preferences for durable application settings, while editor content, active findings, ignored words, and correction history are not intended to become persistent cloud data.
The project documentation also deliberately separates privacy guarantees from functionality. That distinction matters.
Privacy isn't just a sentence in a README.
It has to be reflected in architecture.
Deterministic corrections
One of the more interesting parts of the project is correction safety.
Imagine the user has:
Helo world
The spelling engine detects:
Helo
and suggests:
Hello
That looks trivial.
But a real editor can change after the original analysis.
For example:
- The application analyzes the text.
- The user types additional characters.
- The application still has an older spelling result.
- A correction action is triggered.
If the application blindly uses the old character offsets, the wrong text could be modified.
That's unacceptable for an editor.
SpellChecker therefore uses source-range validation before applying corrections.
The idea is effectively:
analysis result
↓
remember original range
↓
verify current source still matches
↓
apply correction
If the current source no longer matches the analyzed range, the correction should not blindly overwrite whatever is now there.
This is a small example of a broader software engineering lesson:
Cached analysis should never be treated as unquestionable truth after the source has changed.
Safe batch writing corrections
Writing rules introduce another challenge.
Suppose a sentence contains several problems:
hello world!!
Potential findings could include:
- repeated spaces
- repeated punctuation
- sentence capitalization
Now imagine multiple automatic corrections being applied together.
Those corrections can interact.
Two edits could overlap or change the position of later edits.
To make this predictable, the project uses a deterministic and conservative approach to correction ranges.
The goal isn't to make the application aggressively "smart."
The goal is to make correction behavior understandable and safe.
That is an important philosophy throughout the project.
Unicode is harder than ASCII
One of the biggest lessons from building text-processing software is that text is not simply an array of English characters.
Real-world text can contain:
- accented characters
- non-Latin scripts
- combining marks
- Unicode punctuation
- multilingual content
- join controls
SpellChecker therefore includes Unicode-aware tokenization.
The project also takes care to keep source offsets compatible with Dart and Flutter text editing behavior.
This matters because an editor doesn't just need to know:
"This word is wrong."
It needs to know:
"This exact range in this exact source string is wrong."
And those ranges must remain meaningful in the environment where the text is actually edited.
That relationship between Unicode processing and editor offsets is one of the less visible but more important engineering details in a project like this.
Thirteen offline spelling languages
One of the parts of SpellChecker I'm especially interested in is the language-pack system.
Instead of making each supported language a special case inside the spelling engine, the project uses explicit language packs.
The current built-in set includes:
en-US
en-GB
hi-IN
es-ES
fr-FR
de-DE
pt-BR
it-IT
bn-IN
mr-IN
ta-IN
te-IN
ru-RU
This makes language selection explicit.
It also keeps personal vocabulary separated by language.
That distinction matters.
A personal dictionary isn't simply:
Set<String>
It can be treated as language-aware user vocabulary.
That makes behavior easier to reason about when switching between languages.
For example, a word accepted in one language should not automatically become a global accepted word in every language.
Writing analysis is separate from spelling
Another architectural decision was to distinguish spelling analysis from writing analysis.
These are related problems, but they aren't the same problem.
Spelling asks questions such as:
Is this word present in the active language dictionary?
Writing analysis can ask different questions:
Are there repeated words?
Is there repeated spacing?
Is punctuation spaced correctly?
Is there trailing whitespace?
Is the sentence beginning with an unexpected lowercase character?
SpellChecker currently includes ten built-in writing rules covering cases such as:
- repeated words
- sentence capitalization
- repeated spaces
- punctuation spacing
- missing punctuation spacing
- trailing whitespace
- repeated punctuation
- unmatched parentheses
- unmatched square brackets
- unmatched curly braces
This separation creates a cleaner architecture.
Instead of one giant "grammar engine," the project can have a registry of explicit writing rules.
Why some rules are advisory instead of automatic
One subtle example is unmatched delimiters.
Consider:
Hello (world
An application can detect the unmatched parenthesis.
But what should it do?
Should it:
Hello (world)
or:
Hello world
or:
Hello (world
and ask the user to fix it manually?
There isn't enough information to confidently know the user's intent.
So some structural findings are intentionally advisory rather than automatically corrected.
That's a useful design principle for developer tools:
Detection does not automatically imply safe correction.
A tool can know that something looks unusual without pretending it knows exactly how the author intended to fix it.
The editor experience
The project isn't only a library.
There is also a Flutter application around the analysis engine.
The workflow is intentionally straightforward.
A user can:
- Enter or paste text.
- Select a language.
- Run spelling analysis.
- Review underlined spelling issues.
- Inspect ranked suggestions.
- Move between issues with keyboard shortcuts.
- Run local writing analysis.
- Add words to a personal dictionary.
- Ignore a word for the current session.
- Export or transfer selected local settings.
Keyboard interaction is also an important part of the design.
For example:
Ctrl+Enter / Command+Enter
runs spelling analysis.
And:
F7
Shift+F7
can be used to move through spelling issues.
There is also a dedicated writing-insights workflow.
The purpose isn't to replace the editor with a complicated dashboard.
It's to make the feedback easy to review while keeping the underlying analysis deterministic.
Public Dart APIs
Another goal of the project is to avoid making the analysis engine inseparable from the Flutter UI.
The reusable layers expose public Dart APIs.
For example, a basic spelling check can look like:
import 'package:spellchecker/spell_checker.dart';
final engine = SpellCheckerEngine();
final issues = engine.check('Helo world');
for (final issue in issues) {
print('${issue.word}: ${issue.suggestions}');
}
That means another Dart or Flutter application can potentially use the core analysis functionality without reproducing the editor UI.
For bounded analysis, the engine can also work with explicit limits:
final report = engine.analyze(
text,
suggestionLimit: 5,
maxIssues: 200,
);
print(report.capturedIssueCount);
print(report.truncated);
Writing analysis is exposed separately:
import 'package:spellchecker/language.dart';
import 'package:spellchecker/writing.dart';
final analyzer = WritingAnalyzer();
final result = analyzer.analyze(
'hello world!!',
languagePack: SpellLanguageRegistry.englishUs,
maxIssues: 200,
);
for (final issue in result.issues) {
print('${issue.ruleId}: ${issue.message}');
}
This separation is valuable because the project becomes more than one application.
It becomes a reusable text-processing toolkit.
Large documents need boundaries
Another lesson from building developer tools is that "just process everything" isn't always a good design.
Large documents can contain huge numbers of possible findings.
Displaying thousands of findings in a UI isn't necessarily useful.
SpellChecker therefore has explicit bounded-result behavior.
The bundled UI captures the first 200 spelling issues and first 200 writing findings while preserving explicit semantics around truncation and totals.
This creates a distinction between:
total findings
and:
findings currently captured for interaction
That's an important distinction for both performance and user experience.
A tool should be honest about what it has processed and what it has decided to present.
Portable settings vs personal dictionaries
Another thing I wanted to keep explicit was local-data separation.
Personal vocabulary and application settings are different things.
The project therefore treats them as different transfer paths.
The personal dictionary can contain language-specific vocabulary.
Portable settings are intended for application preferences such as:
- selected language
- suggestion limit
- writing-rule overrides
Portable settings deliberately exclude things such as:
- editor text
- current findings
- source excerpts
- correction history
- ignored session words
- temporary Writing Insights state
This separation makes exported data more understandable and reduces the chance of accidentally bundling sensitive editor content into a settings transfer.
Architecture
At a high level, the application can be thought of like this:
┌──────────────────────┐
│ Flutter Editor │
└──────────┬───────────┘
│
┌─────────────┴─────────────┐
│ │
▼ ▼
┌────────────────────┐ ┌────────────────────┐
│ SpellCheckerEngine │ │ WritingAnalyzer │
└──────────┬─────────┘ └──────────┬─────────┘
│ │
┌──────┴──────┐ ┌─────┴──────┐
▼ ▼ ▼ ▼
Language Pack Ranker Rule Registry Findings
│ │
└──────┬──────┘
▼
Spell Issues
│
▼
Safe Correction
Preferences ───────► Local Storage
The important architectural point is that the reusable spelling and writing layers are not built around Flutter widgets.
That makes the domain logic easier to test, reuse, and reason about.
The repository structure reflects this separation.
There are dedicated areas for:
lib/core/
lib/data/
lib/writing/
lib/features/
lib/storage/
docs/
test/
tool/
and the cross-platform Flutter runner directories are committed as part of the repository.
Testing the project
A writing assistant needs tests because text processing can fail in subtle ways.
A simple test like:
"Helo" -> "Hello"
isn't enough.
You also want to test things like:
- empty text
- single words
- Unicode text
- different languages
- repeated words
- overlapping corrections
- stale source ranges
- settings persistence
- transfer codecs
- suggestion ranking
- writing-rule behavior
- large input
- UI workflows
SpellChecker's development workflow includes formatting, static analysis, the Flutter test suite, and deterministic benchmark smoke testing.
The repository also documents cross-platform validation and release-oriented build checks.
That is important because "it runs on my machine" is not enough for a cross-platform project.
Why deterministic behavior matters
A lot of modern developer tools depend on probabilistic systems.
That's powerful, but not every problem needs a generative model.
SpellChecker intentionally focuses on deterministic local behavior.
For example:
same input
+
same language
+
same rules
+
same configuration
=
same analysis
That can make testing easier.
It can make bug reports easier.
It can make corrections easier to reproduce.
And it can make users more confident about why the application flagged something.
There is also something appealing about building useful developer tooling without requiring an AI backend for every feature.
Building with Flutter
Flutter was a natural choice because the project needs a shared application architecture across multiple platforms.
The repository currently has runners for:
Android
iOS
Linux
macOS
Windows
Web
That means the same overall project can target desktop, mobile, and browser environments.
Flutter also makes it possible to build a polished editor experience while keeping the reusable Dart logic relatively independent.
The architecture therefore combines:
Dart domain logic
+
Flutter application layer
+
local persistence
+
platform runners
rather than creating six completely separate applications.
Lessons from building a real open-source project
Projects like this teach lessons that aren't always obvious from tutorials.
1. Edge cases become features
Unicode handling wasn't just "extra polish."
Source-range safety wasn't just "defensive programming."
Bounded analysis wasn't just "optimization."
These became part of the actual product.
2. Documentation is part of engineering
As a project grows, people need to answer questions such as:
- How does the architecture work?
- Which languages are included?
- What does the privacy model actually mean?
- How do I add a language pack?
- How do I create a custom writing rule?
- What APIs are public?
- How do I run tests?
- How do I build for each platform?
That is why SpellChecker contains a substantial documentation structure rather than relying on one giant README.
A project becomes much easier to maintain when its documentation reflects the architecture.
3. Open source means designing for other developers
When code is public, you're no longer writing only for yourself.
Someone else might:
- fork the project
- submit a pull request
- use the Dart API
- add another language
- report a Unicode bug
- improve accessibility
- propose a new writing rule
So code organization, documentation, tests, contribution guidance, and clear boundaries matter.
4. "Automatic" should have a high bar
A writing tool shouldn't make edits simply because an algorithm can detect an anomaly.
There should be a distinction between:
detected
and:
safe to automatically modify
That distinction influenced the design of advisory writing rules and source-range-safe corrections.
What I'd like to improve next
SpellChecker is an ongoing open-source project.
There are many directions that could make it more useful.
Potential areas include:
More language packs
The current language registry can grow beyond the existing 13 offline spelling languages.
Adding a language isn't just about adding words.
It requires thinking about:
- normalization
- tokenization
- dictionaries
- frequency data
- suggestion quality
- personal vocabulary
- testing
- documentation
More writing rules
The writing-rule architecture makes it possible to add additional deterministic analysis rules.
Potential future areas include:
- additional punctuation patterns
- consistency checks
- formatting checks
- style diagnostics
- repeated phrasing detection
- configurable organization-specific writing rules
The challenge is keeping these rules explainable and predictable.
Better developer APIs
Another direction is making SpellChecker easier to embed into other Dart and Flutter projects.
For example:
Flutter applications
Desktop editors
Note-taking tools
Documentation editors
Educational software
Offline writing tools
Developer tools
A reusable analysis engine can potentially serve all of these without requiring the complete SpellChecker UI.
Improved performance for very large text
As document size increases, analysis strategies become more important.
Areas worth exploring include:
- incremental analysis
- smarter caching
- partial document analysis
- background processing
- better benchmark coverage
- memory optimization
The objective would be to make large-document behavior feel responsive without compromising deterministic results.
How you can contribute
Because SpellChecker is open source, contributions are welcome.
You don't need to rewrite the entire project to contribute.
Useful contributions can include:
documentation improvements
bug fixes
tests
language support
writing rules
accessibility improvements
UI improvements
performance improvements
developer tooling
cross-platform fixes
Before contributing, the repository includes guides for development, testing, contributing, documentation maintenance, and release workflows.
A good first contribution can be something as simple as improving a test case or documentation page.
Sometimes those contributions are what eventually lead to larger improvements.
Why I open-sourced it
I could have kept the project private.
But open source creates a different development environment.
People can inspect the implementation.
They can question design decisions.
They can reproduce issues.
They can propose alternatives.
They can fork it.
They can learn from it.
And I can learn from them.
That's one of the biggest reasons I enjoy building public software.
An open-source repository becomes more than a code archive.
It becomes a technical conversation.
What I learned from SpellChecker
If I had to summarize the biggest lessons from this project, they would be these:
Text processing is deceptively complex
Spelling is easy to demonstrate and much harder to engineer correctly.
Privacy has to be architectural
A "privacy-first" product shouldn't depend on a cloud service for its core editor analysis.
Determinism is valuable
Predictable tools are easier to test, debug, reproduce, and trust.
Unicode matters
Modern text-processing software has to account for the real world, not only simple ASCII examples.
Correction safety matters as much as detection
Finding an issue is only half the problem.
Applying a correction safely is another engineering problem entirely.
Open source is a feedback loop
The more understandable and reusable a project becomes, the more useful it can be to other developers.
Try the project
You can explore the full source code, documentation, tests, and development workflow in the GitHub repository:
SpellChecker:
https://github.com/sanskarIN/SpellChecker
The project is released under the MIT License.
You can also explore my other open-source work through my GitHub profile:
And my open-source developer website:
Final thoughts
SpellChecker started from a relatively simple idea:
Build a useful spell-checking tool that works locally.
But turning that idea into a real project exposed a much larger engineering landscape.
There are language systems.
There are Unicode rules.
There are correction safety problems.
There are document-size constraints.
There are persistence boundaries.
There are public APIs.
There are platform concerns.
There are tests.
There is documentation.
There is accessibility.
And there is the ongoing challenge of making all of those pieces work together without turning the system into something unpredictable.
That is what makes software projects interesting.
The code is only one part of the product.
The architecture, boundaries, testing, documentation, user experience, privacy model, and community around the code matter too.
SpellChecker is still evolving, and I'm interested in continuing to improve the project while keeping its core principles intact:
local processing, deterministic behavior, explicit boundaries, reusable APIs, and open-source development.
If you're interested in Flutter, Dart, text processing, developer tools, Unicode, offline-first software, or open-source projects, I'd love for you to explore the repository and see how the pieces fit together.
Repository:
https://github.com/sanskarIN/SpellChecker
Developer:
Sanskar
Made by the Sanskar.
Top comments (0)