DEV Community

Sanskar
Sanskar

Posted on

I Built a Privacy-First Open-Source Spell Checker in Flutter — Here’s What I Learned

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The spelling engine detects:

Helo
Enter fullscreen mode Exit fullscreen mode

and suggests:

Hello
Enter fullscreen mode Exit fullscreen mode

That looks trivial.

But a real editor can change after the original analysis.

For example:

  1. The application analyzes the text.
  2. The user types additional characters.
  3. The application still has an older spelling result.
  4. 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
Enter fullscreen mode Exit fullscreen mode

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!!
Enter fullscreen mode Exit fullscreen mode

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."
Enter fullscreen mode Exit fullscreen mode

It needs to know:

"This exact range in this exact source string is wrong."
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

This makes language selection explicit.

It also keeps personal vocabulary separated by language.

That distinction matters.

A personal dictionary isn't simply:

Set<String>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

An application can detect the unmatched parenthesis.

But what should it do?

Should it:

Hello (world)
Enter fullscreen mode Exit fullscreen mode

or:

Hello world
Enter fullscreen mode Exit fullscreen mode

or:

Hello (world
Enter fullscreen mode Exit fullscreen mode

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:

  1. Enter or paste text.
  2. Select a language.
  3. Run spelling analysis.
  4. Review underlined spelling issues.
  5. Inspect ranked suggestions.
  6. Move between issues with keyboard shortcuts.
  7. Run local writing analysis.
  8. Add words to a personal dictionary.
  9. Ignore a word for the current session.
  10. Export or transfer selected local settings.

Keyboard interaction is also an important part of the design.

For example:

Ctrl+Enter / Command+Enter
Enter fullscreen mode Exit fullscreen mode

runs spelling analysis.

And:

F7
Shift+F7
Enter fullscreen mode Exit fullscreen mode

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}');
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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}');
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

and:

findings currently captured for interaction
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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/
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

and:

safe to automatically modify
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

https://github.com/sanskarIN

And my open-source developer website:

https://sanskarin.github.io


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)