A Neovim plugin that displays inline translations for React i18n keys using virtual text overlays.
- π Inline Translation Preview: See translations directly in your code
- π― Auto Path Detection: Automatically finds your locale files - zero configuration needed!
- π¨ Flexible Display Modes: Choose between overlay (replaces text) or end-of-line display
- β‘ Customizable Auto-Commands: Configure when translations appear (CursorHold, CursorMoved, etc.)
- π High Performance: JSON caching, debounced updates, visible-lines-only processing
- π Dynamic Locale Switching: Change locales on the fly
- π¦ Multi-Framework Support: React i18next, Vue i18n, Svelte, and more
- π§ Flexible Patterns: With/without namespace, Trans components, pluralization
- π― Smart Truncation: Handles long translations and Unicode properly
- π’ Advanced Key Formats: Supports numbers, nested paths, special characters
Using lazy.nvim:
-- In your plugins directory (e.g., ~/.config/nvim/lua/plugins/i18n-view.lua)
return {
"asce4s/i18n-view.nvim",
config = function()
require("i18n-view").setup({
locale = "en", -- default locale
path = "public/locales", -- path to locale files (relative to cwd)
default_namespace = "common", -- default namespace for keys without namespace
verbose = false, -- show error messages
prefix = "π ", -- prefix for virtual text
debounce_ms = 150, -- debounce delay
max_text_length = 80, -- truncate long translations
highlight = {
fg = "#a9b1d6",
bg = "#1a1b26",
},
})
end,
}Using packer.nvim:
use {
"asce4s/i18n-view.nvim",
config = function()
require("i18n-view").setup({
locale = "en",
path = "public/locales",
})
end
}The plugin can automatically detect your locale file location! It searches common i18n directory patterns:
project/
βββ public/locales/ β Automatically detected
βββ locales/ β Automatically detected
βββ i18n/ β Automatically detected
βββ src/locales/ β Automatically detected
βββ ... and 10+ more patterns
π Learn more about Auto-Detection - Detailed guide on how auto-detection works
Each locale directory should contain language subdirectories with JSON files:
public/locales/
βββ en/
β βββ common.json
β βββ home.json
β βββ auth.json
βββ es/
β βββ common.json
β βββ home.json
β βββ auth.json
βββ fr/
βββ ...
You can also specify a custom path:
require("i18n-view").setup({
path = "custom/path/to/locales",
auto_detect_path = false, -- Disable auto-detection
})Example translation file (public/locales/en/common.json):
{
"welcome": "Welcome to our app",
"buttons": {
"submit": "Submit",
"cancel": "Cancel"
},
"user": {
"profile": {
"name": "Name"
}
}
}The plugin automatically detects a wide variety of i18n patterns across multiple frameworks.
π View Complete Pattern Reference - Comprehensive documentation of all 50+ supported patterns
// Standard function call with namespace
t("common:welcome");
t("common:buttons.submit");
t('namespace:key.with.numbers0.test');
// Template literals with namespace
t(`common:welcome`);
// i18n object methods with namespace
i18n.t("common:welcome");
i18n.t(`common:welcome`);
// i18next object methods with namespace
i18next.t("common:welcome");
i18next.t(`common:welcome`);
// JSX expressions with namespace
{t("common:welcome")}
{t(`common:welcome`)}
// Function calls with options
t('common:key', { defaultValue: 'Hello' });
t('common:key', { count: 5 });
// Trans component
<Trans i18nKey="common:welcome" />
<Trans i18nKey={"common:key"} />When keys don't include a namespace (no :), the plugin uses the default_namespace config option (default: "common"):
// Simple keys (uses default namespace)
t("welcome");
t("buttons.submit");
t(`greeting`);
// With i18n object
i18n.t("welcome");
i18next.t("buttons.submit");
// JSX expressions
{t("welcome")}
// Trans component
<Trans i18nKey="welcome" /><!-- Vue i18n with namespace -->
{{ $t('common:welcome') }}
{{ $t(`common:buttons.submit`) }}
<!-- Vue i18n without namespace -->
{{ $t('welcome') }}
{{ $t('buttons.submit') }}
<!-- Vue i18n pluralization -->
{{ $tc('common:items', count) }}
{{ $tc('items', 5) }}<!-- Svelte i18n with namespace -->
{$_('common:welcome')}
{$_(`common:buttons.submit`)}
<!-- Svelte i18n without namespace -->
{$_('welcome')}
{$_('buttons.submit')}// Gettext-style with namespace
_('common:welcome');
_(`common:buttons.submit`);
// Gettext-style without namespace
_('welcome');
_('buttons.submit');// Multi-line with namespace
t(
'common:welcome'
)
i18n.t(
"common:buttons.submit"
)
// Multi-line without namespace
t(
'welcome'
)
// Vue patterns multi-line
$t(
'common:greeting'
)
// Svelte patterns multi-line
{$_(
'common:welcome'
)}The plugin supports keys with:
- Nested paths:
user.profile.name - Numbers:
items.0.title,section2.header - Hyphens:
my-key,button-text - Underscores:
my_key,button_text - Mixed formats:
user_profile.settings-menu.item0
The plugin works with a wide variety of i18n libraries and frameworks:
| Framework | Pattern Examples | Support |
|---|---|---|
| React i18next | t(), i18n.t(), i18next.t(), <Trans> |
β Full |
| Vue i18n | $t(), $tc() |
β Full |
| Svelte i18n | {$_()} |
β Full |
| Next.js | next-i18next patterns |
β Full |
| Gettext-style | _() |
β Full |
| Custom | Configurable patterns | β Extensible |
The plugin automatically detects 50+ pattern variations including:
- β
With/without namespace separation (
:) - β Single/double quotes and template literals
- β Function calls with options objects
- β JSX expressions and components
- β Multi-line function calls
- β Nested key paths with numbers
- β Special characters (hyphens, underscores)
// Your React component
import { useTranslation } from "react-i18next";
export function HomePage() {
const { t } = useTranslation();
return (
<div>
<h1>{t("home:title")}</h1> {/* Shows: π Home Page */}
<p>{t("home:description")}</p> {/* Shows: π Welcome to... */}
<button>{t("common:buttons.submit")}</button> {/* Shows: π Submit */}
</div>
);
}When you hover (cursor hold) over these lines, the translation keys will be replaced with the actual translations from your JSON files.
| Command | Description |
|---|---|
:I18nToggle |
Enable/disable the plugin |
:I18nDisplayMode |
Toggle between overlay and eol modes |
:I18nDisplayMode <mode> |
Set display mode (overlay or eol) |
:I18nLocale <locale> |
Switch to a different locale (e.g., :I18nLocale es) |
:I18nReload |
Reload translations (clears cache) |
:I18nClearCache |
Clear the translation cache |
:I18nStatus |
Show current configuration and stats |
:I18nVerbose |
Toggle verbose error messages |
Add these to your keymaps.lua:
vim.keymap.set("n", "<leader>it", ":I18nToggle<CR>", { desc = "Toggle i18n view" })
vim.keymap.set("n", "<leader>id", ":I18nDisplayMode<CR>", { desc = "Toggle display mode" })
vim.keymap.set("n", "<leader>ir", ":I18nReload<CR>", { desc = "Reload i18n" })
vim.keymap.set("n", "<leader>is", ":I18nStatus<CR>", { desc = "i18n status" })Quick toggle display mode in normal mode:
vim.keymap.set("n", "<leader>im", ":I18nDisplayMode<CR>", { desc = "Toggle overlay/eol mode" }){
enabled = true, -- Enable plugin on startup
locale = "en", -- Default locale
path = "public/locales", -- Base path to locale files (or leave empty for auto-detection)
auto_detect_path = true, -- Automatically detect locale paths in project
default_namespace = "common", -- Default namespace when not specified
prefix = "π ", -- Prefix for virtual text
verbose = false, -- Show error messages
debounce_ms = 150, -- Debounce delay (ms)
max_text_length = 80, -- Max translation length before truncation
cache_enabled = true, -- Enable JSON caching
display_mode = "overlay", -- "overlay" (replaces text) or "eol" (end-of-line)
highlight = {
fg = "#a9b1d6", -- Foreground color
bg = "#1a1b26", -- Background color
},
-- Auto-command configuration
auto_commands = {
-- Events that trigger showing translations
show_events = { "CursorHold" }, -- Can also include: "CursorMoved", "CursorHoldI", "BufEnter", etc.
-- Events that trigger clearing translations
clear_events = { "InsertEnter", "BufLeave" }, -- Can also include: "CursorMoved", "TextChanged", etc.
-- File patterns to watch
file_patterns = { "*.tsx", "*.ts", "*.jsx", "*.js", "*.vue", "*.svelte" },
},
-- Auto-detection search paths (used when auto_detect_path is true)
search_paths = {
"public/locales",
"locales",
"locale",
"translations",
"i18n",
"lang",
"languages",
"src/locales",
"src/locale",
"src/i18n",
"src/translations",
"assets/i18n",
"assets/locales",
"resources/i18n",
"resources/locales",
},
-- Advanced: Custom patterns (Lua patterns)
-- The plugin includes comprehensive default patterns for:
-- - React i18next (t(), i18n.t(), i18next.t())
-- - Vue i18n ($t(), $tc())
-- - Trans components
-- - With/without namespaces
-- You can add more patterns here if needed:
patterns = {
-- Default patterns are already extensive
-- Add your own custom patterns here if needed
},
}π Complete Display Modes Guide - Detailed guide with examples and presets
The plugin offers two display modes for showing translations:
Replaces the translation key with the actual translation text inline:
// Before (what you write):
t('common:welcome')
// After (what you see with overlay):
π Welcome to our appBest for: Quick preview while maintaining code structure
Shows translation at the end of the line:
// What you see:
t('common:welcome') π Welcome to our appBest for: Keeping original code visible while showing translations
require("i18n-view").setup({
display_mode = "overlay", -- or "eol"
})You can change display mode without restarting Neovim:
:I18nDisplayMode " Toggle between overlay and eol
:I18nDisplayMode overlay " Set to overlay mode
:I18nDisplayMode eol " Set to end-of-line modeThis is useful for switching contexts:
- Writing code: Use
overlayfor minimal visual clutter - Reviewing translations: Use
eolto see both key and translation - Editing keys: Use
eolto keep keys visible
Control when and how translations appear by customizing the auto-commands:
require("i18n-view").setup({
auto_commands = {
show_events = { "CursorHold" }, -- Show when cursor stops moving
},
})require("i18n-view").setup({
auto_commands = {
show_events = { "CursorMoved", "CursorMovedI" }, -- Show immediately on movement
},
})require("i18n-view").setup({
auto_commands = {
show_events = { "BufEnter", "BufWinEnter" }, -- Show when opening file
},
})require("i18n-view").setup({
auto_commands = {
show_events = { "CursorHold", "CursorMoved", "BufEnter" },
},
})require("i18n-view").setup({
auto_commands = {
show_events = { "CursorHold" },
clear_events = { "InsertEnter", "BufLeave", "CursorMoved" }, -- Clear on these events
},
})require("i18n-view").setup({
auto_commands = {
file_patterns = { "*.tsx", "*.ts", "*.vue" }, -- Only TypeScript and Vue files
},
})Minimal (show rarely):
auto_commands = {
show_events = { "CursorHold" }, -- Only when cursor stops
clear_events = { "InsertEnter" }, -- Only clear in insert mode
}Aggressive (show always):
auto_commands = {
show_events = { "CursorMoved", "CursorMovedI", "BufEnter", "CursorHold" },
clear_events = {}, -- Never clear automatically
}Balanced (recommended for most):
auto_commands = {
show_events = { "CursorHold" },
clear_events = { "InsertEnter", "BufLeave" },
}- Path Detection: Automatically finds locale directories in your project (or uses configured path)
- Event Triggering: Listens for configured events (CursorHold, CursorMoved, etc.)
- Pattern Matching: Extracts file name and key path (e.g.,
common:buttons.submit) - File Resolution: Tries multiple locale paths until file is found
- JSON Loading: Loads and caches translation file
- Virtual Text: Displays translation based on display_mode (overlay or eol)
- Smart Caching: Caches both JSON data and resolved file paths
- Caching: JSON files are cached after first load (10-100x faster)
- Visible Lines Only: Only processes lines currently visible in window
- Debouncing: Prevents excessive redraws (configurable)
- Lazy Loading: Files loaded on-demand, not all at once
Run :I18nStatus to see which locale paths were auto-detected:
i18n-view Status:
Enabled: true
Locale: en
Configured Path: public/locales
Auto-detect: true
Cache entries: 3
Resolved paths: 2
Detected locale paths:
1. public/locales
2. src/i18n
- Check detected paths: Run
:I18nStatusto verify locale paths were found - Enable verbose mode:
:I18nVerboseto see detailed error messages - Force path detection: Run
:I18nReloadto re-scan for locale directories - Manual configuration: If auto-detection fails, set path manually:
require("i18n-view").setup({ path = "your/locale/path", auto_detect_path = false, })
- Check locale:
:I18nLocale ento set correct locale - Test pattern: Make sure your code matches a supported pattern
- Hover longer: Wait for
updatetime(default 4000ms = 4s)- Reduce it:
:set updatetime=500in your config
- Reduce it:
- Clear cache:
:I18nClearCache - Reload:
:I18nReload
- Check current locale:
:I18nStatus - Switch locale:
:I18nLocale <locale>
Add to your options.lua:
vim.opt.updatetime = 500 -- Trigger CursorHold after 500ms (default: 4000ms)Feel free to submit issues and enhancement requests!
MIT