Español 🇪🇸 | 中文 🇨🇳 | Deutsch 🇩🇪 | Français 🇫🇷 | 日本語 🇯🇵 | 한국어 🇰🇷 | Português 🇧🇷 | Русский 🇷🇺 | Tiếng Việt 🇻🇳 | हिन्दी 🇮🇳 | العربية 🇸🇦...
A tiny multilingual spell checker with correction suggestions. Dictionaries are bundled, so
npm i fixnowgives you everything — with zero runtime dependencies, in both ESM and CommonJS.
- 📦 Zero Dependencies — Keeps your
node_modulesclean and lightweight. - 🌍 Built-in Dictionaries — Includes Arabic, German, English, Spanish, French, Portuguese, Russian, and Vietnamese.
- ⚡ Slim Builds — Import only the language you need (e.g.
import { check } from "fixnow/es") to optimize bundle size. - 🛡️ Smart Tokenization — Automatically ignores code spans, URLs, emails, and identifiers to prevent false positives.
- 🧩 Universal — Works seamlessly in both ESM and CommonJS projects.
flowchart LR
Input["Input Text"] --> Tokenizer
Tokenizer --> |"Skips Code / URLs"| Check{"Dictionary"}
Check -->|"Valid Word"| Pass["No Issues"]
Check -->|"Typo Detected"| Engine("Correction Engine")
Engine --> Issues["SpellIssue[]"]
style Input fill:#18181b,stroke:#ef4444,stroke-width:2px,color:#fff
style Issues fill:#18181b,stroke:#ef4444,stroke-width:2px,color:#fff
npm i fixnow| Code | Language | Dictionary license |
|---|---|---|
ar |
Arabic | LGPL-3.0 |
de |
German | LGPL-3.0 |
en |
English | MIT |
es |
Spanish | LGPL-3.0 |
fr |
French | MIT |
pt |
Portuguese | GPL-3.0-or-later |
ru |
Russian | GPL-3.0-or-later |
vi |
Vietnamese | MIT |
import { checkText, suggest, createChecker } from "fixnow";
// English
const enIssues = await checkText("This sentance has a typo", {
language: "en",
suggestions: true,
});
// -> [{ offset: 5, length: 8, word: 'sentance', suggestions: [...] }]
// Spanish — opt in to accent leniency if you don't want "codigo" flagged.
const esIssues = await checkText("Esto es un herror", {
language: "es",
suggestions: true,
acceptAccentOmissions: true,
});
// -> [{ offset: 11, length: 6, word: 'herror', suggestions: [...] }]
// One-off correction suggestions
await suggest("bonjoor", { language: "fr" }); // -> ['bonjour', ...]
// A checker bound to one language
const de = createChecker("de");
await de.isCorrect("Haus"); // -> trueCommonJS works too:
const { checkText } = require("fixnow");checkText(text, options)→Promise<SpellIssue[]>isCorrect(word, language, options?)→Promise<boolean>suggest(word, { language, max? })→Promise<string[]>createChecker(language)→ bound{ check, suggest, isCorrect, warmup }warmup(language?)— preload dictionaries (skip first-call decode cost)tokenize(text, protectedSegments?),DEFAULT_PROTECTED_PATTERNSUPPORTED_LANGUAGES,LANGUAGES,isSupportedLanguage
CheckOptions: language (required), caseSensitive (false), acceptAccentOmissions
(false; Spanish only), suggestions, maxSuggestions (5), minWordLength (3),
ignoreWords, flagWords, isProtectedWord, protectedSegments.
checkText skips anything inside a "protected segment" (code spans, URLs, emails, paths,
CLI flags, hex colors, ACRONYMS, file names and dotted identifiers). Override the
patterns with protectedSegments:
import { checkText, DEFAULT_PROTECTED_PATTERN } from "fixnow";
// Use only your own pattern
await checkText(text, { language: "en", protectedSegments: /\{\{[^}]+\}\}/g });
// Compose with the default
await checkText(text, {
language: "en",
protectedSegments: [DEFAULT_PROTECTED_PATTERN, /\{\{[^}]+\}\}/g],
});
// Disable protection entirely
await checkText(text, { language: "en", protectedSegments: false });The same option is exposed on tokenize(text, protectedSegments).
If you only need one language, import it via the language subpath. Your bundler only copies the dictionary you actually use:
import { check, suggest } from "fixnow/es";
const issues = await check("Esto es un herror", { suggestions: true });
await suggest("bonjoor", 3); // bound suggest is (word, max?)The slim entries (fixnow/ar, fixnow/de, fixnow/en, fixnow/es, fixnow/fr,
fixnow/pt, fixnow/ru, fixnow/vi) re-export a checker pre-bound to that language.
fixnow reads its dictionaries from disk at runtime — they ship as files under
node_modules/fixnow/dictionaries/, not as inlined bytes in the JS. So any bundler
must treat fixnow as external, leaving it to load from node_modules at runtime.
This is required for VS Code extensions and any CJS bundle: inlining fixnow into
a CJS output strips the path anchor it uses to find its dictionaries, and it will throw
a clear "mark 'fixnow' as external" error instead of resolving them.
// esbuild
await esbuild.build({
entryPoints: ["src/extension.ts"],
bundle: true,
format: "cjs",
platform: "node",
external: ["fixnow"],
});The matching option for other bundlers:
- Vite —
build.rollupOptions.external: ['fixnow'] - Rollup —
external: ['fixnow'] - webpack —
externals: { fixnow: 'commonjs fixnow' }
2.0.0 cleans up three rough edges from the extraction-from-F1 release. Each is a
breaking change:
languageis now required. There is no default language anymore.// before await checkText("hola"); // implicitly Spanish // after await checkText("hola", { language: "es" });
strictis split intocaseSensitiveandacceptAccentOmissions. The new default is strict (the oldstrict: true). If you relied onstrict: falseto tolerate Spanish accent omissions, opt in explicitly:The legacy// before await checkText("codigo", { language: "es" }); // accepted // after await checkText("codigo", { language: "es", acceptAccentOmissions: true });
strictkey still works in 2.x with aconsole.warn; it is removed in3.0.0.- F1-specific markers are gone from the default tokenizer.
[Image #1],[Skills #…],/skills #N, and/skillno longer auto-skip. If you need them, pass them viaprotectedSegments:const F1_MARKERS = /\[(?:Image|Code|Text) #\d+[^\]\n]*\]|\[Skills? #[^\]\n]+\]|\/skills #\d+|\/skill\b/g; await checkText(text, { language: "en", protectedSegments: [DEFAULT_PROTECTED_PATTERN, F1_MARKERS], });