Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 62 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ use {
```lua
require('comment-translate').setup({
target_language = 'ja', -- Target language (default: auto-detected from system locale, fallback 'en')
translate_service = 'google', -- Currently only 'google' is supported
translate_service = 'google', -- 'google' or 'llm'
hover = {
enabled = true, -- Enable hover translation
delay = 500, -- Additional delay (ms) after CursorHold before showing hover
Expand All @@ -109,6 +109,14 @@ require('comment-translate').setup({
comment = true, -- Include comments as translation targets
string = true, -- Include strings as translation targets
},
llm = {
provider = 'openai', -- 'openai' | 'anthropic' | 'gemini' | 'ollama'
api_key = nil, -- Required except provider='ollama' (can also use provider-specific env vars)
model = 'gpt-5.2',
endpoint = nil, -- Optional custom endpoint (default depends on provider)
system_prompt = nil, -- Optional custom system prompt
timeout = 20, -- curl max-time in seconds
},
keymaps = {
hover = '<leader>th', -- Hover translation
hover_manual = '<leader>tc', -- Manual hover trigger (when auto is disabled)
Expand All @@ -122,6 +130,52 @@ require('comment-translate').setup({

* **target_language**: Automatically detected from system locale (`LANG`, `LANGUAGE`, `LC_ALL`) or Vim language settings. Falls back to `'en'` if detection fails.
* **hover.delay**: Applied after the `CursorHold` event. Total delay is `updatetime` (Neovim option) plus `hover.delay`.
* **translate_service = 'llm'**: Supports `openai`, `anthropic`, `gemini`, and `ollama`.
* **API key env vars**:
* `openai`: `OPENAI_API_KEY`
* `anthropic`: `ANTHROPIC_API_KEY`
* `gemini`: `GEMINI_API_KEY`
* `ollama`: API key not required

### LLM Translation Example

```lua
require('comment-translate').setup({
translate_service = 'llm',
target_language = 'ja',
llm = {
provider = 'openai',
api_key = vim.env.OPENAI_API_KEY, -- or a literal key string
model = 'gpt-5.2',
},
})
```

### Provider Examples

```lua
-- Anthropic
llm = {
provider = 'anthropic',
model = 'claude-sonnet-4-0',
api_key = vim.env.ANTHROPIC_API_KEY,
}

-- Gemini
llm = {
provider = 'gemini',
model = 'gemini-2.5-flash',
api_key = vim.env.GEMINI_API_KEY,
}

-- Ollama (local)
llm = {
provider = 'ollama',
model = 'translategemma:4b',
endpoint = 'http://localhost:11434/api/chat', -- optional (default shown)
}

```

## Commands

Expand All @@ -137,7 +191,7 @@ require('comment-translate').setup({

This plugin prioritizes transparency. To provide real-time translation, text is sent to an external translation service.

* **External transmission**: Translation uses the unofficial Google Translate HTTP endpoint (`translate.googleapis.com`) via `curl`.
* **External transmission**: Translation sends text to the configured external service (`google` or `llm`) via `curl`.
* **What is sent**: The selected text or detected comment/string content. If it contains **personal data**, **credentials**, **internal code**, or other sensitive information, it may be transmitted outside your environment.
* **Cache behavior**: The built-in cache is **in-memory only**. No files are written by the plugin, and the cache is cleared when Neovim exits.
* **Control**: Automatic hover translation can be disabled, and all translation features are user-controlled.
Expand Down Expand Up @@ -177,6 +231,12 @@ make fmt
make fmt-check
```

* Run tests:

```sh
make test
```

## License

MIT
42 changes: 36 additions & 6 deletions doc/comment-translate.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,17 @@ REQUIREMENTS *comment-translate-requirements*
- nvim-treesitter (recommended)
- curl command (for translation API)
- Internet connection
- Translation uses the unofficial Google Translate HTTP endpoint via curl.
Network policy changes (proxy/firewall) or upstream API changes may break
translation; inform users in restricted environments.
- Translation uses the configured external service (`google` or `llm`) via
curl. Network policy changes (proxy/firewall) or upstream API changes may
break translation; inform users in restricted environments.

PRIVACY / DATA HANDLING *comment-translate-privacy*

{comment-translate.nvim} sends the text you translate (comments, strings, or
visual selections) to an external translation service over the network.

- External transmission: Translation uses the unofficial Google Translate HTTP
endpoint (translate.googleapis.com) via curl.
- External transmission: Translation sends text to the configured external
service (`google` or `llm`) via curl.
- What is sent: The target text content itself. If it contains personal data,
credentials, internal code, or other sensitive information, it may be sent
outside your environment.
Expand Down Expand Up @@ -106,9 +106,39 @@ Options:
Default: System locale or 'en'

*comment-translate.translate_service*
Translation service to use: currently only 'google' is available.
Translation service to use: 'google' or 'llm'.
Default: 'google'

*comment-translate.llm.provider*
LLM provider when translate_service = 'llm'.
Supported: 'openai', 'anthropic', 'gemini', 'ollama'
Default: 'openai'

*comment-translate.llm.api_key*
API key for LLM translation service.
Required for all providers except 'ollama'.
Environment variable fallback:
- openai: OPENAI_API_KEY
- anthropic: ANTHROPIC_API_KEY
- gemini: GEMINI_API_KEY
Default: nil

*comment-translate.llm.model*
Model name for LLM translation.
Default: 'gpt-5.2'

*comment-translate.llm.endpoint*
Optional endpoint override. If nil, provider-specific defaults are used.
Default: nil

*comment-translate.llm.system_prompt*
Optional system prompt for translation behavior.
Default: nil (plugin uses internal prompt)

*comment-translate.llm.timeout*
HTTP timeout (seconds) used by curl for LLM translation requests.
Default: 20

*comment-translate.hover.enabled*
Enable automatic hover translation.
Default: true
Expand Down
77 changes: 77 additions & 0 deletions lua/comment-translate/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
---@field cache CommentTranslateCacheConfig
---@field max_length number
---@field targets CommentTranslateTargetsConfig
---@field llm CommentTranslateLLMConfig

---@class CommentTranslateHoverConfig
---@field enabled boolean
Expand All @@ -23,13 +24,31 @@
---@field comment boolean
---@field string boolean

---@class CommentTranslateLLMConfig
---@field provider string
---@field api_key string?
---@field model string
---@field endpoint string?
---@field system_prompt string?
---@field timeout number

---@class CommentTranslateKeymapsConfig
---@field hover? string|false
---@field hover_manual? string|false Keymap for manual hover when auto-hover is disabled
---@field replace? string|false
---@field toggle? string|false

local M = {}
local SUPPORTED_SERVICES = {
google = true,
llm = true,
}
local SUPPORTED_LLM_PROVIDERS = {
openai = true,
anthropic = true,
gemini = true,
ollama = true,
}

---@return string
local function get_default_language()
Expand Down Expand Up @@ -67,6 +86,14 @@ local default_config = {
comment = true,
string = true,
},
llm = {
provider = 'openai',
api_key = nil,
model = 'gpt-5.2',
endpoint = nil,
system_prompt = nil,
timeout = 20,
},
keymaps = {
hover = '<leader>th',
hover_manual = '<leader>tc',
Expand Down Expand Up @@ -102,9 +129,21 @@ local function validate(user_config)
cache = { user_config.cache, 'table', true },
max_length = { user_config.max_length, 'number', true },
targets = { user_config.targets, 'table', true },
llm = { user_config.llm, 'table', true },
keymaps = { user_config.keymaps, 'table', true },
})

