Skip to content

Commit 8fe816f

Browse files
Developerclaude
andcommitted
feat: translate.nvim — minimal translation plugin for Neovim
DeepL and Google Cloud Translation API support with floating window output, engine/target language switching, state persistence, parallel chunking, and race guard for rapid re-translations. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
0 parents  commit 8fe816f

35 files changed

+4121
-0
lines changed

.github/workflows/headless.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Headless Tests
2+
3+
on:
4+
push:
5+
pull_request:
6+
workflow_dispatch:
7+
8+
jobs:
9+
test:
10+
runs-on: ${{ matrix.os }}
11+
strategy:
12+
fail-fast: false
13+
matrix:
14+
os:
15+
- ubuntu-latest
16+
- macos-latest
17+
steps:
18+
- name: Checkout
19+
uses: actions/checkout@v4
20+
21+
- name: Setup Neovim
22+
uses: rhysd/action-setup-vim@v1
23+
with:
24+
neovim: true
25+
version: stable
26+
27+
- name: Run headless tests
28+
run: nvim --headless -u NONE -i NONE "+set rtp+=." "+lua require('tests.headless.run_all').run()" "+qa!"

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.DS_Store
2+
tmp/
3+
.claude/
4+
AGENTS.md

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 translate.nvim contributors
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
# translate.nvim
2+
3+
Minimal translation plugin for Neovim with DeepL and Google Cloud Translation engines.
4+
5+
![Neovim](https://img.shields.io/badge/Neovim-0.10%2B-green?logo=neovim)
6+
![License](https://img.shields.io/badge/License-MIT-blue)
7+
![Lua](https://img.shields.io/badge/Lua-blue?logo=lua)
8+
9+
## Features
10+
11+
- Translate visual selections with a single keymap
12+
- DeepL and Google Cloud Translation API v2 support
13+
- Floating window output with centered engine title in border
14+
- Target language picker (`vim.ui.select`) per engine
15+
- Engine switcher with automatic target language normalization
16+
- Persist last-used engine and target language across sessions
17+
- Preserve original line structure: blank lines and leading indentation are kept intact
18+
- Parallel chunking for large selections (50 lines per request)
19+
- Race guard: rapid re-translations cancel stale in-flight requests
20+
- Security: API keys are passed via curl stdin (`--config -`), never as CLI arguments
21+
22+
## Requirements
23+
24+
- Neovim 0.10+ (`vim.system()` required)
25+
- `curl` in `$PATH`
26+
- At least one API key:
27+
- **DeepL**: `DEEPL_AUTH_KEY` environment variable or `setup({ api_key = "..." })`
28+
- **Google**: `GOOGLE_TRANSLATE_API_KEY` or `GOOGLE_API_KEY` environment variable, or `setup({ google_api_key = "..." })`
29+
30+
## Installation
31+
32+
### lazy.nvim
33+
34+
```lua
35+
{
36+
"addsalt1t/translate.nvim",
37+
opts = {
38+
api_key = vim.env.DEEPL_AUTH_KEY,
39+
},
40+
}
41+
```
42+
43+
### packer.nvim
44+
45+
```lua
46+
use({
47+
"addsalt1t/translate.nvim",
48+
config = function()
49+
require("translate").setup({
50+
api_key = vim.env.DEEPL_AUTH_KEY,
51+
})
52+
end,
53+
})
54+
```
55+
56+
### Local Development
57+
58+
```lua
59+
{
60+
dir = "/path/to/translate.nvim",
61+
name = "translate.nvim",
62+
config = function()
63+
require("translate").setup()
64+
end,
65+
}
66+
```
67+
68+
## Configuration
69+
70+
The plugin auto-calls `setup()` on load with sensible defaults. Override any option as needed.
71+
72+
### Default configuration
73+
74+
```lua
75+
require("translate").setup({
76+
api_key = vim.env.DEEPL_AUTH_KEY,
77+
google_api_key = vim.env.GOOGLE_TRANSLATE_API_KEY or vim.env.GOOGLE_API_KEY,
78+
engine = "deepl",
79+
engine_labels = {
80+
deepl = "DeepL",
81+
google = "Google",
82+
},
83+
free_api = true,
84+
default_target = "KO",
85+
persist_target = true,
86+
state_path = vim.fs.normalize(vim.fn.stdpath("state") .. "/translate.nvim/state.json"),
87+
keymaps = {
88+
translate_visual = "<Space>tr",
89+
select_target = "<Space>tl",
90+
select_engine = "<Space>te",
91+
},
92+
float = {
93+
border = "rounded",
94+
winhighlight = "NormalFloat:Normal,FloatBorder:Normal",
95+
size_base = "window",
96+
width_ratio = 1.0,
97+
height_ratio = 1.0,
98+
width_offset = 0,
99+
height_offset = 0,
100+
min_width = 40,
101+
min_height = 8,
102+
inherit_view = true,
103+
center_vertical = false,
104+
-- width = 100, -- absolute override (skips ratio calculation)
105+
-- height = 14, -- absolute override (skips ratio calculation)
106+
},
107+
})
108+
```
109+
110+
### Option reference
111+
112+
| Option | Type | Default | Description |
113+
|---|---|---|---|
114+
| `api_key` | `string?` | `$DEEPL_AUTH_KEY` | DeepL API authentication key |
115+
| `google_api_key` | `string?` | `$GOOGLE_TRANSLATE_API_KEY` | Google Cloud Translation API key |
116+
| `engine` | `string` | `"deepl"` | Active translation engine (`"deepl"` or `"google"`) |
117+
| `engine_labels` | `table` | `{ deepl="DeepL", google="Google" }` | Display labels for engine names |
118+
| `free_api` | `boolean` | `true` | Use DeepL free API endpoint (`api-free.deepl.com`) |
119+
| `default_target` | `string` | `"KO"` | Fallback target language when current is invalid for engine |
120+
| `persist_target` | `boolean` | `true` | Save engine and target language to disk between sessions |
121+
| `state_path` | `string` | `stdpath("state").."/translate.nvim/state.json"` | Absolute path for persisted state file |
122+
| `keymaps.translate_visual` | `string` | `"<Space>tr"` | Keymap to translate visual selection |
123+
| `keymaps.select_target` | `string` | `"<Space>tl"` | Keymap to open target language picker |
124+
| `keymaps.select_engine` | `string` | `"<Space>te"` | Keymap to open engine picker |
125+
126+
### Float window options
127+
128+
| Option | Type | Default | Description |
129+
|---|---|---|---|
130+
| `float.border` | `string` | `"rounded"` | Border style (see `:h nvim_open_win`) |
131+
| `float.winhighlight` | `string` | `"NormalFloat:Normal,FloatBorder:Normal"` | Window highlight groups |
132+
| `float.size_base` | `string` | `"window"` | Base dimensions from `"window"` or `"editor"` |
133+
| `float.width_ratio` | `number` | `1.0` | Width as fraction of base (0.0-1.0) |
134+
| `float.height_ratio` | `number` | `1.0` | Height as fraction of base (0.0-1.0) |
135+
| `float.width_offset` | `number` | `0` | Additive columns after ratio calculation |
136+
| `float.height_offset` | `number` | `0` | Additive rows after ratio calculation |
137+
| `float.min_width` | `integer` | `40` | Minimum window width in columns |
138+
| `float.min_height` | `integer` | `8` | Minimum window height in rows |
139+
| `float.width` | `integer?` | `nil` | Absolute width override (skips ratio) |
140+
| `float.height` | `integer?` | `nil` | Absolute height override (skips ratio) |
141+
| `float.inherit_view` | `boolean` | `true` | Copy `tabstop`, `shiftwidth`, etc. from source window |
142+
| `float.center_vertical` | `boolean` | `false` | Vertically center short text in the float |
143+
144+
> **Deprecated:** `max_width_ratio` and `max_height_ratio` are silently migrated to `width_ratio`/`height_ratio`.
145+
146+
### Engine selection priority
147+
148+
The active engine is resolved in this order:
149+
150+
1. Explicit `engine` option passed to `setup()`
151+
2. Saved engine from state file (if `persist_target = true`)
152+
3. First-run auto-prefer: `"google"` when both API keys are present and no saved state
153+
4. Base default: `"deepl"`
154+
155+
## Translation Engines
156+
157+
### Provider comparison
158+
159+
| | DeepL | Google Cloud Translation |
160+
|---|---|---|
161+
| API | DeepL API v2 | Cloud Translation API v2 |
162+
| Target languages | 35 | 109 |
163+
| API key env var | `DEEPL_AUTH_KEY` | `GOOGLE_TRANSLATE_API_KEY` / `GOOGLE_API_KEY` |
164+
| Auth method | `Authorization` header | `X-Goog-Api-Key` header |
165+
| Free tier | `free_api = true` (default) | N/A (pay-per-use) |
166+
| Max texts/request | 50 | 50 |
167+
| Language codes | Region-specific (e.g. `EN-US`, `PT-BR`) | Simple (e.g. `EN`, `PT`) |
168+
169+
### Engine switching
170+
171+
When switching engines via `:TranslateSelectEngine` or `set_engine()`:
172+
173+
- The current target language is normalized and validated for the new engine
174+
- If the target is unsupported, it falls back to `default_target`
175+
- Language code aliases are applied automatically (e.g. `EN``EN-US` for DeepL)
176+
177+
## Keymaps
178+
179+
| Keymap | Mode | Action | Default |
180+
|---|---|---|---|
181+
| `keymaps.translate_visual` | `x` | Translate visual selection | `<Space>tr` |
182+
| `keymaps.select_target` | `n`, `x` | Open target language picker | `<Space>tl` |
183+
| `keymaps.select_engine` | `n`, `x` | Open engine picker | `<Space>te` |
184+
185+
Set any keymap to `""` (empty string) to disable it.
186+
187+
## Commands
188+
189+
| Command | Description |
190+
|---|---|
191+
| `:TranslateSelectTarget` | Open target language picker for current engine |
192+
| `:TranslateSelectEngine` | Switch translation engine (`deepl` / `google`) |
193+
194+
## Lua API
195+
196+
```lua
197+
local translate = require("translate")
198+
199+
-- Setup with options (auto-called on plugin load)
200+
translate.setup(opts?)
201+
202+
-- Translate current visual selection and show result in float
203+
translate.translate_visual()
204+
205+
-- Open target language picker (vim.ui.select)
206+
translate.select_target()
207+
208+
-- Open engine picker (vim.ui.select)
209+
translate.select_engine()
210+
211+
-- Set target language programmatically
212+
translate.set_target(code) -- e.g. "EN", "JA", "KO"
213+
214+
-- Set engine programmatically
215+
translate.set_engine(engine) -- "deepl" or "google"
216+
217+
-- Query current state
218+
translate.current_target() -- returns e.g. "KO"
219+
translate.current_engine() -- returns e.g. "deepl"
220+
```
221+
222+
## Health Check
223+
224+
```vim
225+
:checkhealth translate
226+
```
227+
228+
Checks: Neovim version (0.10+), `curl` availability, API key environment variables.
229+
230+
## Testing
231+
232+
Run the headless regression test suite:
233+
234+
```bash
235+
nvim --headless -u NONE -i NONE "+set rtp+=." "+lua require('tests.headless.run_all').run()" "+qa!"
236+
```
237+
238+
## License
239+
240+
MIT

doc/tags

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
:TranslateSelectEngine translate.txt /*:TranslateSelectEngine*
2+
:TranslateSelectTarget translate.txt /*:TranslateSelectTarget*
3+
translate-api translate.txt /*translate-api*
4+
translate-commands translate.txt /*translate-commands*
5+
translate-config translate.txt /*translate-config*
6+
translate-deepl translate.txt /*translate-deepl*
7+
translate-defaults translate.txt /*translate-defaults*
8+
translate-engine-priority translate.txt /*translate-engine-priority*
9+
translate-engines translate.txt /*translate-engines*
10+
translate-float translate.txt /*translate-float*
11+
translate-float-window translate.txt /*translate-float-window*
12+
translate-google translate.txt /*translate-google*
13+
translate-health translate.txt /*translate-health*
14+
translate-install translate.txt /*translate-install*
15+
translate-intro translate.txt /*translate-intro*
16+
translate-keymaps translate.txt /*translate-keymaps*
17+
translate-multiline translate.txt /*translate-multiline*
18+
translate-opt-api_key translate.txt /*translate-opt-api_key*
19+
translate-opt-default_target translate.txt /*translate-opt-default_target*
20+
translate-opt-engine translate.txt /*translate-opt-engine*
21+
translate-opt-engine_labels translate.txt /*translate-opt-engine_labels*
22+
translate-opt-float.border translate.txt /*translate-opt-float.border*
23+
translate-opt-float.center_vertical translate.txt /*translate-opt-float.center_vertical*
24+
translate-opt-float.height translate.txt /*translate-opt-float.height*
25+
translate-opt-float.height_offset translate.txt /*translate-opt-float.height_offset*
26+
translate-opt-float.height_ratio translate.txt /*translate-opt-float.height_ratio*
27+
translate-opt-float.inherit_view translate.txt /*translate-opt-float.inherit_view*
28+
translate-opt-float.min_height translate.txt /*translate-opt-float.min_height*
29+
translate-opt-float.min_width translate.txt /*translate-opt-float.min_width*
30+
translate-opt-float.size_base translate.txt /*translate-opt-float.size_base*
31+
translate-opt-float.width translate.txt /*translate-opt-float.width*
32+
translate-opt-float.width_offset translate.txt /*translate-opt-float.width_offset*
33+
translate-opt-float.width_ratio translate.txt /*translate-opt-float.width_ratio*
34+
translate-opt-float.winhighlight translate.txt /*translate-opt-float.winhighlight*
35+
translate-opt-free_api translate.txt /*translate-opt-free_api*
36+
translate-opt-google_api_key translate.txt /*translate-opt-google_api_key*
37+
translate-opt-keymaps translate.txt /*translate-opt-keymaps*
38+
translate-opt-persist_target translate.txt /*translate-opt-persist_target*
39+
translate-opt-state_path translate.txt /*translate-opt-state_path*
40+
translate-options translate.txt /*translate-options*
41+
translate-reqs translate.txt /*translate-reqs*
42+
translate-state translate.txt /*translate-state*
43+
translate.current_engine() translate.txt /*translate.current_engine()*
44+
translate.current_target() translate.txt /*translate.current_target()*
45+
translate.nvim-contents translate.txt /*translate.nvim-contents*
46+
translate.nvim.txt translate.txt /*translate.nvim.txt*
47+
translate.select_engine() translate.txt /*translate.select_engine()*
48+
translate.select_target() translate.txt /*translate.select_target()*
49+
translate.set_engine() translate.txt /*translate.set_engine()*
50+
translate.set_target() translate.txt /*translate.set_target()*
51+
translate.setup() translate.txt /*translate.setup()*
52+
translate.translate_visual() translate.txt /*translate.translate_visual()*

0 commit comments

Comments
 (0)