language.nvim · Editing · vimdoc

:help language

Spell, grammar and translation tooling

doc/language.txt — rendered from the plugin's own vimdoc

*language.txt*                     Spell, grammar and translation tooling
                                                               *language.nvim*

                         language.nvim manual~

Author:  Stefan Bartl

CONTENTS *language-contents*

    1. Introduction ......................... |language-introduction|
    2. Requirements ......................... |language-requirements|
    3. Setup ................................ |language-setup|
    4. Commands ............................. |language-commands|
    5. Scoping .............................. |language-scoping|
    6. Spell providers ...................... |language-spell-providers|
    7. Translate engines .................... |language-translate-engines|
    8. The review panel ..................... |language-panel|
    9. Configuration ........................ |language-config|
   10. Health ............................... |language-health|

1. INTRODUCTION *language-introduction*

language.nvim bundles three text-language tools behind a small set of commands:

    :Spellcheck        spelling + grammar review, worked through in one place
    :Translate         translate a range/selection, shown in a popup (default)
    :TranslateReplace   translate a range/selection and replace it in place

It is built on lib.nvim as a hard dependency — including for UI (the
translation popup, pickers, menus use ui.kit). Translation is
native — no external Neovim plugin is required; only curl (the default
Google engine is keyless and works out of the box).

Both domains share one explicit scope model (|language-scoping|) and run
long/blocking work asynchronously with cancellation and timeouts.

2. REQUIREMENTS *language-requirements*

    - Neovim >= 0.9 (0.10+ recommended for vim.system)
    - StefanBartl/lib.nvim
    - curl (for translation)
    - Optional: folke/trouble.nvim (nicer list; pcall-guarded)
    - Optional spell CLIs: typos, cspell, codespell
    - Optional grammar LSP: harper_ls, ltex
    - Optional: trans (translate-shell) for the "shell" engine

3. SETUP *language-setup*

    require("language").setup({})
Call once. See |language-config| for the full option tree. With lazy.nvim:
    {
      "StefanBartl/language.nvim",
      dependencies = { "StefanBartl/lib.nvim", "folke/trouble.nvim" },
      event = "VeryLazy",
      config = function() require("language").setup({}) end,
    }

4. COMMANDS *language-commands*

Each command is its own lib.nvim.bindings.usercmd.composer verb (a flat root route,
no subcommand tree). An unrecognized --flag on :Translate/:TranslateReplace
now reports a clear error instead of being silently ignored, as it was
before this command layer was rebuilt on the composer; dispatch for valid
input is otherwise unchanged. Likewise, a positional token that is neither
the language nor a recognized scope word (typically a typo, e.g. selction
for selection) is reported as an error rather than silently dropped and
defaulting to the whole-buffer scope.

                                                                   *:Spellcheck*
:Spellcheck [lang] [scope|verb]
    Run a spell/grammar review. Optional first argument is a language code
    (e.g. en, de, en,de). The scope/verb may be one of:
        buffer      current buffer (default)
        visible     the visible window range only
        cwd         recursively across the working directory
        path=<p>    a file or directory
        clear       end the session, remove diagnostics
        refresh     re-scan the current buffer session
    Examples:
        :Spellcheck
        :Spellcheck en cwd
        :Spellcheck de path=~/notes
        :Spellcheck clear
                                                                    *:Translate*
:[range]Translate <lang> [--nocode] [--output=<mode>] [scope]
    Translate the given range/selection to <lang> and **show the result**
    without touching the buffer (default output: popup, a read-only,
    focusable, closable (q/<Esc>) ui.kit float near the cursor —
    scroll/yank freely, nothing is written back). Options:
        --nocode            skip fenced code blocks and inline-code lines
                            (only takes effect together with --output=replace)
        --output=<mode>     popup (default) | replace | buffer | vsplit
                            | split | tab | insert | clipboard | notify
        --files=<mode>      output mode when scope is cwd/path=<dir> — see
                            |language-translate-files|
        scope               selection (default with a range) | buffer | cwd
                            | path=<p>
    Examples:
        :'<,'>Translate DE            " popup near the cursor, buffer untouched
        :Translate EN --output=vsplit " open the translation in a vertical split
        :Translate FR --output=buffer " open the translation in a new buffer
                                                             *:TranslateReplace*
