WP Manifestindependent plugin directory
manifest / content / splecheh

Splecheh - WordPress spellcheck plugin releases

Splecheh - Free Spell Check WordPress Plugin. A lightweight, entirely free background spell checker that audits your content for typos without the premium nags or caps.

by Aivars Lauzis · github.com/lauzis/splecheh · website

0stars
0forks

Install

No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:

wp plugin install https://github.com/lauzis/splecheh/archive/refs/heads/main.zip

Readme

Splecheh — WordPress Spellcheck Plugin

What is it?

A WordPress plugin that runs spell checks across all articles and post types to surface spelling errors in a single view.

Who's it for?

Editors and content managers who need to maintain writing quality across a WordPress site without reviewing each post individually.

What it does

  • Scans all posts and custom post types for spelling errors.
  • Flags double spaces (runs of 2+ spaces/tabs between words) in the same pass — invisible in the rendered page, but noise in the source.
  • Lists all spellcheck issues in a central admin view.
  • Admin menu accessible to editors (and above).
  • Optional Interpunction Check: uses an LLM to fix punctuation and capitalization, sentence by sentence (see below).

How It Works

This section diagrams how each feature actually behaves in code. Node colors follow the project convention: external systems / persistence (Aspell, LLM providers, WordPress storage) are purple, content-modifying actions are green, and report / output artifacts are blue.

Text Splitting (Splecheh_ContentSplitter)

Both checks start from the same tree-based split so text is never merged across a block boundary (issue #62). Each block-level element (h1h6, p, li, blockquote, td/th, div, pre, figcaption, …) becomes one chunk carrying both its plain text (for spell check / sentence splitting) and its inner HTML (so inline <strong>/<em>/<a> formatting is preserved). A container that itself holds block children is recursed into; loose inline/text between blocks is flushed into its own anonymous chunk.

flowchart TD
    A[post_content HTML] --> B{Ignore Shortcodes<br/>enabled?}
    B -->|yes| C[strip_shortcodes:<br/>bracket literals → space]
    B -->|no| D[load HTML tree<br/>via DOMDocument / libxml]
    C --> D
    D --> E[walk children]
    E --> F{Node kind?}
    F -->|leaf block| G[emit chunk:<br/>tag + plain text + inner HTML]
    F -->|block with<br/>block children| H[recurse into element]
    F -->|inline / text node| I[buffer as loose inline run]
    H --> E
    I --> J[flush at next block boundary<br/>→ anonymous chunk]
    G --> K[ordered chunk list]
    J --> K
    K --> L[Spell Check:<br/>plain texts joined by a space]
    K --> M[Interpunction Check:<br/>sentences split per block, in order]

    classDef output fill:#0075ca,color:#fff
    class K,L,M output

Writing Fixes Back

A fix always edits the raw post_content — inline formatting survives because the surrounding HTML is left untouched. The entry points differ in scope and in how they locate the text:

flowchart TD
    F1[Details: Fix one occurrence] --> R1[replace_occurrence<br/>locate via excerpt, replace 1]
    F2[Auto-Apply list match on a run] --> R2[replace_all_occurrences<br/>every whole-word match]
    F3[Interpunction: Fix a sentence] --> R3[apply_fix<br/>literal, then whitespace-flexible]
    F4[Details: Fix a whitespace run] --> R4[replace_whitespace_run<br/>literal, first occurrence]
    R1 --> WP[(wp_update_post<br/>or report JSON update)]
    R2 --> WP
    R3 --> WP
    R4 --> WP
    WP --> V{find_unapplied_fixes<br/>re-read + re-split the saved post}
    V -->|text is there| DONE[mark resolved]
    V -->|missing| OPEN[leave unresolved,<br/>log + report to the user]

    classDef store fill:#6d28d9,color:#fff
    classDef act fill:#16a34a,color:#fff
    class WP store
    class R1,R2,R3,R4,DONE,OPEN act

Locating the text is the fragile part, so nothing is marked resolved on faith. replace_occurrence(), apply_fix() and replace_whitespace_run() all return null when the text isn't in the content any more, and after the write find_unapplied_fixes() re-reads the post and confirms each fix survived saving (kses, other plugins' content filters). Anything that fails either check stays unresolved and is reported on the Details page.

apply_fix() matches whitespace-flexibly on purpose: report sentences come from the splitter with every whitespace run collapsed to one space, so a literal search would miss a paragraph containing a double space or a line break mid-sentence. A sentence broken up by inline markup (<strong>, <a>) is still not matched — writing plain text back over markup would destroy it — and is reported as unapplied.

Spell Check (Splecheh_SpellCheckReport::run)

