DEV Community

Cover image for Neovim translate popup
Sérgio Araújo
Sérgio Araújo

Posted on

Neovim translate popup

Neovim Translation with translate-shell and Custom Popup

A guide to adding quick command-line translation in Neovim, with a visual popup styled like traditional translators.

Creators: opencode and Sérgio Araújo


Prerequisites

1. Install translate-shell

translate-shell is a command-line translator powered by Google Translate, Bing, Yandex, and more.

Direct download:

wget git.io/trans
chmod +x ./trans
sudo mv trans /usr/local/bin/
Enter fullscreen mode Exit fullscreen mode

Or via git:

git clone https://github.com/soimort/translate-shell
cd translate-shell && make && sudo make install
Enter fullscreen mode Exit fullscreen mode

Dependencies: gawk (4.0+) and bash or zsh (usually pre-installed on Linux).


Neovim Implementation

1. Add the translation function

In ~/.config/nvim/lua/core/utils/nvim_utils.lua, add the translate_popup function:

function M.translate_popup()
  local text = ''
  local is_multiline = false
  local mode = vim.api.nvim_get_mode().mode

  if mode == 'v' or mode == 'V' or mode == '\x16' then
    local start_pos = vim.api.nvim_buf_get_mark(0, '<')
    local end_pos = vim.api.nvim_buf_get_mark(0, '>')
    local lines = vim.api.nvim_buf_get_lines(0, start_pos[1] - 1, end_pos[1], false)
    if #lines > 1 then
      is_multiline = true
      text = table.concat(lines, '\n')
    else
      text = table.concat(lines, ' ')
    end
  else
    text = vim.fn.expand('<cword>') or ''
  end

  if text == '' then
    return
  end

  local ok, trans = pcall(function()
    local handle = io.popen('trans -b -no-ansi en:pt-BR ' .. vim.fn.shellescape(text))
    if not handle then return nil end
    local result = handle:read('*a')
    handle:close()
    return result
  end)

  if not ok or not trans or trans == '' then
    vim.notify('Failed to translate', vim.log.levels.ERROR, { title = 'Translate' })
    return
  end

  local lines = vim.split(trans:gsub('^%s+', ''):gsub('%s+$', ''), '\n')
  local filtered = {}
  for _, line in ipairs(lines) do
    table.insert(filtered, line)
  end
  if #filtered == 0 then
    vim.notify('Empty translation result', vim.log.levels.WARN, { title = 'Translate' })
    return
  end

  local width = 20
  for _, line in ipairs(filtered) do
    local line_width = vim.fn.strdisplaywidth(line)
    if line_width > width then
      width = math.min(80, line_width)
    end
  end
  local height = #filtered

  local buf = vim.api.nvim_create_buf(false, true)
  vim.api.nvim_buf_set_lines(buf, 0, -1, false, filtered)
  vim.api.nvim_buf_set_option(buf, 'filetype', 'markdown')
  vim.api.nvim_buf_set_option(buf, 'modifiable', false)

  vim.api.nvim_set_hl(0, 'TranslateBg', { bg = '#ffcc66', fg = '#000000' })
  vim.api.nvim_buf_set_extmark(buf, vim.api.nvim_create_namespace('translate'), 0, 0, {
    hl_group = 'TranslateBg',
    hl_eol = true,
    end_line = height - 1,
  })

  local row, col = unpack(vim.api.nvim_win_get_cursor(0))

  local win = vim.api.nvim_open_win(buf, true, {
    style = 'minimal',
    relative = 'cursor',
    width = width + 2,
    height = height,
    row = 1,
    col = 0,
    border = 'rounded',
    noautocmd = true,
  })

  vim.api.nvim_set_option_value('winblend', 0, { win = win })
  vim.api.nvim_set_option_value('winhighlight', 'Normal:TranslateBg', { win = win })

  vim.keymap.set('n', 'q', '<cmd>close<CR>', { buffer = buf, nowait = true })
  vim.keymap.set('n', '<Esc>', '<cmd>close<CR>', { buffer = buf, nowait = true })
end
Enter fullscreen mode Exit fullscreen mode

2. Add the keymaps

NOTE: I have created a map utility to help me out creating mappings:

-- https://blog.devgenius.io/create-custom-keymaps-in-neovim-with-lua-d1167de0f2c2
-- https://oroques.dev/notes/neovim-init/
function M.map(mode, lhs, rhs, opts)
  local options = { noremap = true, silent = true }
  if opts then options = vim.tbl_extend('force', options, opts) end
  vim.keymap.set(mode, lhs, rhs, options)
end
Enter fullscreen mode Exit fullscreen mode

In ~/.config/nvim/lua/core/keymaps.lua, add:

map('n', '<leader>st', nvim_utils.translate_popup, { desc = 'Show Translate (word under cursor)' })
map('x', '<leader>st', nvim_utils.translate_popup, { desc = 'Show Translate (selection)' })
Enter fullscreen mode Exit fullscreen mode

3. Mnemonic

The <leader>st shortcut was chosen with the mnemonic:

  • s → show
  • t → translate

Just like other translators show ("show translate"), or simply "translate".


Features

  • Word under cursor: Press <leader>st with cursor on a word
  • Visual selection: Select text and press <leader>st to translate
  • Multiple lines: Selected paragraphs preserve line breaks
  • Default language: English → Brazilian Portuguese (en:pt-BR)
  • Styled popup: Orange/yellow background (#ffcc66) with black text
  • Close with: q or <Esc>

Demo


Credits

  • opencode - Implementation and development
  • Sérgio Araújo - Idea, testing and feedback

References

Top comments (0)