:[range]TranslateReplace <lang> [--nocode] [scope]
    Translate the given range/selection to <lang> and **replace it in
    place** — the direct, mutating counterpart to :Translate (this is what
    :Translate did before language.nvim gained a non-mutating default; the
    name/behavior matches the classic :TranslateReplace). Always replaces;
    there is no --output=. --nocode skips fenced code blocks and
    inline-code lines. scope is selection (default with a range) |
    buffer | cwd | path=<p> — for cwd/path=<dir> this picks and
    overwrites multiple files in place (with a confirmation prompt), see
    |language-translate-files|.
    Examples:
        :'<,'>TranslateReplace DE
        :TranslateReplace EN --nocode
        :TranslateReplace DE cwd       " pick files, overwrite in place
    The target span/file is re-verified against its current content
    immediately before writing; if it changed while the translation was in
    flight (an edit elsewhere, another process touching the file), the write
    is skipped and reported rather than blindly overwriting newer content.
Both :Translate and :TranslateReplace preserve each line's leading indentation
across the round trip: providers (notably Google's gtx endpoint) normalize
away leading whitespace, so a translated list item like     - foo would
otherwise come back as - bar with the indent dropped to column 0. The indent
is stripped before the request and re-applied to the matching output line
(skipped when the provider merges/splits lines, since the 1:1 mapping no longer
holds).

                                                                   *:Translate!*
:[range]Translate![lang]
    Open the interactive translation window (|language-translate-window|),
    prefilled from the range if given. The target is [lang], else
    translate.default_target, else a picker.

                                                          *language-translate-files*
:Translate <lang> cwd
:Translate <lang> path=<dir> [--files=<mode>]
:TranslateReplace <lang> cwd
:TranslateReplace <lang> path=<dir>
    Translate multiple files. Gathers translatable files under the directory
    (by translate.files.extensions, skipping .git/node_modules/…), opens a
    multi-select chooser (<Tab> to toggle, <CR> to confirm), then translates
    each picked file. Output per file:
        suffix   write a sibling  name.<LANG>.ext  (default, non-destructive)
        replace  overwrite the file in place (asks for confirmation first)
        buffers  open each translation in a scratch buffer (no disk write)
    :Translate … --files=<mode> overrides translate.files.output for one
    call; :TranslateReplace … cwd|path=<dir> always forces replace.
    Note: --nocode code-skipping applies to buffer/selection translation,
    not to whole-file translation. In replace mode, each file is
    re-verified against its on-disk content immediately before it is
    overwritten; a file that changed during its (sequential, one-at-a-time)
    translation is skipped rather than overwritten, and the final summary
    counts files actually written, not files picked. If the directory walk
    that gathers translatable files fails partway (permissions, a vanishing
    entry, …), the file list may be incomplete rather than "this directory
    has nothing to translate" going unremarked; a notify warning names the
    directory and the reason.

5. SCOPING *language-scoping*

Every action operates over an explicit scope, parsed once from the command
arguments:

    buffer      whole current buffer
    visible     only the visible window range (fast; good for live scanning)
    cwd         recursively across the working directory (always async)
    path=<p>    a file OR a directory (recursive)
    selection   a visual/line range (mainly for :Translate)

path=<p> expands ~ and environment variables only (not Vim's %/#/
<cfile>/<cword> specials, and no shell/backtick command substitution) —
exactly what a path typed on a command line needs, nothing more.

For cwd/path scopes with many files, an external CLI spell provider is
preferred (one process over the tree, fastest); otherwise the native scanner
recursively walks the directory on disk (not just already-open buffers),
async and chunked (20 files per tick) so the editor never freezes. Live
(unsaved) buffer content is used for files that happen to be open; closed
files are read fresh from disk. Vendor directories (.git, node_modules, .venv,
dist, build, target, .cache, __pycache__) are skipped, as are files above 5 MB
or spell.max_file_lines.

6. SPELL PROVIDERS *language-spell-providers*

    native      Neovim's own vim.spell (always available). Respects
                'spelllang'. Splits CamelCase/snake_case identifiers and only
                reports genuinely misspelled subwords; restricts checking to
                Treesitter @spell regions (comments/strings/prose) and skips
                URLs/emails. For cwd/path scopes it recursively scans every
                matching file on disk (see |language-scoping|), not only
                already-open buffers.
    typos       fast tree-wide scanning via the typos CLI (async), used for
                cwd/path scopes when available.
    cspell,     external CLIs (opt-in) for cwd/path scopes.
    codespell
    All three exit non-zero on a normal "found something" run, so a
    genuine failure (the process times out, never starts, or crashes) is
    told apart by producing no output at all rather than by its exit code;
    that case is reported via notify instead of showing as a clean 0-issue
    scan.
    cspell_server  a persistent Node sidecar that keeps cspell-lib loaded, for
                fast, code-aware, live buffer checks (needs node + cspell). Opt
                in by adding "cspell_server" to spell.providers.buffer. If it
                never starts, dies, or a check exceeds its ~6s guard, the
                other configured providers' results are still shown; a
                one-time-per-session notify warns that cspell's own
                contribution is missing, rather than that being silent.
    harper/ltex grammar diagnostics from a running language server are
                harvested and shown alongside spelling issues (kind = grammar).
    custom      escape hatch for a checker without a bundled adapter (cwd/path
                scopes). Configure `spell.providers.custom = { cmd = fun(scope,
                cfg) -> argv, parse = fun(out, base) -> partial-issue[] }` and
                add "custom" to spell.providers.cwd. parse returns a list of
                `{ path, lnum, col, end_col?, word, kind?, message?,
                suggestions? }; bufnr/source` are filled in automatically.
                Mirrors translate.custom. A cmd/parse that throws,
                returns the wrong shape, or whose entries are missing
                word/path is reported via notify rather than read as a
                clean (issue-free) scan.

Which providers run per scope is controlled by spell.providers (see
|language-config|).

7. TRANSLATE ENGINES *language-translate-engines*

    google      default, keyless (only curl). Modern gtx endpoint.
    deepl       official DeepL API. Key from translate.deepl.api_key or the
                DEEPL_API_KEY environment variable. Free keys (suffix ":fx")
                use api-free.deepl.com automatically.
    shell       the trans CLI (translate-shell).
    custom      any CLI: provide `translate.custom = { cmd = fun(lines,
                target, source) -> argv, parse = fun(stdout) -> lines }`.

translate.engine selects the engine; translate.fallback is a chain tried
when the chosen engine is unavailable (e.g. deepl without a key -> google).

                                                        *language-translate-maps*
Motion / visual mappings (opt-in via translate.keymaps):
    operator   <lhs>{motion} translates the moved-over text object, e.g.
               <lhs>ip for a paragraph or <lhs>iw for a word.
    visual     <lhs> translates the current visual selection.
Char-wise motions and char-wise visual selections translate and replace the
exact byte span (multibyte-safe); line-wise (V) uses the whole line range.
The target language is translate.default_target when set, otherwise chosen
from translate.default_langs via a picker. These are rewrite-in-place
operators (like gu/gq) — they always replace, independent of
translate.default_output (which only affects :Translate).
    require("language").setup({
      translate = {
        default_target = "DE",             -- skip the picker
        keymaps = { operator = "gtr", visual = "gtr" },
      },
    })

8. THE REVIEW PANEL *language-panel*

With spell.ui.view = "picker" (default), :Spellcheck opens an interactive
panel listing every issue. <CR> on an item opens its action menu:

Spelling issues:
    Choose suggestion…     pick a replacement (fuzzy)
    Replace all in buffer… apply a replacement to every occurrence
    Add to dictionary      zg-equivalent (persistent or session)
    Ignore (session)       drop this word for the session
    Ignore (persistent)    drop this word permanently (ignore file)
    Jump to location       go to the issue

Grammar/style issues (harper_ls/ltex):
    Apply LSP fix…         run the language server's code actions at the issue
    Ignore (session/persistent), Jump to location

The panel list itself also has direct keys for the actions above, so common
ones don't need the menu round-trip:
    a       Add to dictionary
    i / I   Ignore (session / persistent)
    L       Apply LSP fix (grammar/style issues)
    gd, o   Jump to location (closes the panel)
    ?       Show this cheatsheet
A key not applicable to the issue under cursor (e.g. a on a grammar issue)
is reported rather than silently ignored.

                                                       *language-translate-window*

Interactive translation window

:Translate! opens two stacked floats: an editable input (top) and a read-only
output (bottom) that translates as you type (debounced). Actions are NORMAL-mode
keys (press <Esc> first) so insert-mode typing keeps its native keys:
    <C-l>   change target language
    <C-r>   reverse: use the translation as the new input, then pick a target
            (round-trip / translate onward)
    <C-h>   open the translation-history picker
    <C-y>   copy the translation to the clipboard (also records to history)
    q,<Esc> close  (also <C-c> in insert mode)
The window uses the configured engine/fallback like every other translation.

                                                          *language-thesaurus*

Thesaurus / synonyms

require("language").synonyms() (or the opt-in thesaurus.keymap) looks up
synonyms for the word under the cursor and replaces it with your pick. The
default source is the free, keyless Datamuse API (English); set
thesaurus.source = "custom" with thesaurus.custom = fun(word, cb) for another
source or language.
    require("language").setup({
      thesaurus = { keymap = "<leader>sy" },  -- opt-in
    })
The word's span is re-verified right before the replacement is written; if
the text under it changed while the lookup/menu was open (an edit elsewhere,
another window), the replacement is discarded and reported instead of being
written over unrelated text. A lookup failure (the request fails, or
thesaurus.custom throws) is reported via notify rather than read as "no
synonyms for this word" -- the same distinction the CLI spell providers make
between a genuinely clean result and a broken one.

Live scanning

Set spell.live = true for always-on inline diagnostics that update as you
edit (debounced by scan_debounce_ms). It is decoupled from the panel and runs
only for spell.filetypes. With spell.live_scope = "visible" (default) it
scans just the visible window range and follows the viewport; set it to
"buffer" to scan the whole buffer. The max_file_lines, readonly and
max_highlights gates apply. Whole-buffer native scans are cached by
changedtick, so re-opening the panel or re-scanning an unchanged buffer is
instant.

                                                        *language-spell-silencing*

Silencing false positives

Besides the dictionary (zg / "Add to dictionary") and the ignore list, inline
directives silence spots without touching any list:
    language:disable-line        skip the line the directive is on
    language:disable-next-line    skip the following line
    language:disable-file         skip the whole buffer
Put them in a comment so they don't show in rendered output.

                                                        *language-spell-guard*

Block writing on errors (opt-in)

spell.guard.block_write_on_error = true aborts :w on spell.filetypes
buffers while spelling errors remain (grammar is advisory and never blocks).
Bypass a single write with :noautocmd w.

                                                        *language-spell-replace-all*
spell.dictionary.replace_all = true (default) makes "Choose suggestion…" in
the panel apply the pick to every occurrence of the word in the buffer; set it
to false to replace only the exact occurrence.

After each action the panel re-scans and re-opens. Set view = "quickfix" for
the classic diagnostics + quickfix/Trouble session flow with z= fixing.

                                                     *language-spell-highlights*

Buffer highlights (opt-in)

Visibility normally runs entirely through |vim.diagnostic|, whose appearance
follows the user's own vim.diagnostic.config(). Set
spell.highlights.enable = true to additionally mark issues directly in the
buffer via extmarks, independent of that config — useful when diagnostics
virtual text/underline is muted globally or filtered elsewhere. Two highlight
groups are defined (default = true, so a colorscheme or :hi can override
them): LanguageSpellHighlight (spelling/rare/caps) and
LanguageGrammarHighlight (grammar/style). spell.highlights.style selects
"underline" (default) or "undercurl".
    require("language").setup({
      spell = { highlights = { enable = true, style = "undercurl" } },
    })

9. CONFIGURATION *language-config*

Defaults (abridged; see lua/language/config/DEFAULTS.lua):
    require("language").setup({
      spell = {
        providers = {
          buffer = { "native", "lsp" },
          cwd    = { "typos", "native" },
          lsp    = { enable = true, servers = { "harper_ls", "ltex" } },
          custom = nil,  -- { cmd = fun(scope,cfg)->argv, parse = fun(out,base)->issues }
        },
        filetypes = { "markdown","text","gitcommit","tex","rst","asciidoc","help" },
        default_scope = "buffer",
        live = false, live_scope = "visible", scan_debounce_ms = 400,
        word_split = { enable = true, min_length = 4 },
        regions = { treesitter_spell = true, skip_urls = true, skip_emails = true },
        programming_dict = false,
        extra_wordlists = {},  -- { ["my-list"] = { "word1", ... } }, session-only
        max_highlights = 100, max_file_lines = 20000, skip_readonly = true,
        ui = { view = "picker", preview = true, group_by = "file", dedupe = true },
        dictionary = {
          ignore_file = vim.fn.stdpath("state") .. "/language/spell_ignore.txt",
          use_spellfile = true, replace_all = true,
        },
        highlights = { enable = false, style = "underline" },  -- opt-in buffer extmarks
        keymaps = { panel = "<leader>ss", next = "]s",
                    fix = "<leader>z=", fix1 = "<leader>z1" },
      },
      translate = {
        engine = "google", fallback = { "google" },
        default_output = "popup", default_input = "selection",
        nocode_default = false, timeout_ms = 8000,
        deepl = { api_key = nil },
        files = { output = "suffix" },  -- :Translate cwd/path=<dir> default
      },
      commands = true,
    })

10. HEALTH *language-health*

    :checkhealth language
Reports Neovim/lib.nvim availability, spell provider tools, grammar LSP
clients, curl and DeepL key status, and 'spelllang'.