if user_config.translate_service and not SUPPORTED_SERVICES[user_config.translate_service] then
vim.notify(
string.format(
"comment-translate: unsupported translate_service '%s', defaulting to 'google'",
tostring(user_config.translate_service)
),
vim.log.levels.WARN
)
user_config.translate_service = 'google'
end

if user_config.hover then
warn_unknown('hover', user_config.hover, { enabled = true, delay = true, auto = true })
vim.validate({
Expand Down Expand Up @@ -145,6 +184,44 @@ local function validate(user_config)
})
end

if user_config.llm then
warn_unknown('llm', user_config.llm, {
provider = true,
api_key = true,
model = true,
endpoint = true,
system_prompt = true,
timeout = true,
})
vim.validate({
['llm.provider'] = { user_config.llm.provider, 'string', true },
['llm.api_key'] = { user_config.llm.api_key, 'string', true },
['llm.model'] = { user_config.llm.model, 'string', true },
['llm.endpoint'] = { user_config.llm.endpoint, 'string', true },
['llm.system_prompt'] = { user_config.llm.system_prompt, 'string', true },
['llm.timeout'] = { user_config.llm.timeout, 'number', true },
})

if user_config.llm.provider and not SUPPORTED_LLM_PROVIDERS[user_config.llm.provider] then
vim.notify(
string.format(
"comment-translate: unsupported llm.provider '%s', defaulting to 'openai'",
tostring(user_config.llm.provider)
),
vim.log.levels.WARN
)
user_config.llm.provider = 'openai'
end

