Inline-first i18n status & review tooling for Neovim for i18next / next-intl projects
Stop guessing whether t("login.title") is safe.
See i18n health inline, inspect details on demand, and run safe edits.
t("login.title"): Login [=]
t("login.description"): Welcome back [≠]
t("login.button"): Login [?]
t("login.error"): {count} errors [!]
t("signup.title"): [×]| Marker | Meaning | When shown |
|---|---|---|
[=] |
All languages same | All languages exist and have identical values (may need translation) |
[≠] |
Localized | All languages exist but values differ |
[?] |
Fallback is used | Missing in some languages (but primary exists) |
[×] |
Missing in primary | Primary language value is missing |
[!] |
Placeholder mismatch | Placeholders differ between languages |
Priority: × > ! > ? > ≠ > =
Examples:
[=]:en: "Login",ja: "Login"(identical values - check if translated)[≠]:en: "Welcome back",ja: "おかえりなさい"(both exist, localized values)[?]:en: "Login",ja: missing(primary exists, other missing)[×]:en: missing,ja: "サインアップ"(primary missing)[!]:en: "{count} items",ja: "{name} 件"(different placeholders)
- ✅ Inline status: Lightweight extmarks, quiet by design.
- 🗂️ Translation file inline: When you open a resource JSON file, show another language inline (controlled by
:I18nLang). - 💬 Hover details: Values for all languages + reason + file path (git-relative when possible).
- 🔁 Language cycling: yankround-style next/prev + "back to previous".
- 🎯 Inline goto definition (opt-in): Map any keys (e.g.
gd) to jump straight to the translation file under the cursor. - 🩺 Doctor + Review: Diagnose project-wide issues and review/fix them in a two-pane floating UI where the left list drives every action and the right side stays as a live preview.
- ✂️ Interactive JSX extraction: Detect hardcoded JSX text and open a Review UI with diff preview, bulk selection, and one-shot apply.
- ⚡ Completion: blink.cmp source (first argument only), missing-first sorting.
- 🧩 Dynamic key resolution: const refs, template interpolations, ternary branches — resolved statically via Rust AST.
- 🔄 Auto reload: Translation file changes update inline quickly (watcher + cache).
- Neovim >= 0.12
- Tree-sitter (optional):
json/jsoncparsers for better resource editing UX - blink.cmp (optional): i18n key completion
Set up the plugin with minimal options, then configure language-cycling helpers and custom keymaps.
{
"mhiro2/i18n-status.nvim",
build = "./scripts/download-binary.sh",
config = function()
local i18n_status = require("i18n-status")
i18n_status.setup({
-- Source-of-truth language (used for inline rendering + doctor comparisons).
primary_lang = "en",
auto_hover = {
enabled = true, -- auto-show hover on cursor hold (default: true)
},
inline = {
-- Controls how inline virtual text is displayed. (JSON files always use "eol")
position = "eol", -- set to "after_key" to draw text directly after the key
},
})
end,
}download-binary.sh resolves binary version in this order:
I18N_STATUS_CORE_TAGenvironment variable- current plugin git tag (if
HEADis tagged) - latest GitHub Release (fallback)
If binary download prerequisites are missing or download fails, it falls back to cargo build --release.
If you prefer local build:
cd rust && cargo build --release-- Example keymaps:
vim.keymap.set("n", "<leader>in", "<Cmd>I18nLangNext<CR>", { desc = "Next language" })
vim.keymap.set("n", "<leader>ip", "<Cmd>I18nLangPrev<CR>", { desc = "Previous language" })
vim.keymap.set("n", "<leader>id", "<Cmd>I18nDoctor<CR>", { desc = "i18n doctor" })
vim.keymap.set("n", "<leader>ia", "<Cmd>I18nAddKey<CR>", { desc = "Add new i18n key" })
-- Inline goto-definition is opt-in. Configure it per-buffer so LSP mappings don't override it.
vim.api.nvim_create_autocmd("LspAttach", {
callback = function(args)
local bufnr = args.buf
vim.keymap.set("n", "gd", function()
if not require("i18n-status").goto_definition(bufnr) then
vim.lsp.buf.definition()
end
end, { buffer = bufnr, desc = "i18n-status: goto translation or LSP definition" })
end,
})Configure via require("i18n-status").setup({ ... }).
For full option and command details, see :help i18n-status.
Default Settings
{
primary_lang = "en",
inline = {
position = "eol", -- "eol" | "after_key"
max_len = 80,
visible_only = true,
status_only = false,
debounce_ms = 80,
hl = {
text = "Comment",
same = "I18nStatusSame",
diff = "I18nStatusDiff",
fallback = "I18nStatusFallback",
missing = "I18nStatusMissing",
mismatch = "I18nStatusMismatch",
},
},
resource_watch = {
enabled = true,
debounce_ms = 200,
},
doctor = {
ignore_keys = {},
float = {
width = 0.8, -- 0.0-1.0
height = 0.8, -- 0.0-1.0
border = "rounded", -- "none" | "single" | "double" | "rounded" | "solid" | "shadow"
},
},
auto_hover = {
enabled = true,
},
extract = {
min_length = 2,
exclude_components = { "Trans", "Translation" },
key_separator = "-", -- "." | "_" | "-"
},
}Resource roots are auto-detected from the current buffer's directory.
Namespace is inferred from useTranslation(s)/getTranslations or explicit ns:key.
If none is found, a best-effort fallback is used and :checkhealth will warn.
:I18nHover: Hover details for the i18n key under cursor:I18nGotoDefinition: Jump to the translation file for the i18n key under cursor:I18nDoctor: Diagnose i18n issues across the entire project and open Review UI:I18nDoctorCancel: Cancel a running doctor scan:I18nAddKey: Add a new i18n key to all language files interactively (writes per language; when a write fails mid-way, successful languages remain written):I18nExtract: Detect hardcoded JSX text and open the Extract Review UI (supports:'<,'>I18nExtractfor range extraction):I18nRefresh: Force refresh current buffer
:I18nLang [lang]: With no argument, same as:I18nLangNext. With argument, set language explicitly (warns if unknown). Supports command-line completion.:I18nLangNext/:I18nLangPrev: Cycle languages
Run :checkhealth i18n-status to verify core binary, configuration, resource discovery, optional Treesitter parsers, and blink.cmp integration.
Run :I18nDoctor to diagnose i18n issues across the entire project and open Review UI.
Warning
:I18nDoctor scans project files and can be slow on large codebases.
It is only run when you explicitly invoke it.
Doctor opens a floating window with two panes (left: key list, right: color-coded preview).
You can switch between two modes:
- Problems: issue-only view (fast, includes missing/mismatch/unused/drift issues)
- Overview: full key list (includes same/≠ status; heavier)
In Overview, = means the value matches the primary language, and ≠ means the value is localized (differs from the primary language; informational only).
Keymaps:
q/<Esc>: CloseTab: Toggle Problems/Overview/: Filter key list by key name (empty input clears)e: Edit display locale valueE: Select locale to editr: Rename key (updates resources + open buffers)a: Add missing key (only for missing primary keys)gd: Jump to definition file (Overview: open resource file)?: Toggle keymap help overlay
Tip
The list pane statusline mirrors the most common shortcuts so you can glance without opening the help overlay.
Review highlight groups:
- Layout:
I18nStatusReviewListNormal,I18nStatusReviewListCursorLine,I18nStatusReviewDetailNormal,I18nStatusReviewBorder - Text:
I18nStatusReviewHeader,I18nStatusReviewDivider,I18nStatusReviewMeta,I18nStatusReviewKey,I18nStatusReviewTableHeader - Status colors:
I18nStatusReviewStatusOk,...Missing,...Fallback,...Localized,...Mismatch,...Primary,...Focus,...StatusDefault
All groups are created with default=true and linked to Telescope/Diagnostic groups, so you can override them via :highlight if you prefer custom colors.
| Type | Meaning |
|---|---|
missing |
key is missing in the primary language |
mismatch |
placeholder mismatch between languages |
unused |
key exists in resources but is not referenced |
drift |
key differs across languages (missing/extra) |
resource errors |
invalid JSON or read errors |
roots missing |
no resource root found (locales/, public/locales/, or messages/) |
Run :I18nExtract (or :'<,'>I18nExtract for a range) to detect hardcoded JSX text and open a two-pane floating window: left for candidates, right for diff preview.
Keymaps:
q/<Esc>: Close<Space>: Toggle current candidate selection/: Filter candidates by key or source text (empty input clears)a: Select allA: Deselect allr: Edit key nameu: Reuse existing key (for conflicting keys)U: Switch back to new key mode<CR>: Apply selected candidates?: Toggle keymap help overlay
Candidates are unselected by default. Select targets with <Space>, then use <CR>.
The list pane keeps shortcut hints minimal (?:help, q:quit). Use ? to view the full keymap.
The diff preview shows both the source replacement and the resource file changes that will be written. When a key conflicts with an existing one, u lets you reuse it (source replacement only, no resource write).
Manual setup (recommended):
require("blink.cmp").setup({
sources = {
default = { "lsp", "path", "buffer", "i18n_status" },
providers = {
i18n_status = { name = "i18n-status", module = "i18n-status.blink" },
},
},
})Note
- Only completes the first argument of
t("...")/something.t("..."). - Namespace is inferred from
useTranslation("ns")/useTranslations("ns")/getTranslations("ns")scopes.
Default links (created with default=true):
I18nStatusSame->DiagnosticHintI18nStatusDiff->DiagnosticOkI18nStatusFallback->DiagnosticWarnI18nStatusMissing->DiagnosticErrorI18nStatusMismatch->DiagnosticError
Override example:
require("i18n-status").setup({
inline = {
hl = {
text = "Comment",
same = "I18nStatusSame",
diff = "I18nStatusDiff",
fallback = "I18nStatusFallback",
missing = "I18nStatusMissing",
mismatch = "I18nStatusMismatch",
},
},
})locales/{lang}/{namespace}.jsonpublic/locales/{lang}/{namespace}.json
messages/{lang}.json(root file)messages/{lang}/{namespace}.json(namespace file)
Note
Conflict precedence rules:
- In
next-intl, when bothmessages/{lang}.json(root file) andmessages/{lang}/{namespace}.jsonexist, the root file is prioritized. Actions like "Add missing" and "Extract" will write to the root file in this case. - When multiple i18n roots coexist, key conflict precedence (
lang+ key) is:i18nextnamespace file (locales/..., priority 30)next-intlroot file (messages/{lang}.json, priority 40)next-intlnamespace file (messages/{lang}/{namespace}.json, priority 50)
- Resource indexing and cross-file resolve target strict
*.jsonfiles.jsoncis supported as an editor filetype UX improvement, but JSONC comments/trailing commas are not indexed.
Supported:
- String literals
- Literal concatenation (e.g.
"a" + "b") - Template literals (e.g.
`a.b`) - Template literals with const interpolations (e.g.
`${prefix}.key`whereprefixis aconst) conststring references in the same scope- TypeScript type assertions (
"key" as const,"key" satisfies string,key!) - Conditional (ternary) expressions — both branches are emitted (e.g.
cond ? "a" : "b"→ 2 keys)
Not supported:
- Runtime-dependent values
- Expressions with function calls
- Conditional const bindings (
const K = cond ? "a" : "b"; t(K)— uset(cond ? "a" : "b")directly instead)
MIT License. See LICENSE.