Content is flattened via the splitter, checked against Aspell/pspell, then passed through the auto-apply and the three ignore filters in this order before a JSON report is written to wp-content/uploads/splecheh/ and the unresolved count is stored in post meta.

flowchart TD
    A[run post_id] --> B[prepare_text:<br/>splitter → plain text]
    B --> C{Empty text?}
    C -->|yes| Z[empty report]
    C -->|no| D[[Aspell / pspell lookup]]
    D --> E{Wordlist<br/>installed?}
    E -->|no| ERR[friendly missing-wordlist error]
    E -->|yes| F[misspelled words<br/>+ suggestions + example sentence]
    F --> G[apply_auto_fixes:<br/>replace + drop + audit log]
    G --> H[filter_ignored_words:<br/>per-post meta + global list]
    H --> I[filter_term_ignored:<br/>multi-word terms]
    I --> R[save_report JSON]
    Z --> R
    R --> M[(post meta:<br/>checked_at, version, issue_count)]

    classDef ext fill:#6d28d9,color:#fff
    classDef store fill:#6d28d9,color:#fff
    classDef act fill:#16a34a,color:#fff
    classDef output fill:#0075ca,color:#fff
    class D ext
    class M store
    class G act
    class R output

Auto-Apply List (Splecheh_AutoApplyList)

A global, language-scoped store of word → replacement pairs (splecheh_auto_apply_list option), keyed by the post's resolved language code (Polylang → WPML → Settings/locale fallback). Entries are added from the Details page's "Fix everywhere in {language}" action or the Settings > Auto-Apply List page. On each run, a matching misspelling is rewritten everywhere in the post, dropped from the report, and recorded in a separate auto-apply-YYYY-MM-DD.log audit log — applied on the next run/re-run/cron pass, not retroactively.

flowchart TD
    subgraph Add
      A1[Details: Fix everywhere in language] --> S[(splecheh_auto_apply_list<br/>option, per language)]
      A2[Settings: Add an entry] --> S
    end
    subgraph Run
      B[flagged word on a run] --> C{lowercased word<br/>in pairs for this language?}
      C -->|no| K[keep as report error]
      C -->|yes| D[replace_all_occurrences<br/>in post_content]
      D --> E[(wp_update_post)]
      D --> L[[auto-apply-YYYY-MM-DD.log]]
      D --> P[drop from report]
    end
    S -.lookup by language.-> C

    classDef store fill:#6d28d9,color:#fff
    classDef act fill:#16a34a,color:#fff
    class S,E,L store
    class D act

The Three Ignore Mechanisms

All three filter errors during a spell check run and are language-scoped, but they differ in reach:

  • Per-post ignore_splecheh_ignored_words post meta; only affects that one post ("Ignore in post").
  • Global ignore listsplecheh_ignore_list option, per language; affects every post in that language ("Ignore always" / Settings > Ignore List).
  • Term ignore listsplecheh_term_ignore_list option, per language, for multi-word terms (e.g. "Steam Deck"). Aspell flags each word separately, so an error is only dropped when the flagged word is part of a listed term and the word's sentence contains every word of that term (a partial appearance is still flagged).
flowchart TD
    E[flagged word + example sentence] --> A{word in per-post<br/>ignored list?}
    A -->|yes| DROP[drop from report]
    A -->|no| B{word in global<br/>ignore list for language?}
    B -->|yes| DROP
    B -->|no| C{word part of a listed term<br/>AND full term present<br/>in the sentence?}
    C -->|yes| DROP
    C -->|no| KEEP[keep as report error]

    M1[(_splecheh_ignored_words<br/>post meta)] -.-> A
    M2[(splecheh_ignore_list<br/>option)] -.-> B
    M3[(splecheh_term_ignore_list<br/>option)] -.-> C

    classDef store fill:#6d28d9,color:#fff
    classDef output fill:#0075ca,color:#fff
    class M1,M2,M3 store
    class KEEP,DROP output

Interpunction Check (Splecheh_InterpunctionReport::run)

An opt-in, LLM-based punctuation/capitalization check. It is gated on Spell Check being clean first (when "Require Spell Check First" is on), splits the post into sentences per block, sends them to the configured provider in chunks (default 5 sentences/call), keeps only sentences the model actually changed, and saves a report to wp-content/uploads/splecheh-interpunction/. A failed chunk still saves the chunks that already succeeded as a partial report.