if user_config.llm.timeout and user_config.llm.timeout <= 0 then
vim.notify(
'comment-translate: llm.timeout must be > 0, defaulting to 20',
vim.log.levels.WARN
)
user_config.llm.timeout = 20
end
end

if user_config.keymaps then
warn_unknown(
'keymaps',
Expand Down
35 changes: 35 additions & 0 deletions lua/comment-translate/health.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
---Run with :checkhealth comment-translate

local M = {}
local utils = require('comment-translate.utils')

---@param module_name string
---@return boolean
Expand Down Expand Up @@ -64,6 +65,40 @@ function M.check()
vim.health.ok('Plugin is configured')
vim.health.info('Target language: ' .. (config.config.target_language or 'not set'))
vim.health.info('Translate service: ' .. (config.config.translate_service or 'not set'))

if config.config.translate_service == 'llm' then
local provider = (config.config.llm and config.config.llm.provider) or 'openai'
vim.health.info('LLM provider: ' .. provider)

if provider == 'ollama' then
vim.health.ok('LLM API key is not required for ollama provider')
else
local env_keys = {
openai = { 'OPENAI_API_KEY' },
anthropic = { 'ANTHROPIC_API_KEY' },
gemini = { 'GEMINI_API_KEY' },
}
local configured = config.config.llm and config.config.llm.api_key
local has_key = configured and utils.trim(configured) ~= ''
if not has_key then
for _, env_name in ipairs(env_keys[provider] or {}) do
local value = vim.env[env_name]
if value and utils.trim(value) ~= '' then
has_key = true
break
end
end
end

if has_key then
vim.health.ok('LLM API key is configured')
else
vim.health.error('LLM API key is missing', {
'Set `llm.api_key` in setup() or required env var for provider',
})
end
end
end
else
vim.health.warn('Plugin setup() has not been called', {
"Call require('comment-translate').setup({}) in your config",
Expand Down
5 changes: 4 additions & 1 deletion lua/comment-translate/translate/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ local M = {}

M.SERVICES = {
google = 'google',
llm = 'llm',
}

---@param service_name? string
Expand All @@ -13,6 +14,8 @@ local function get_service(service_name)

if service_name == M.SERVICES.google then
return require('comment-translate.translate.google'), nil
elseif service_name == M.SERVICES.llm then
return require('comment-translate.translate.llm'), nil
else
return nil, 'Unknown translate service: ' .. tostring(service_name)
end
Expand Down Expand Up @@ -47,7 +50,7 @@ end

---@return string[]
function M.get_available_services()
return { M.SERVICES.google }
return { M.SERVICES.google, M.SERVICES.llm }
end

return M
Loading