flowchart TD
    A[run post_id] --> G{Require Spell Check First<br/>and post not clean?}
    G -->|yes| STOP[skip: no LLM call]
    G -->|no| B[split_content_into_sentences:<br/>per-block sentence split]
    B --> C[chunk sentences<br/>default 5 per call]
    C --> D{Provider type}
    D -->|Commandline| P1[[shell command<br/>JSON in → JSON out]]
    D -->|OpenAI| P2[[OpenAI API]]
    D -->|Claude| P3[[Anthropic API]]
    D -->|Gemini| P4[[Gemini API]]
    P1 --> E{chunk failed?}
    P2 --> E
    P3 --> E
    P4 --> E
    E -->|yes| PART[save succeeded chunks<br/>as partial report + return error]
    E -->|no| F[build_issues:<br/>keep only changed sentences]
    F --> H[filter_ignored_sentences:<br/>per-post + global]
    H --> R[save report JSON<br/>+ diff_highlight per issue]
    PART --> M[(post meta:<br/>checked_at, chunks_processed/total)]
    R --> M

    classDef ext fill:#6d28d9,color:#fff
    classDef store fill:#6d28d9,color:#fff
    classDef output fill:#0075ca,color:#fff
    class P1,P2,P3,P4 ext
    class M store
    class R,PART output

Interpunction Check

Interpunction Check is a separate, opt-in feature (Settings > Interpunction Check, disabled by default) that reviews punctuation and capitalization using an LLM instead of a dictionary — sentence by sentence, with the same Run Now/Re-run, report/status tracking, and Details page (Fix / Ignore in post / Mark Complete) as Spell Check. There is no "Ignore always" here — unlike a misspelled word, a flagged sentence won't recur verbatim in another post.

Settings:

  • Enable Interpunction Check — shows the "Interpunction Check" page in the admin menu.
  • Require Spell Check First — enabled by default; skips Interpunction Check for a post (Run Now, bulk runs, background check) until its Spell Check is up to date with zero unresolved issues. Applying an interpunction fix re-runs Spell Check for that post automatically, so the fix's own edit never leaves the post blocked — and a spelling error introduced by the fix is caught immediately.
  • Type — how the request is made: Commandline - Local model, OpenAI, Claude, or Gemini.
  • Commandline Command — shown only for the Commandline type (see contract below); defaults to the bundled tools/llm-wrapper.php, which calls the claude CLI unless the Local Model dropdown below selects an Ollama model.
  • Local Model (via wrapper) — shown only for the Commandline type; picks an Ollama model (Qwen 2.5 3B/7B/14B/32B) to append to the Commandline Command as --provider ollama --model <selection>. Left on its default, the command runs as typed (claude). See tools/README.md for setup.
  • Endpoint — optional override of the default API URL; shown only for OpenAI/Claude/Gemini.
  • Access Key — API token for OpenAI/Claude/Gemini; not needed (or stored) for Commandline.
  • Prompt — instruction sent to the LLM; defaults to You are a professional {language} editor. Your only task is to fix the punctuation and capitalization of the provided text. Keep the original text content exactly as is. Output only the corrected text.{language} is replaced with the post's language.
  • Sentence Chunk Size — how many sentences are sent per LLM call (default 5, 0 disables chunking); see below for why this exists. Also filterable via splecheh_interpunction_chunk_size, which takes precedence over this field.
  • Command Timeout (seconds) — shown only for the Commandline type; how long one call may take before it is killed and reported as an error (default 60). Also filterable via splecheh_interpunction_command_timeout, which takes precedence over this field. When the command runs the bundled tools/llm-wrapper.php, the wrapper is passed this value minus 5 seconds as --timeout (unless the command already sets its own), so the two timeouts can't drift apart. A browser-triggered Run Now is additionally bound by max_execution_time, PHP-FPM's request_terminate_timeout, and the web server's proxy/FastCGI read timeout — all of which must exceed this value for a longer timeout to take effect.
  • Background Interpunction Check — Enable, Schedule Interval (default every 10 minutes), and Batch Size (default 1 post per run) for an automatic WP-Cron check of outdated posts, mirroring Background Spell Check.

A post's sentences are sent to the provider in chunks, not all in one call — a real post can have far more sentences than the Settings page "Test" button's small sample, and a single call for dozens of sentences can need much longer than any reasonable timeout, especially for a CLI/local model. Chunking keeps each call's timeout meaningful and means one slow/failing chunk doesn't necessarily require redoing the whole post; the trade-off is that a large post now takes several sequential calls (and correspondingly longer in total) instead of one — see tools/README.md for measured per-call timings you can use to size the chunk value for your setup.

Read the full README on GitHub →

Releases

TagPublished
v0.30.1 Aug 18, 2026
v0.30.0 Aug 18, 2026

These releases are tags only. The author does not attach a packaged zip, so there are no download counts to report.