├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── autoload ├── health │ └── translator.vim ├── translator.vim └── translator │ ├── action.vim │ ├── buffer.vim │ ├── cmdline.vim │ ├── history.vim │ ├── job.vim │ ├── logger.vim │ ├── util.vim │ ├── window.vim │ └── window │ ├── float.vim │ ├── popup.vim │ └── preview.vim ├── doc └── translator.txt ├── plugin └── translator.vim ├── script └── translator.py ├── syntax └── translator.vim └── test ├── default_config.vader ├── test_translator.py ├── translator_history.vader ├── translator_ui.vader ├── translator_util.vader └── vimrc /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | vader-tests: 6 | name: Vader tests 7 | strategy: 8 | matrix: 9 | os: [ubuntu-latest, macos-latest] 10 | neovim: [true] # only test on neovim currently 11 | runs-on: ${{ matrix.os }} 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Checkout vader.vim 15 | uses: actions/checkout@v2 16 | with: 17 | repository: junegunn/vader.vim 18 | path: vader.vim 19 | - name: Install Neovim 20 | uses: rhysd/action-setup-vim@v1 21 | id: vim 22 | with: 23 | neovim: ${{ matrix.neovim }} 24 | - name: Run vader tests 25 | env: 26 | VIM_EXEC: ${{ steps.vim.outputs.executable }} 27 | run: | 28 | $VIM_EXEC --version 29 | $VIM_EXEC -u test/vimrc -c 'Vader! test/*' 30 | 31 | vint: 32 | name: Run vint 33 | runs-on: ubuntu-latest 34 | steps: 35 | - uses: actions/checkout@v2 36 | - uses: actions/setup-python@v1 37 | - run: pip install vim-vint 38 | - run: vint --error --verbose --enable-neovim --color --style ./autoload ./plugin ./syntax 39 | 40 | translator-script: 41 | name: Test translator.py 42 | runs-on: ubuntu-latest 43 | steps: 44 | - uses: actions/checkout@v2 45 | - name: Install python 46 | uses: actions/setup-python@v1 47 | - name: Install translate-shell 48 | run: | 49 | sudo apt-get install gawk -y 50 | git clone https://github.com/soimort/translate-shell 51 | cd translate-shell/ 52 | make 53 | sudo make install 54 | cd - 55 | - name: Run translator tests 56 | run: | 57 | python --version 58 | python ./test/test_translator.py 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # For current directory only 2 | # ---------------------------------------------------------------------------- 3 | 4 | translation_history.data 5 | 6 | # General 7 | # ---------------------------------------------------------------------------- 8 | *.o 9 | *.out 10 | 11 | # log 12 | *.log 13 | 14 | # cache 15 | *.cache 16 | cache/ 17 | 18 | # Windows 19 | # ---------------------------------------------------------------------------- 20 | Thumbs.db 21 | Desktop.ini 22 | 23 | # Tags 24 | # ----------------------------------------------------------------------------- 25 | TAGS 26 | !TAGS/ 27 | tags 28 | tags-cn 29 | !tags/ 30 | .tags 31 | .tags1 32 | tags.lock 33 | tags.temp 34 | gtags.files 35 | GTAGS 36 | GRTAGS 37 | GPATH 38 | cscope.files 39 | cscope.out 40 | cscope.in.out 41 | cscope.po.out 42 | 43 | # Vim 44 | # ------------------------------------------------------------------------------ 45 | [._]*.s[a-w][a-z] 46 | [._]s[a-w][a-z] 47 | *.un~ 48 | Session.vim 49 | .netrwhist 50 | *~ 51 | 52 | # Test % Tmp 53 | # ------------------------------------------------------------------------------- 54 | test.* 55 | tmp.* 56 | temp.* 57 | 58 | # Java 59 | # ------------------------------------------------------------------------------- 60 | *.class 61 | 62 | # JavaScript 63 | # ------------------------------------------------------------------------------- 64 | node_modules 65 | 66 | # Python 67 | # ------------------------------------------------------------------------------- 68 | *.pyc 69 | .idea/ 70 | /.idea 71 | build/ 72 | __pycache__ 73 | 74 | # Rust 75 | # ------------------------------------------------------------------------------- 76 | target/ 77 | **/*.rs.bk 78 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Yunzhi Duan 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vim-translator 2 | 3 | ![CI](https://github.com/voldikss/vim-translator/workflows/CI/badge.svg) 4 | 5 | Asynchronous translating plugin for Vim/Neovim 6 | 7 | ![](https://user-images.githubusercontent.com/20282795/89249090-e3218880-d643-11ea-83a5-44915445690e.gif) 8 | 9 | - [Installation](#installation) 10 | - [Features](#features) 11 | - [Configuration](#configuration) 12 | - [Keymaps](#key-mappings) 13 | - [Commands](#commands) 14 | - [Highlight](#highlight) 15 | - [Statusline](#statusline) 16 | - [Know bugs](#know-bugs) 17 | - [FAQ](#faq) 18 | - [Breaking changes](#breaking-changes) 19 | - [References](#references) 20 | - [License](#license) 21 | 22 | ## Installation 23 | 24 | ```vim 25 | Plug 'voldikss/vim-translator' 26 | ``` 27 | 28 | ## Features 29 | 30 | - Asynchronous & mutithreading translating 31 | - Popupwin(vim8) & floatwin(neovim) support 32 | - Multiple engines: see [g:translator_default_engines](#gtranslator_default_engines) 33 | - Proxy support 34 | - No requirements for appid/appkey 35 | 36 | ## Configuration 37 | 38 | #### **`g:translator_target_lang`** 39 | 40 | Type `String`. 41 | 42 | Default: `'zh'` 43 | 44 | Please refer to [language support list](https://github.com/voldikss/vim-translator/wiki) 45 | 46 | #### **`g:translator_source_lang`** 47 | 48 | Type `String`. 49 | 50 | Default: `'auto'` 51 | 52 | Please refer to [language support list](https://github.com/voldikss/vim-translator/wiki) 53 | 54 | #### **`g:translator_default_engines`** 55 | 56 | Type `List` of `String`. 57 | 58 | Available: `'bing'`, `'google'`, `'haici'`, `'iciba'`(expired), `'sdcv'`, `'trans'`, `'youdao'` 59 | 60 | Default: If `g:translator_target_lang` is `'zh'`, this will be `['bing', 'google', 'haici', 'youdao']`, otherwise `['google']` 61 | 62 | #### **`g:translator_proxy_url`** 63 | 64 | Type `String`. Default: `''` 65 | 66 | Example: `let g:translator_proxy_url = 'socks5://127.0.0.1:1080'` 67 | 68 | #### **`g:translator_history_enable`** 69 | 70 | Type `Boolean`. 71 | 72 | Default: `v:false` 73 | 74 | #### **`g:translator_window_type`** 75 | 76 | Type `String`. 77 | 78 | Default: `'popup'` 79 | 80 | Available: `'popup'`(use floatwin in nvim or popup in vim), `'preview'` 81 | 82 | #### **`g:translator_window_max_width`** 83 | 84 | Type `Number` (number of columns) or `Float` (between 0 and 1). If `Float`, 85 | the width is relative to `&columns`. 86 | 87 | Default: `0.6` 88 | 89 | #### **`g:translator_window_max_height`** 90 | 91 | Type `Number` (number of lines) or `Float` (between 0 and 1). If `Float`, the 92 | height is relative to `&lines`. 93 | 94 | Default: `0.6` 95 | 96 | #### **`g:translator_window_borderchars`** 97 | 98 | Type `List` of `String`. Characters of the floating window border. 99 | 100 | Default: `['─', '│', '─', '│', '┌', '┐', '┘', '└']` 101 | 102 | ## Key Mappings 103 | 104 | This plugin doesn't supply any default mappings. 105 | 106 | ```vim 107 | """ Configuration example 108 | " Echo translation in the cmdline 109 | nmap t Translate 110 | vmap t TranslateV 111 | " Display translation in a window 112 | nmap w TranslateW 113 | vmap w TranslateWV 114 | " Replace the text with translation 115 | nmap r TranslateR 116 | vmap r TranslateRV 117 | " Translate the text in clipboard 118 | nmap x TranslateX 119 | ``` 120 | 121 | Once the translation window is opened, type `p` to jump into it and again to jump back 122 | 123 | Beside, there is a function which can be used to scroll window, only works in neovim. 124 | 125 | ```vim 126 | nnoremap translator#window#float#has_scroll() ? 127 | \ translator#window#float#scroll(1) : "\" 128 | nnoremap translator#window#float#has_scroll() ? 129 | \ translator#window#float#scroll(0) : "\" 130 | ``` 131 | 132 | ## Commands 133 | 134 | #### `Translate` 135 | 136 | `:Translate[!] [--engines=ENGINES] [--target_lang=TARGET_LANG] [--source_lang=SOURCE_LANG] [your text]` 137 | 138 | Translate the `text` from the source language `source_lang` to the target language `target_lang` with `engine`, echo the result in the cmdline 139 | 140 | If `engines` is not given, use `g:translator_default_engines` 141 | 142 | If `text` is not given, use the text under the cursor 143 | 144 | If `target_lang` is not given, use `g:translator_target_lang` 145 | 146 | The command can also be passed to a range, i.e., `:'<,'>Translate ...`, which translates text in visual selection 147 | 148 | If `!` is provided, the plugin will perform a reverse translating by switching `target_lang` and `source_lang` 149 | 150 | Examples(you can use `` to get completion): 151 | 152 | ```vim 153 | :Translate " translate the word under the cursor 154 | :Translate --engines=google,youdao are you ok " translate text `are you ok` using google and youdao engines 155 | :2Translate ... " translate line 2 156 | :1,3Translate ... " translate line 1 to line 3 157 | :'<,'>Translate ... " translate selected lines 158 | ``` 159 | 160 | #### `TranslateW` 161 | 162 | `:TranslateW[!] [--engines=ENGINES] [--target_lang=TARGET_LANG] [--source_lang=SOURCE_LANG] [your text]` 163 | 164 | Like `:Translate...`, but display the translation in a window 165 | 166 | #### `TranslateR` 167 | 168 | `:TranslateR[!] [--engines=ENGINES] [--target_lang=TARGET_LANG] [--source_lang=SOURCE_LANG] [your text]` 169 | 170 | Like `:Translate...`, but replace the current text with the translation 171 | 172 | #### `TranslateX` 173 | 174 | `:TranslateX[!] [--engines=ENGINES] [--target_lang=TARGET_LANG] [--source_lang=SOURCE_LANG] [your text]` 175 | 176 | Translate the text in the clipboard 177 | 178 | #### `TranslateH` 179 | 180 | `:TranslateH` 181 | 182 | Export the translation history 183 | 184 | #### `TranslateL` 185 | 186 | `:TranslateL` 187 | 188 | Display log message 189 | 190 | ## Highlight 191 | 192 | Here are the default highlight links. To customize, use `hi` or `hi link` 193 | 194 | ```vim 195 | " Text highlight of translator window 196 | hi def link TranslatorQuery Identifier 197 | hi def link TranslatorDelimiter Special 198 | hi def link TranslatorExplain Statement 199 | 200 | " Background of translator window border 201 | hi def link Translator Normal 202 | hi def link TranslatorBorder NormalFloat 203 | ``` 204 | 205 | ## Statusline 206 | 207 | - `g:translator_status` 208 | 209 | ## FAQ 210 | 211 | https://github.com/voldikss/vim-translator/issues?q=label%3AFAQ 212 | 213 | ## Breaking Changes 214 | 215 | https://github.com/voldikss/vim-translator/issues?q=label%3A%22breaking+change%22 216 | 217 | ## References 218 | 219 | - [dict.vim](https://github.com/iamcco/dict.vim) 220 | - [translator](https://github.com/skywind3000/translator) 221 | 222 | ## License 223 | 224 | MIT 225 | -------------------------------------------------------------------------------- /autoload/health/translator.vim: -------------------------------------------------------------------------------- 1 | " @Author: voldikss 2 | " @Date: 2019-04-28 13:32:21 3 | " @Last Modified by: voldikss 4 | " @Last Modified time: 2019-06-20 18:57:36 5 | 6 | function! s:check_job() abort 7 | if exists('*jobstart') || exists('*job_start') 8 | call health#report_ok('Async check passed') 9 | else 10 | call health#report_error('Job feature is required but not found') 11 | endif 12 | endfunction 13 | 14 | function! s:check_floating_window() abort 15 | " nvim, but doesn't have floating window 16 | if !exists('*nvim_open_win') 17 | call health#report_error( 18 | \ 'Floating window is missed on the current version Nvim', 19 | \ 'Upgrade your Nvim")' 20 | \ ) 21 | return 22 | endif 23 | 24 | " has floating window, but different parameters(old version) 25 | try 26 | let test_win = nvim_open_win(bufnr('%'), v:false, { 27 | \ 'relative': 'editor', 28 | \ 'row': 0, 29 | \ 'col': 0, 30 | \ 'width': 1, 31 | \ 'height': 1, 32 | \ }) 33 | call nvim_win_close(test_win, v:true) 34 | catch /^Vim\%((\a\+)\)\=:E119/ 35 | call health#report_error( 36 | \ 'The newest floating window feature is missed on the current version Nvim', 37 | \ 'Upgrade your Nvim")' 38 | \ ) 39 | return 40 | endtry 41 | call health#report_ok('Floating window check passed') 42 | endfunction 43 | 44 | function! s:check_python() abort 45 | if exists('g:python3_host_prog') && executable('g:python3_host_prog') 46 | let translator_python_host = g:python3_host_prog 47 | elseif executable('python3') 48 | let translator_python_host = 'python3' 49 | elseif executable('python') 50 | let translator_python_host = 'python' 51 | else 52 | call health#report_error('Python is required but not executable: ' . translator_python_host) 53 | return 54 | endif 55 | call health#report_ok('Using '.translator_python_host) 56 | endfunction 57 | 58 | function! health#translator#check() abort 59 | call s:check_job() 60 | call s:check_floating_window() 61 | call s:check_python() 62 | endfunction 63 | -------------------------------------------------------------------------------- /autoload/translator.vim: -------------------------------------------------------------------------------- 1 | " ============================================================================ 2 | " FileName: translator.vim 3 | " Author: voldikss 4 | " GitHub: https://github.com/voldikss 5 | " ============================================================================ 6 | 7 | let s:py_file = expand(':p:h') . '/../script/translator.py' 8 | 9 | if !exists('s:python_executable') 10 | if exists('g:python3_host_prog') && executable('g:python3_host_prog') 11 | let s:python_executable = g:python3_host_prog 12 | elseif executable('python3') 13 | let s:python_executable = 'python3' 14 | elseif executable('python') 15 | let s:python_executable = 'python' 16 | else 17 | call translator#util#show_msg('python is required but not found', 'error') 18 | finish 19 | endif 20 | endif 21 | 22 | if stridx(s:python_executable, ' ') >= 0 23 | let s:python_executable = shellescape(s:python_executable) 24 | endif 25 | if stridx(s:py_file, ' ') >= 0 26 | let s:py_file = shellescape(s:py_file) 27 | endif 28 | 29 | function! translator#start(displaymode, bang, range, line1, line2, argstr) abort 30 | call translator#logger#init() 31 | let options = translator#cmdline#parse(a:bang, a:range, a:line1, a:line2, a:argstr) 32 | if options is v:null | return | endif 33 | call translator#translate(options, a:displaymode) 34 | endfunction 35 | 36 | function! translator#translate(options, displaymode) abort 37 | let cmd = [ 38 | \ s:python_executable, 39 | \ s:py_file, 40 | \ '--target_lang', a:options.target_lang, 41 | \ '--source_lang', a:options.source_lang, 42 | \ a:options.text, 43 | \ '--engines' 44 | \ ] 45 | \ + a:options.engines 46 | if !empty(g:translator_proxy_url) 47 | let cmd += ['--proxy', g:translator_proxy_url] 48 | endif 49 | if match(a:options.engines, 'trans') >= 0 50 | let cmd += [printf("--options='%s'", join(g:translator_translate_shell_options, ','))] 51 | endif 52 | call translator#logger#log(join(cmd, ' ')) 53 | call translator#job#jobstart(cmd, a:displaymode) 54 | endfunction 55 | -------------------------------------------------------------------------------- /autoload/translator/action.vim: -------------------------------------------------------------------------------- 1 | " ============================================================================ 2 | " FileName: action.vim 3 | " Author: voldikss 4 | " GitHub: https://github.com/voldikss 5 | " ============================================================================ 6 | 7 | function! translator#action#window(translations) abort 8 | let marker = '• ' 9 | let content = [] 10 | if len(a:translations['text']) > 30 11 | let text = a:translations['text'][:30] . '...' 12 | else 13 | let text = a:translations['text'] 14 | endif 15 | call add(content, printf('⟦ %s ⟧', text)) 16 | 17 | for t in a:translations['results'] 18 | if empty(t.paraphrase) && empty(t.explains) 19 | continue 20 | endif 21 | call add(content, '') 22 | call add(content, printf('─── %s ───', t.engine)) 23 | if !empty(t.phonetic) 24 | let phonetic = marker . printf('[%s]', t.phonetic) 25 | call add(content, phonetic) 26 | endif 27 | if !empty(t.paraphrase) 28 | let paraphrase = marker . t['paraphrase'] 29 | call add(content, paraphrase) 30 | endif 31 | if !empty(t.explains) 32 | for expl in t.explains 33 | let expl = translator#util#safe_trim(expl) 34 | if !empty(expl) 35 | let explains = marker . expl 36 | call add(content, explains) 37 | endif 38 | endfor 39 | endif 40 | endfor 41 | call translator#logger#log(content) 42 | call translator#window#open(content) 43 | endfunction 44 | 45 | function! translator#action#echo(translations) abort 46 | let phonetic = '' 47 | let paraphrase = '' 48 | let explains = '' 49 | 50 | for t in a:translations['results'] 51 | if !empty(t.phonetic) && empty(phonetic) 52 | let phonetic = printf('[%s]', t.phonetic) 53 | endif 54 | if !empty(t.paraphrase) && empty(paraphrase) 55 | let paraphrase = t.paraphrase 56 | endif 57 | if !empty(t.explains) && empty(explains) 58 | let explains = join(t.explains, ' ') 59 | endif 60 | endfor 61 | 62 | if len(a:translations['text']) > 30 63 | let text = a:translations['text'][:30] . '...' 64 | else 65 | let text = a:translations['text'] 66 | endif 67 | call translator#util#echo('Function', text) 68 | call translator#util#echon('Constant', '==>') 69 | call translator#util#echon('Type', phonetic) 70 | call translator#util#echon('Normal', paraphrase) 71 | call translator#util#echon('Normal', explains) 72 | endfunction 73 | 74 | function! translator#action#replace(translations) abort 75 | for t in a:translations['results'] 76 | if !empty(t.paraphrase) 77 | let reg_tmp = @a 78 | let @a = t.paraphrase 79 | normal! gv"ap 80 | let @a = reg_tmp 81 | unlet reg_tmp 82 | return 83 | endif 84 | endfor 85 | call translator#util#show_msg('No paraphrases for the replacement', 'warning') 86 | endfunction 87 | -------------------------------------------------------------------------------- /autoload/translator/buffer.vim: -------------------------------------------------------------------------------- 1 | " ============================================================================ 2 | " FileName: buffer.vim 3 | " Author: voldikss 4 | " GitHub: https://github.com/voldikss 5 | " ============================================================================ 6 | 7 | function! translator#buffer#create_border(configs) abort 8 | let repeat_width = a:configs.width - 2 9 | let title_width = strdisplaywidth(a:configs.title) 10 | let [c_top, c_right, c_bottom, c_left, c_topleft, c_topright, c_botright, c_botleft] = a:configs.borderchars 11 | let content = [c_topleft . a:configs.title . repeat(c_top, repeat_width - title_width) . c_topright] 12 | let content += repeat([c_left . repeat(' ', repeat_width) . c_right], a:configs.height-2) 13 | let content += [c_botleft . repeat(c_bottom, repeat_width) . c_botright] 14 | let bd_bufnr = translator#buffer#create_scratch_buf(content) 15 | call nvim_buf_set_option(bd_bufnr, 'filetype', 'translatorborder') 16 | return bd_bufnr 17 | endfunction 18 | 19 | function! translator#buffer#create_scratch_buf(...) abort 20 | let bufnr = nvim_create_buf(v:false, v:true) 21 | call nvim_buf_set_option(bufnr, 'buftype', 'nofile') 22 | call nvim_buf_set_option(bufnr, 'buftype', 'nofile') 23 | call nvim_buf_set_option(bufnr, 'bufhidden', 'wipe') 24 | call nvim_buf_set_option(bufnr, 'swapfile', v:false) 25 | call nvim_buf_set_option(bufnr, 'undolevels', -1) 26 | let lines = get(a:, 1, v:null) 27 | if type(lines) != 7 28 | call nvim_buf_set_option(bufnr, 'modifiable', v:true) 29 | call nvim_buf_set_lines(bufnr, 0, -1, v:false, lines) 30 | call nvim_buf_set_option(bufnr, 'modifiable', v:false) 31 | endif 32 | return bufnr 33 | endfunction 34 | 35 | function! translator#buffer#init(bufnr) abort 36 | call setbufvar(a:bufnr, '&filetype', 'translator') 37 | call setbufvar(a:bufnr, '&buftype', 'nofile') 38 | call setbufvar(a:bufnr, '&bufhidden', 'wipe') 39 | call setbufvar(a:bufnr, '&buflisted', 0) 40 | call setbufvar(a:bufnr, '&swapfile', 0) 41 | endfunction 42 | -------------------------------------------------------------------------------- /autoload/translator/cmdline.vim: -------------------------------------------------------------------------------- 1 | " ============================================================================ 2 | " FileName: cmdline.vim 3 | " Author: voldikss 4 | " GitHub: https://github.com/voldikss 5 | " ============================================================================ 6 | 7 | function! translator#cmdline#parse(bang, range, line1, line2, argstr) abort 8 | call translator#logger#log(a:argstr) 9 | let options = { 10 | \ 'text': '', 11 | \ 'engines': [], 12 | \ 'target_lang': '', 13 | \ 'source_lang': '' 14 | \ } 15 | let arglist = split(a:argstr) 16 | if !empty(arglist) 17 | let c = 0 18 | for arg in arglist 19 | if arg =~ '^--\S.*=.*$' 20 | let opt = split(arg, '=') 21 | if len(opt) != 2 22 | call translator#util#show_msg('Argument Error: No value given to option: ' . opt[0], 'error') 23 | return v:null 24 | endif 25 | let [key, value] = [opt[0][2:], opt[1]] 26 | if key == 'engines' 27 | let options.engines = split(value, ',') 28 | else 29 | let options[key] = value 30 | endif 31 | else 32 | let options.text = join(arglist[c:]) 33 | break 34 | endif 35 | let c += 1 36 | endfor 37 | endif 38 | 39 | if empty(options.text) 40 | let options.text = translator#util#visual_select(a:range, a:line1, a:line2) 41 | endif 42 | let options.text = translator#util#text_proc(options.text) 43 | if empty(options.text) 44 | return v:null 45 | endif 46 | 47 | if empty(options.engines) 48 | let options.engines = g:translator_default_engines 49 | endif 50 | 51 | if empty(options.target_lang) 52 | let options.target_lang = g:translator_target_lang 53 | endif 54 | 55 | if empty(options.source_lang) 56 | let options.source_lang = g:translator_source_lang 57 | endif 58 | 59 | if a:bang && options.source_lang != 'auto' 60 | let [options.source_lang, options.target_lang] = [options.target_lang, options.source_lang] 61 | endif 62 | 63 | return options 64 | endfunction 65 | 66 | function! translator#cmdline#complete(arg_lead, cmd_line, cursor_pos) abort 67 | let opts_key = ['--engines=', '--target_lang=', '--source_lang='] 68 | let candidates = opts_key 69 | 70 | let cmd_line_before_cursor = a:cmd_line[:a:cursor_pos - 1] 71 | let args = split(cmd_line_before_cursor, '\v\\@ -1 89 | let pos = s:matchlastpos(prefix, ',') 90 | let preprefix = prefix[:pos] 91 | let unused_engines = [] 92 | for e in engines 93 | if match(prefix, e) == -1 94 | call add(unused_engines, e) 95 | endif 96 | endfor 97 | let candidates = map(unused_engines, {idx -> preprefix . unused_engines[idx]}) 98 | elseif match(prefix, '--engines=') > -1 99 | let candidates = map(engines, {idx -> "--engines=" . engines[idx]}) 100 | endif 101 | return filter(candidates, 'v:val[:len(prefix) - 1] == prefix') 102 | endfunction 103 | 104 | function! s:matchlastpos(expr, pat) abort 105 | let pos = -1 106 | for i in range(1, 10) 107 | let p = match(a:expr, a:pat, 0, i) 108 | if p > pos 109 | let pos = p 110 | endif 111 | endfor 112 | return pos 113 | endfunction 114 | -------------------------------------------------------------------------------- /autoload/translator/history.vim: -------------------------------------------------------------------------------- 1 | " ============================================================================ 2 | " FileName: history.vim 3 | " Author: voldikss 4 | " GitHub: https://github.com/voldikss 5 | " ============================================================================ 6 | 7 | let s:history_file = expand(':p:h') . '/../../translation_history.data' 8 | 9 | function! s:padding_end(text, length) abort 10 | let text = a:text 11 | let len = len(text) 12 | if len < a:length 13 | for i in range(a:length-len) 14 | let text .= ' ' 15 | endfor 16 | endif 17 | return text 18 | endfunction 19 | 20 | function! translator#history#save(translations) abort 21 | if !g:translator_history_enable 22 | return 23 | endif 24 | 25 | let text = a:translations['text'] 26 | for t in a:translations['results'] 27 | let paraphrase = t['paraphrase'] 28 | let explains = t['explains'] 29 | 30 | if !empty(explains) 31 | let item = s:padding_end(text, 25) . explains[0] 32 | break 33 | elseif !empty(paraphrase) && text !=? paraphrase 34 | let item = s:padding_end(text, 25) . paraphrase 35 | break 36 | else 37 | return 38 | endif 39 | endfor 40 | 41 | if !filereadable(s:history_file) 42 | call writefile([], s:history_file) 43 | endif 44 | 45 | let trans_data = readfile(s:history_file) 46 | 47 | " already in 48 | if match(string(trans_data), text) >= 0 49 | return 50 | endif 51 | 52 | execute 'redir >> ' . s:history_file 53 | silent! echon item . "\n" 54 | redir END 55 | endfunction 56 | 57 | function! translator#history#export() abort 58 | if !filereadable(s:history_file) 59 | let message = 'History file not exist yet' 60 | call translator#util#show_msg(message, 'error') 61 | return 62 | endif 63 | 64 | execute 'tabnew ' . s:history_file 65 | setlocal filetype=translator_history 66 | syn match TranslateHistoryQuery #\v^.*\v%25v# 67 | syn match TranslateHistoryTrans #\v%26v.*$# 68 | hi def link TranslateHistoryQuery Keyword 69 | hi def link TranslateHistoryTrans String 70 | endfunction 71 | -------------------------------------------------------------------------------- /autoload/translator/job.vim: -------------------------------------------------------------------------------- 1 | " ============================================================================ 2 | " FileName: job.vim 3 | " Author: voldikss 4 | " GitHub: https://github.com/voldikss 5 | " ============================================================================ 6 | 7 | if has('nvim') 8 | function! s:on_stdout_nvim(type, jobid, data, event) abort 9 | call s:start(a:type, a:data, a:event) 10 | endfunction 11 | 12 | function! s:on_exit_nvim(jobid, code, event) abort 13 | endfunction 14 | else 15 | function! s:on_stdout_vim(type, event, ch, msg) abort 16 | call s:start(a:type, a:msg, a:event) 17 | endfunction 18 | 19 | function! s:on_exit_vim(ch, code) abort 20 | endfunction 21 | endif 22 | 23 | function! translator#job#jobstart(cmd, type) abort 24 | let g:translator_status = 'translating' 25 | let s:stdout_save = {} 26 | if has('nvim') 27 | let callback = { 28 | \ 'on_stdout': function('s:on_stdout_nvim', [a:type]), 29 | \ 'on_stderr': function('s:on_stdout_nvim', [a:type]), 30 | \ 'on_exit': function('s:on_exit_nvim') 31 | \ } 32 | call jobstart(a:cmd, callback) 33 | else 34 | let callback = { 35 | \ 'out_cb': function('s:on_stdout_vim', [a:type, 'stdout']), 36 | \ 'err_cb': function('s:on_stdout_vim', [a:type, 'stderr']), 37 | \ 'exit_cb': function('s:on_exit_vim'), 38 | \ 'out_io': 'pipe', 39 | \ 'err_io': 'pipe', 40 | \ 'in_io': 'null', 41 | \ 'out_mode': 'nl', 42 | \ 'err_mode': 'nl', 43 | \ 'timeout': '2000' 44 | \ } 45 | call job_start(join(a:cmd,' '), callback) 46 | endif 47 | endfunction 48 | 49 | function! s:start(type, data, event) abort 50 | let g:translator_status = '' 51 | " Nvim will return a v:t_list, while Vim will return a v:t_string 52 | if type(a:data) == 3 53 | let message = join(a:data, ' ') 54 | else 55 | let message = a:data 56 | endif 57 | 58 | " On Nvim, this function will be executed twice, firstly it returns data, and then an empty string 59 | " Check the data value in order to prevent overlap 60 | if translator#util#safe_trim(message) == '' | return | endif 61 | call translator#logger#log(message) 62 | 63 | " 1. remove `u` before strings 64 | let message = substitute(message, '\(: \|: [\|{\)\(u\)\("\)', '\=submatch(1).submatch(3)', 'g') 65 | let message = substitute(message, "\\(: \\|: [\\|{\\)\\(u\\)\\('\\)", '\=submatch(1).submatch(3)', 'g') 66 | let message = substitute(message, "\\([: \\|: \[]\\)\\(u\\)\\('\\)", '\=submatch(1).submatch(3)', 'g') 67 | " 2. convert hex code to normal chars 68 | let message = substitute(message, '\\u\(\x\{4\}\)', '\=nr2char("0x".submatch(1),1)', 'g') 69 | call translator#logger#log(message) 70 | 71 | if a:event == 'stdout' 72 | let translations = eval(message) 73 | if type(translations) != 4 && !translations['status'] 74 | call translator#util#show_msg('Translation failed', 'error') 75 | endif 76 | 77 | let s:stdout_save = translations 78 | if a:type == 'echo' 79 | call translator#action#echo(translations) 80 | elseif a:type == 'window' 81 | call translator#action#window(translations) 82 | else 83 | call translator#action#replace(translations) 84 | endif 85 | call translator#history#save(translations) 86 | elseif a:event == 'stderr' 87 | call translator#util#show_msg(message, 'error') 88 | if !empty(s:stdout_save) && a:type == 'echo' 89 | call translator#action#echo(s:stdout_save) 90 | endif 91 | endif 92 | endfunction 93 | -------------------------------------------------------------------------------- /autoload/translator/logger.vim: -------------------------------------------------------------------------------- 1 | " ============================================================================ 2 | " FileName: logger.vim 3 | " Author: voldikss 4 | " GitHub: https://github.com/voldikss 5 | " ============================================================================ 6 | 7 | let s:log = [] 8 | 9 | function! translator#logger#init() abort 10 | let s:log = [] 11 | endfunction 12 | 13 | function! translator#logger#log(info) abort 14 | let trace = expand('') 15 | let info = {} 16 | let info[trace] = a:info 17 | call add(s:log, info) 18 | endfunction 19 | 20 | function! translator#logger#open_log() abort 21 | bo vsplit vim-translator.log 22 | setlocal buftype=nofile 23 | setlocal commentstring=@\ %s 24 | call matchadd('Constant', '\v\@.*$') 25 | for log in s:log 26 | for [k,v] in items(log) 27 | call append('$', '@' . k) 28 | if type(v) == v:t_dict 29 | call append('$', string(v)) 30 | else 31 | call append('$', v) 32 | endif 33 | call append('$', '') 34 | endfor 35 | endfor 36 | endfunction 37 | -------------------------------------------------------------------------------- /autoload/translator/util.vim: -------------------------------------------------------------------------------- 1 | " ============================================================================ 2 | " FileName: util.vim 3 | " Author: voldikss 4 | " GitHub: https://github.com/voldikss 5 | " ============================================================================ 6 | 7 | function! translator#util#echo(group, msg) abort 8 | if a:msg == '' | return | endif 9 | execute 'echohl' a:group 10 | echo a:msg 11 | echon ' ' 12 | echohl NONE 13 | endfunction 14 | 15 | function! translator#util#echon(group, msg) abort 16 | if a:msg == '' | return | endif 17 | execute 'echohl' a:group 18 | echon a:msg 19 | echon ' ' 20 | echohl NONE 21 | endfunction 22 | 23 | function! translator#util#show_msg(message, ...) abort 24 | if a:0 == 0 25 | let msg_type = 'info' 26 | else 27 | let msg_type = a:1 28 | endif 29 | 30 | if type(a:message) != 1 31 | let message = string(a:message) 32 | else 33 | let message = a:message 34 | endif 35 | 36 | call translator#util#echo('Constant', '[vim-translator]') 37 | 38 | if msg_type == 'info' 39 | call translator#util#echon('Normal', message) 40 | elseif msg_type == 'warning' 41 | call translator#util#echon('WarningMsg', message) 42 | elseif msg_type == 'error' 43 | call translator#util#echon('Error', message) 44 | endif 45 | endfunction 46 | 47 | function! translator#util#pad(text, width, char) abort 48 | let padding_size = (a:width - strdisplaywidth(a:text)) / 2 49 | let padding = repeat(a:char, padding_size / strdisplaywidth(a:char)) 50 | let padend = repeat(a:char, (a:width - strdisplaywidth(a:text)) % 2) 51 | let text = padding . a:text . padding 52 | if a:width >= strdisplaywidth(text) + strdisplaywidth(padend) 53 | let text .= padend 54 | endif 55 | return text 56 | endfunction 57 | 58 | function! translator#util#fit_lines(linelist, width) abort 59 | for i in range(len(a:linelist)) 60 | let line = a:linelist[i] 61 | if match(line, '───') == 0 && a:width > strdisplaywidth(line) 62 | let a:linelist[i] = translator#util#pad(a:linelist[i], a:width, '─') 63 | elseif match(line, '⟦') == 0 && a:width > strdisplaywidth(line) 64 | let a:linelist[i] = translator#util#pad(a:linelist[i], a:width, ' ') 65 | endif 66 | endfor 67 | return a:linelist 68 | endfunction 69 | 70 | function! translator#util#visual_select(range, line1, line2) abort 71 | if a:range == 0 72 | let lines = [expand('')] 73 | elseif a:range == 1 74 | let lines = [getline('.')] 75 | else 76 | if a:line1 == a:line2 77 | " https://vi.stackexchange.com/a/11028/17515 78 | let [lnum1, col1] = getpos("'<")[1:2] 79 | let [lnum2, col2] = getpos("'>")[1:2] 80 | let lines = getline(lnum1, lnum2) 81 | if empty(lines) 82 | call floaterm#util#show_msg('No lines were selected', 'error') 83 | return 84 | endif 85 | let lines[-1] = lines[-1][: col2 - 1] 86 | let lines[0] = lines[0][col1 - 1:] 87 | else 88 | let lines = getline(a:line1, a:line2) 89 | endif 90 | endif 91 | return join(lines) 92 | endfunction 93 | 94 | function! translator#util#safe_trim(text) abort 95 | return substitute(a:text,'\%#=1^[[:space:]]\+\|[[:space:]]\+$', '', 'g') 96 | endfunction 97 | 98 | function! translator#util#text_proc(text) abort 99 | let text = substitute(a:text, "\n", ' ', 'g') 100 | let text = substitute(text, "\n\r", ' ', 'g') 101 | let text = substitute(text, '\v^\s+', '', '') 102 | let text = substitute(text, '\v\s+$', '', '') 103 | let text = escape(text, '"') 104 | let text = printf('"%s"', text) 105 | return text 106 | endfunction 107 | -------------------------------------------------------------------------------- /autoload/translator/window.vim: -------------------------------------------------------------------------------- 1 | " ============================================================================ 2 | " FileName: window.vim 3 | " Author: voldikss 4 | " GitHub: https://github.com/voldikss 5 | " ============================================================================ 6 | 7 | let s:has_popup = has('textprop') && has('patch-8.2.0286') 8 | let s:has_float = has('nvim') && exists('*nvim_win_set_config') 9 | 10 | function! s:win_gettype() abort 11 | if g:translator_window_type == 'popup' 12 | if s:has_float 13 | return 'float' 14 | elseif s:has_popup 15 | return 'popup' 16 | else 17 | call translator#util#show_msg("popup is not supported, use preview window", 'warning') 18 | return 'preview' 19 | endif 20 | endif 21 | return 'preview' 22 | endfunction 23 | let s:wintype = s:win_gettype() 24 | 25 | function! s:win_getsize(translation, max_width, max_height) abort 26 | let width = 0 27 | let height = 0 28 | 29 | for line in a:translation 30 | let line_width = strdisplaywidth(line) 31 | if line_width > a:max_width 32 | let width = a:max_width 33 | let height += line_width / a:max_width + 1 34 | else 35 | let width = max([line_width, width]) 36 | let height += 1 37 | endif 38 | endfor 39 | 40 | if height > a:max_height 41 | let height = a:max_height 42 | endif 43 | return [width, height] 44 | endfunction 45 | 46 | function! s:win_getoptions(width, height) abort 47 | let pos = win_screenpos('.') 48 | let y_pos = pos[0] + winline() - 1 49 | let x_pos = pos[1] + wincol() -1 50 | 51 | let border = empty(g:translator_window_borderchars) ? 0 : 2 52 | let y_margin = 2 53 | let [width, height] = [a:width, a:height] 54 | 55 | if y_pos + height + border + y_margin <= &lines 56 | let vert = 'N' 57 | let y_offset = 0 58 | elseif y_pos - height -border - y_margin >= 0 59 | let vert = 'S' 60 | let y_offset = -1 61 | elseif &lines - y_pos >= y_pos 62 | let vert = 'N' 63 | let y_offset = 0 64 | let height = &lines - y_pos - border - y_margin 65 | else 66 | let vert = 'S' 67 | let y_offset = -1 68 | let height = y_pos - border - y_margin 69 | endif 70 | 71 | if x_pos + a:width + border <= &columns 72 | let hor = 'W' 73 | let x_offset = -1 74 | elseif x_pos - width - border >= 0 75 | let hor = 'E' 76 | let x_offset = 0 77 | elseif &columns - x_pos >= x_pos 78 | let hor = 'W' 79 | let x_offset = -1 80 | let width = &columns - x_pos - border 81 | else 82 | let hor = 'E' 83 | let x_offset = 0 84 | let width = x_pos - border 85 | endif 86 | let anchor = vert . hor 87 | if !has('nvim') 88 | let anchor = substitute(anchor, '\CN', 'top', '') 89 | let anchor = substitute(anchor, '\CS', 'bot', '') 90 | let anchor = substitute(anchor, '\CW', 'left', '') 91 | let anchor = substitute(anchor, '\CE', 'right', '') 92 | endif 93 | let row = y_pos + y_offset 94 | let col = x_pos + x_offset 95 | return [anchor, row, col, width, height] 96 | endfunction 97 | 98 | " setwinvar also accept window-ID, which is not mentioned in the document 99 | function! translator#window#init(winid) abort 100 | call setwinvar(a:winid, '&wrap', 1) 101 | call setwinvar(a:winid, '&conceallevel', 3) 102 | call setwinvar(a:winid, '&number', 0) 103 | call setwinvar(a:winid, '&relativenumber', 0) 104 | call setwinvar(a:winid, '&spell', 0) 105 | call setwinvar(a:winid, '&foldcolumn', 0) 106 | if has('nvim') 107 | call setwinvar(a:winid, '&winhl', 'Normal:Translator') 108 | else 109 | call setwinvar(a:winid, '&wincolor', 'Translator') 110 | endif 111 | endfunction 112 | 113 | function! translator#window#open(content) abort 114 | let max_width = g:translator_window_max_width 115 | if type(max_width) == v:t_float | let max_width = max_width * &columns | endif 116 | let max_width = float2nr(max_width) 117 | 118 | let max_height = g:translator_window_max_height 119 | if type(max_height) == v:t_float | let max_height = max_height * &lines | endif 120 | let max_height = float2nr(max_height) 121 | 122 | let [width, height] = s:win_getsize(a:content, max_width, max_height) 123 | let [anchor, row, col, width, height] = s:win_getoptions(width, height) 124 | let linelist = translator#util#fit_lines(a:content, width) 125 | 126 | let configs = { 127 | \ 'anchor': anchor, 128 | \ 'row': row, 129 | \ 'col': col, 130 | \ 'width': width + 2, 131 | \ 'height': height + 2, 132 | \ 'title': '', 133 | \ 'borderchars': g:translator_window_borderchars 134 | \ } 135 | call translator#window#{s:wintype}#create(linelist, configs) 136 | endfunction 137 | -------------------------------------------------------------------------------- /autoload/translator/window/float.vim: -------------------------------------------------------------------------------- 1 | " ============================================================================ 2 | " FileName: float.vim 3 | " Author: voldikss 4 | " GitHub: https://github.com/voldikss 5 | " Description: thanks coc.nvim 6 | " ============================================================================ 7 | 8 | " max firstline of lines, height > 0, width > 0 9 | function! s:max_firstline(lines, height, width) abort 10 | let max = len(a:lines) 11 | let remain = a:height 12 | for line in reverse(copy(a:lines)) 13 | let w = max([1, strdisplaywidth(line)]) 14 | let dh = float2nr(ceil(str2float(string(w))/a:width)) 15 | if remain - dh < 0 16 | break 17 | endif 18 | let remain = remain - dh 19 | let max = max - 1 20 | endfor 21 | return min([len(a:lines), max + 1]) 22 | endfunction 23 | 24 | function! s:content_height(bufnr, width, wrap) abort 25 | if !bufloaded(a:bufnr) 26 | return 0 27 | endif 28 | if !a:wrap 29 | return has('nvim') ? nvim_buf_line_count(a:bufnr) : len(getbufline(a:bufnr, 1, '$')) 30 | endif 31 | let lines = has('nvim') ? nvim_buf_get_lines(a:bufnr, 0, -1, 0) : getbufline(a:bufnr, 1, '$') 32 | let total = 0 33 | for line in lines 34 | let dw = max([1, strdisplaywidth(line)]) 35 | let total += float2nr(ceil(str2float(string(dw))/a:width)) 36 | endfor 37 | return total 38 | endfunction 39 | 40 | " Get best lnum by topline 41 | function! s:get_cursorline(topline, lines, scrolloff, width, height) abort 42 | let lastline = len(a:lines) 43 | if a:topline == lastline 44 | return lastline 45 | endif 46 | let bottomline = a:topline 47 | let used = 0 48 | for lnum in range(a:topline, lastline) 49 | let w = max([1, strdisplaywidth(a:lines[lnum - 1])]) 50 | let dh = float2nr(ceil(str2float(string(w))/a:width)) 51 | let g:l = a:lines 52 | if used + dh >= a:height || lnum == lastline 53 | let bottomline = lnum 54 | break 55 | endif 56 | let used += dh 57 | endfor 58 | let cursorline = a:topline + a:scrolloff 59 | let g:b = bottomline 60 | let g:h = a:height 61 | if cursorline + a:scrolloff > bottomline 62 | " unable to satisfy scrolloff 63 | let cursorline = (a:topline + bottomline)/2 64 | endif 65 | return cursorline 66 | endfunction 67 | 68 | " Get firstline for full scroll 69 | function! s:get_topline(topline, lines, forward, height, width) abort 70 | let used = 0 71 | let lnums = a:forward ? range(a:topline, len(a:lines)) : reverse(range(1, a:topline)) 72 | let topline = a:forward ? len(a:lines) : 1 73 | for lnum in lnums 74 | let w = max([1, strdisplaywidth(a:lines[lnum - 1])]) 75 | let dh = float2nr(ceil(str2float(string(w))/a:width)) 76 | if used + dh >= a:height 77 | let topline = lnum 78 | break 79 | endif 80 | let used += dh 81 | endfor 82 | if topline == a:topline 83 | if a:forward 84 | let topline = min([len(a:lines), topline + 1]) 85 | else 86 | let topline = max([1, topline - 1]) 87 | endif 88 | endif 89 | return topline 90 | endfunction 91 | 92 | " topline content_height content_width 93 | function! s:get_options(winid) abort 94 | if has('nvim') 95 | let width = nvim_win_get_width(a:winid) 96 | if getwinvar(a:winid, '&foldcolumn', 0) 97 | let width = width - 1 98 | endif 99 | let info = getwininfo(a:winid)[0] 100 | return { 101 | \ 'topline': info['topline'], 102 | \ 'height': nvim_win_get_height(a:winid), 103 | \ 'width': width 104 | \ } 105 | else 106 | let pos = popup_getpos(a:winid) 107 | return { 108 | \ 'topline': pos['firstline'], 109 | \ 'width': pos['core_width'], 110 | \ 'height': pos['core_height'] 111 | \ } 112 | endif 113 | endfunction 114 | 115 | function! s:win_execute(winid, command) abort 116 | let curr = nvim_get_current_win() 117 | noa keepalt call nvim_set_current_win(a:winid) 118 | exec a:command 119 | noa keepalt call nvim_set_current_win(curr) 120 | endfunction 121 | 122 | function! s:win_setview(winid, topline, lnum) abort 123 | let cmd = 'call winrestview({"lnum":'.a:lnum.',"topline":'.a:topline.'})' 124 | call s:win_execute(a:winid, cmd) 125 | endfunction 126 | 127 | function! s:win_exists(winid) abort 128 | return !empty(getwininfo(a:winid)) 129 | endfunction 130 | 131 | function! s:win_close_float() abort 132 | if win_getid() == s:winid 133 | return 134 | else 135 | if s:win_exists(s:winid) 136 | call nvim_win_close(s:winid, v:true) 137 | endif 138 | if s:win_exists(s:bd_winid) 139 | call nvim_win_close(s:bd_winid, v:true) 140 | endif 141 | if exists('#close_translator_window') 142 | autocmd! close_translator_window 143 | endif 144 | endif 145 | endfunction 146 | 147 | function! translator#window#float#has_scroll() abort 148 | return s:win_exists(s:winid) 149 | endfunction 150 | 151 | function! translator#window#float#scroll(forward, ...) abort 152 | let amount = get(a:, 1, 0) 153 | if !s:win_exists(s:winid) 154 | call translator#util#show_msg('No translator windows') 155 | else 156 | call translator#window#float#scroll_win(s:winid, a:forward, amount) 157 | endif 158 | return mode() =~ '^i' || mode() ==# 'v' ? "" : "\" 159 | endfunction 160 | 161 | function! translator#window#float#scroll_win(winid, forward, amount) abort 162 | let opts = s:get_options(a:winid) 163 | let lines = getbufline(winbufnr(a:winid), 1, '$') 164 | let maxfirst = s:max_firstline(lines, opts['height'], opts['width']) 165 | let topline = opts['topline'] 166 | let height = opts['height'] 167 | let width = opts['width'] 168 | let scrolloff = getwinvar(a:winid, '&scrolloff', 0) 169 | if a:forward && topline >= maxfirst 170 | return 171 | endif 172 | if !a:forward && topline == 1 173 | return 174 | endif 175 | if a:amount == 0 176 | let topline = s:get_topline(opts['topline'], lines, a:forward, height, width) 177 | else 178 | let topline = topline + (a:forward ? a:amount : - a:amount) 179 | endif 180 | let topline = a:forward ? min([maxfirst, topline]) : max([1, topline]) 181 | let lnum = s:get_cursorline(topline, lines, scrolloff, width, height) 182 | call s:win_setview(a:winid, topline, lnum) 183 | let top = s:get_options(a:winid)['topline'] 184 | " not changed 185 | if top == opts['topline'] 186 | if a:forward 187 | call s:win_setview(a:winid, topline + 1, lnum + 1) 188 | else 189 | call s:win_setview(a:winid, topline - 1, lnum - 1) 190 | endif 191 | endif 192 | endfunction 193 | 194 | let s:winid = -1 195 | let s:bd_winid = -1 196 | function! translator#window#float#create(linelist, configs) abort 197 | call s:win_close_float() 198 | 199 | let options = { 200 | \ 'relative': 'editor', 201 | \ 'anchor': a:configs.anchor, 202 | \ 'row': a:configs.row + (a:configs.anchor[0] == 'N' ? 1 : -1), 203 | \ 'col': a:configs.col + (a:configs.anchor[1] == 'W' ? 1 : -1), 204 | \ 'width': a:configs.width - 2, 205 | \ 'height': a:configs.height - 2, 206 | \ 'style':'minimal', 207 | \ 'zindex':32000, 208 | \ } 209 | let bufnr = translator#buffer#create_scratch_buf(a:linelist) 210 | call translator#buffer#init(bufnr) 211 | let winid = nvim_open_win(bufnr, v:false, options) 212 | call translator#window#init(winid) 213 | 214 | let bd_options = { 215 | \ 'relative': 'editor', 216 | \ 'anchor': a:configs.anchor, 217 | \ 'row': a:configs.row, 218 | \ 'col': a:configs.col, 219 | \ 'width': a:configs.width, 220 | \ 'height': a:configs.height, 221 | \ 'focusable': v:false, 222 | \ 'style':'minimal', 223 | \ 'zindex':32000, 224 | \ } 225 | let bd_bufnr = translator#buffer#create_border(a:configs) 226 | let bd_winid = nvim_open_win(bd_bufnr, v:false, bd_options) 227 | call nvim_win_set_option(bd_winid, 'winhl', 'Normal:TranslatorBorder') 228 | 229 | " NOTE: dont use call nvim_set_current_win(s:translator_winid) 230 | noautocmd call win_gotoid(winid) 231 | noautocmd wincmd p 232 | augroup close_translator_window 233 | autocmd! 234 | autocmd CursorMoved,CursorMovedI,InsertEnter,BufLeave 235 | \ call timer_start(100, { -> s:win_close_float() }) 236 | augroup END 237 | let s:winid = winid 238 | let s:bd_winid = bd_winid 239 | return [winid, bd_winid] 240 | endfunction 241 | -------------------------------------------------------------------------------- /autoload/translator/window/popup.vim: -------------------------------------------------------------------------------- 1 | " ============================================================================ 2 | " FileName: popup.vim 3 | " Author: voldikss 4 | " GitHub: https://github.com/voldikss 5 | " ============================================================================ 6 | 7 | function! s:popup_filter(winid, key) abort 8 | if a:key == "\" 9 | call win_execute(a:winid, "normal! \") 10 | return v:true 11 | elseif a:key == "\" 12 | call win_execute(a:winid, "normal! \") 13 | return v:true 14 | elseif a:key == 'q' || a:key == 'x' 15 | return popup_filter_menu(a:winid, 'x') 16 | endif 17 | return v:false 18 | endfunction 19 | 20 | function! translator#window#popup#create(linelist, configs) abort 21 | let options = { 22 | \ 'pos': a:configs.anchor, 23 | \ 'col': 'cursor', 24 | \ 'line': a:configs.anchor[0:2] == 'top' ? 'cursor+1' : 'cursor-1', 25 | \ 'moved': 'any', 26 | \ 'padding': [0, 0, 0, 0], 27 | \ 'maxwidth': a:configs.width - 2, 28 | \ 'minwidth': a:configs.width - 2, 29 | \ 'maxheight': a:configs.height, 30 | \ 'minheight': a:configs.height, 31 | \ 'filter': function('s:popup_filter'), 32 | \ 'borderchars' : a:configs.borderchars, 33 | \ 'border': [1, 1, 1, 1], 34 | \ 'borderhighlight': ['TranslatorBorder'], 35 | \ 'zindex':32000, 36 | \ } 37 | let winid = popup_create('', options) 38 | call translator#window#init(winid) 39 | let bufnr = winbufnr(winid) 40 | call appendbufline(bufnr, 0, a:linelist) 41 | call translator#buffer#init(bufnr) 42 | endfunction 43 | -------------------------------------------------------------------------------- /autoload/translator/window/preview.vim: -------------------------------------------------------------------------------- 1 | " ============================================================================ 2 | " FileName: preview.vim 3 | " Author: voldikss 4 | " GitHub: https://github.com/voldikss 5 | " ============================================================================ 6 | 7 | function! s:win_exists(winid) abort 8 | return !empty(getwininfo(a:winid)) 9 | endfunction 10 | 11 | function! s:win_close_preview() abort 12 | if win_getid() == s:winid 13 | return 14 | else 15 | if s:win_exists(s:winid) 16 | execute win_id2win(s:winid) . 'hide' 17 | endif 18 | if exists('#close_translator_window') 19 | autocmd! close_translator_window 20 | endif 21 | endif 22 | endfunction 23 | 24 | let s:winid = -1 25 | function! translator#window#preview#create(linelist, configs) abort 26 | call s:win_close_preview() 27 | let curr_pos = getpos('.') 28 | noswapfile bo new 29 | set previewwindow 30 | call setpos('.', curr_pos) 31 | wincmd P 32 | execute a:configs.height + 1 . 'wincmd _' 33 | enew! 34 | let s:winid = win_getid() 35 | call append(0, a:linelist) 36 | call setpos('.', [0, 1, 1, 0]) 37 | call translator#buffer#init(bufnr('%')) 38 | call translator#window#init(s:winid) 39 | noautocmd wincmd p 40 | augroup close_translator_window 41 | autocmd! 42 | autocmd CursorMoved,CursorMovedI,InsertEnter,BufLeave 43 | \ call timer_start(100, { -> s:win_close_preview() }) 44 | augroup END 45 | endfunction 46 | -------------------------------------------------------------------------------- /doc/translator.txt: -------------------------------------------------------------------------------- 1 | *translator.txt* Vim/Neovim plugin for translating Last change: 2020-11-16 2 | 3 | Author : voldikss 4 | License: MIT license 5 | NOTE: This is outdated, please refer to the README file: 6 | ../README.md 7 | or 8 | https://github.com/voldikss/vim-translator/blob/master/README.md 9 | ============================================================================== 10 | CONTENTS *translator-contents* 11 | 12 | Introduction |translator-introduction| 13 | Install |translator-install| 14 | Variables |translator-variables| 15 | Keymappings |translator-key-mappings| 16 | Commands |translator-commands| 17 | Highlight |translator-highlight| 18 | Repository |translator-repository| 19 | 20 | 21 | ============================================================================== 22 | INTRODUCTION *translator-introduction* 23 | 24 | Asynchronous translating plugin for Vim/Neovim 25 | 26 | 27 | ============================================================================== 28 | INSTALL *translator-install* 29 | 30 | Using vim-plug: 31 | > 32 | Plug 'voldikss/vim-translator' 33 | < 34 | 35 | ============================================================================== 36 | VARIABLES *translator-variables* 37 | 38 | g:translator_target_lang *g:translator_target_lang* 39 | Type |String| 40 | Refer to https://github.com/voldikss/vim-translator/wiki 41 | Default: 'zh' 42 | 43 | g:translator_source_lang *g:translator_source_lang* 44 | Type |String| 45 | Refer to https://github.com/voldikss/vim-translator/wiki 46 | Default: 'auto' 47 | 48 | g:translator_default_engines *g:translator_default_engines* 49 | Type |List| of |String| 50 | Available: 'bing', 'google', 'haici', 'iciba'(expired), 'sdcv', 51 | 'trans', 'youdao' 52 | Default: If g:translator_target_lang is 'zh', this will be 53 | ['bing', 'google', 'haici', 'youdao'], otherwise ['google'] 54 | 55 | g:translator_proxy_url *g:translator_proxy_url* 56 | Type |String| 57 | E.g. 58 | `let g:translator_proxy_url = 'socks5://127.0.0.1:1080'` 59 | Default: '' 60 | 61 | g:translator_history_enable *g:translator_history_enable* 62 | Type |Boolean| 63 | Default: |v:false| 64 | 65 | g:translator_window_type *g:translator_window_type* 66 | Type |String| 67 | Available: 'popup'(use floatwin in nvim or popup in vim), 68 | 'preview' 69 | Default: 'popup' 70 | 71 | g:translator_window_max_width *g:translator_window_max_width* 72 | Type |Number| 73 | Max width value of the popup/floating window 74 | Default: 0.6*&columns 75 | 76 | g:translator_window_max_height *g:translator_window_max_height* 77 | Type |Number| 78 | Max width value of the popup/floating height 79 | Default: 0.6*&lines 80 | 81 | g:translator_window_borderchars *g:translator_window_borderchars* 82 | Type |List| of |String| 83 | Default: ['─', '│', '─', '│', '┌', '┐', '┘', '└'] 84 | 85 | 86 | ============================================================================== 87 | 88 | MAPPINGS *translator-key-mappings* 89 | 90 | This plugin doesn't supply any default mappings. > 91 | 92 | """ Configuration example 93 | 94 | " Echo translation in the cmdline 95 | nmap t Translate 96 | vmap t TranslateV 97 | " Display translation in a window 98 | nmap w TranslateW 99 | vmap w TranslateWV 100 | " Replace the text with translation 101 | nmap r TranslateR 102 | vmap r TranslateRV 103 | < 104 | 105 | ============================================================================== 106 | COMMANDS *translator-commands* 107 | 108 | Arguments: 109 | [--engines=ENGINES] [--target_lang=TARGET_LANG] [--source_lang=SOURCE_LANG] [your text] 110 | 111 | :Translate *:Translate* 112 | 113 | Translate the `text` from the source language `source_lang` to the target 114 | language `target_lang` with `engine`, echo the result in the cmdline 115 | 116 | - If `engines` is not given, use `g:translator_default_engines` 117 | - If `text` is not given, use the text under the cursor 118 | - If `target_lang` is not given, use `g:translator_target_lang` 119 | 120 | The command can also be passed to a range, i.e., `:'<,'>Translate ...`, 121 | which translates text in visual selection 122 | 123 | If `!` is provided, the plugin will perform a reverse translating by switching `target_lang` and `source_lang` 124 | 125 | Examples(you can use `` to get completion): > 126 | :Translate " translate the word under the cursor 127 | :Translate --engines=google,youdao are you ok " translate text `are you ok` using google and youdao engines 128 | :2Translate ... " translate line 2 129 | :1,3Translate ... " translate line 1 to line 3 130 | :'<,'>Translate ... " translate selected lines 131 | < 132 | 133 | :TranslateW *:TranslateW* 134 | Like |:Translate|, but display the translation in a window 135 | 136 | :TranslateR *:TranslateR* 137 | Like |:Translate|, but replace the current text with the translation 138 | 139 | :TranslateH *:TranslateH* 140 | Export translation history 141 | 142 | :TranslateL *:TranslateL* 143 | 144 | Display log message 145 | 146 | 147 | ============================================================================== 148 | HIGHLIGHT *translator-highlight* 149 | 150 | Here are the default highlight links. 151 | To customize, use |highlight| command. 152 | > 153 | " Text highlight of translator window 154 | hi def link TranslatorQuery Identifier 155 | hi def link TranslatorPhonetic Type 156 | hi def link TranslatorExplain Statement 157 | hi def link TranslatorDelimiter Special 158 | 159 | " Background of translator window border 160 | hi def link Translator Normal 161 | hi def link TranslatorBorder NormalFloat 162 | 163 | 164 | ============================================================================== 165 | REPOSITORY *translator-repository-page* 166 | 167 | |vim-translator| is developed on GitHub. 168 | 169 | https://github.com/voldikss/vim-translator 170 | 171 | 172 | ============================================================================== 173 | vim:tw=78:nosta:noet:ts=4:sts=0:ft=help:noet:fen:fdm=marker: 174 | -------------------------------------------------------------------------------- /plugin/translator.vim: -------------------------------------------------------------------------------- 1 | " ============================================================================ 2 | " FileName: translator.vim 3 | " Author: voldikss 4 | " GitHub: https://github.com/voldikss 5 | " ============================================================================ 6 | 7 | scriptencoding utf-8 8 | 9 | if exists('g:loaded_translator') 10 | finish 11 | endif 12 | let g:loaded_translator= 1 13 | 14 | let g:translator_history_enable = get(g:, 'translator_history_enable', v:false) 15 | let g:translator_proxy_url = get(g:, 'translator_proxy_url', '') 16 | let g:translator_source_lang = get(g:, 'translator_source_lang', 'auto') 17 | let g:translator_target_lang = get(g:, 'translator_target_lang', 'zh') 18 | let g:translator_translate_shell_options = get(g:, 'translator_translate_shell_options', []) 19 | let g:translator_window_borderchars = get(g:, 'translator_window_borderchars', ['─', '│', '─', '│', '┌', '┐', '┘', '└']) 20 | let g:translator_window_max_height = get(g:, 'translator_window_max_height', 999) 21 | let g:translator_window_max_width = get(g:, 'translator_window_max_width', 999) 22 | let g:translator_window_type = get(g:, 'translator_window_type', 'popup') 23 | 24 | if match(g:translator_target_lang, 'zh') >= 0 25 | let g:translator_default_engines = get(g:, 'translator_default_engines', ['bing', 'google', 'haici', 'youdao']) 26 | else 27 | let g:translator_default_engines = get(g:, 'translator_default_engines', ['google']) 28 | endif 29 | 30 | let g:translator_status = '' 31 | 32 | nnoremap Translate :Translate 33 | vnoremap TranslateV :Translate 34 | nnoremap TranslateW :TranslateW 35 | vnoremap TranslateWV :TranslateW 36 | nnoremap TranslateR viw:TranslateR 37 | vnoremap TranslateRV :TranslateR 38 | nnoremap TranslateX :TranslateX 39 | 40 | command! -complete=customlist,translator#cmdline#complete -nargs=* -bang -range 41 | \ Translate 42 | \ call translator#start('echo', 0, , , , ) 43 | 44 | command! -complete=customlist,translator#cmdline#complete -nargs=* -bang -range 45 | \ TranslateW 46 | \ call translator#start('window', 0, , , , ) 47 | 48 | command! -complete=customlist,translator#cmdline#complete -nargs=* -bang -range 49 | \ TranslateR 50 | \ call translator#start('replace', 0, , , , ) 51 | 52 | command! -complete=customlist,translator#cmdline#complete -nargs=* -bang -range 53 | \ TranslateX 54 | \ call translator#start('echo', 0, , , , . ' ' . @*) 55 | 56 | command! -nargs=0 TranslateH call translator#history#export() 57 | 58 | command! -nargs=0 TranslateL call translator#logger#open_log() 59 | -------------------------------------------------------------------------------- /script/translator.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import re 3 | import threading 4 | import socket 5 | import sys 6 | import time 7 | import os 8 | import random 9 | import copy 10 | import json 11 | import argparse 12 | import codecs 13 | 14 | if sys.version_info.major < 3: 15 | is_py3 = False 16 | reload(sys) 17 | sys.setdefaultencoding("utf-8") 18 | sys.stdout = codecs.getwriter("utf-8")(sys.stdout) 19 | sys.stderr = codecs.getwriter("utf-8")(sys.stderr) 20 | from urlparse import urlparse 21 | from urllib import urlencode 22 | from urllib import quote_plus as url_quote 23 | from urllib2 import urlopen 24 | from urllib2 import Request 25 | from urllib2 import URLError 26 | from urllib2 import HTTPError 27 | else: 28 | is_py3 = True 29 | sys.stdout = codecs.getwriter("utf-8")(sys.stdout.buffer) 30 | sys.stderr = codecs.getwriter("utf-8")(sys.stderr.buffer) 31 | from urllib.parse import urlencode 32 | from urllib.parse import quote_plus as url_quote 33 | from urllib.parse import urlparse 34 | from urllib.request import urlopen 35 | from urllib.request import Request 36 | from urllib.error import URLError 37 | from urllib.error import HTTPError 38 | 39 | 40 | class BaseTranslator(object): 41 | def __init__(self, name): 42 | self._name = name 43 | self._proxy_url = None 44 | self._agent = ( 45 | "Mozilla/5.0 (X11; Linux x86_64; rv:50.0) Gecko/20100101 Firefox/50.0" 46 | ) 47 | 48 | def request(self, url, data=None, post=False, header=None): 49 | if header: 50 | header = copy.deepcopy(header) 51 | else: 52 | header = {} 53 | header[ 54 | "User-Agent" 55 | ] = "Mozilla/5.0 (X11; Linux x86_64) \ 56 | AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36" 57 | 58 | if post: 59 | if data: 60 | data = urlencode(data).encode("utf-8") 61 | else: 62 | if data: 63 | query_string = urlencode(data) 64 | url = url + "?" + query_string 65 | data = None 66 | 67 | req = Request(url, data, header) 68 | 69 | try: 70 | r = urlopen(req, timeout=5) 71 | except (URLError, HTTPError, socket.timeout): 72 | sys.stderr.write( 73 | "Engine %s timed out, please check your network\n" % self._name 74 | ) 75 | return None 76 | 77 | if is_py3: 78 | charset = r.headers.get_param("charset") or "utf-8" 79 | else: 80 | charset = r.headers.getparam("charset") or "utf-8" 81 | 82 | r = r.read().decode(charset) 83 | return r 84 | 85 | def http_get(self, url, data=None, header=None): 86 | return self.request(url, data, False, header) 87 | 88 | def http_post(self, url, data=None, header=None): 89 | return self.request(url, data, True, header) 90 | 91 | def set_proxy(self, proxy_url=None): 92 | try: 93 | import socks 94 | except ImportError: 95 | sys.stderr.write("pySocks module should be installed\n") 96 | return None 97 | 98 | try: 99 | import ssl 100 | 101 | ssl._create_default_https_context = ssl._create_unverified_context 102 | except Exception: 103 | pass 104 | 105 | self._proxy_url = proxy_url 106 | 107 | proxy_types = { 108 | "http": socks.PROXY_TYPE_HTTP, 109 | "socks": socks.PROXY_TYPE_SOCKS4, 110 | "socks4": socks.PROXY_TYPE_SOCKS4, 111 | "socks5": socks.PROXY_TYPE_SOCKS5, 112 | } 113 | 114 | url_component = urlparse(proxy_url) 115 | 116 | proxy_args = { 117 | "proxy_type": proxy_types[url_component.scheme], 118 | "addr": url_component.hostname, 119 | "port": url_component.port, 120 | "username": url_component.username, 121 | "password": url_component.password, 122 | } 123 | 124 | socks.set_default_proxy(**proxy_args) 125 | socket.socket = socks.socksocket 126 | 127 | def test_request(self, test_url): 128 | print("test url: %s" % test_url) 129 | print(self.request(test_url)) 130 | 131 | def create_translation(self, sl="auto", tl="auto", text=""): 132 | res = {} 133 | res["engine"] = self._name 134 | res["sl"] = sl # 来源语言 135 | res["tl"] = tl # 目标语言 136 | res["text"] = text # 需要翻译的文本 137 | res["phonetic"] = "" # 音标 138 | res["paraphrase"] = "" # 简单释义 139 | res["explains"] = [] # 分行解释 140 | return res 141 | 142 | # 翻译结果:需要填充如下字段 143 | def translate(self, sl, tl, text): 144 | return self.create_translation(sl, tl, text) 145 | 146 | def md5sum(self, text): 147 | import hashlib 148 | 149 | m = hashlib.md5() 150 | if sys.version_info[0] < 3: 151 | if isinstance(text, unicode): # noqa: F821 152 | text = text.encode("utf-8") 153 | else: 154 | if isinstance(text, str): 155 | text = text.encode("utf-8") 156 | m.update(text) 157 | return m.hexdigest() 158 | 159 | def html_unescape(self, text): 160 | # https://stackoverflow.com/questions/2087370/decode-html-entities-in-python-string 161 | # Python 3.4+ 162 | if sys.version_info[0] >= 3 and sys.version_info[1] >= 4: 163 | import html 164 | 165 | return html.unescape(text) 166 | else: 167 | try: 168 | # Python 2.6-2.7 169 | from HTMLParser import HTMLParser 170 | except ImportError: 171 | # Python 3 172 | from html.parser import HTMLParser 173 | h = HTMLParser() 174 | return h.unescape(text) 175 | 176 | 177 | # NOTE: expired 178 | class BaicizhanTranslator(BaseTranslator): 179 | def __init__(self): 180 | super(BaicizhanTranslator, self).__init__("baicizhan") 181 | 182 | def translate(self, sl, tl, text, options=None): 183 | url = "http://mall.baicizhan.com/ws/search" 184 | req = {} 185 | req["w"] = url_quote(text) 186 | resp = self.http_get(url, req, None) 187 | if not resp: 188 | return None 189 | try: 190 | obj = json.loads(resp) 191 | except: 192 | return None 193 | 194 | res = self.create_translation(sl, tl, text) 195 | res["phonetic"] = self.get_phonetic(obj) 196 | res["explains"] = self.get_explains(obj) 197 | return res 198 | 199 | def get_phonetic(self, obj): 200 | return obj["accent"] if "accent" in obj else "" 201 | 202 | def get_explains(self, obj): 203 | return ["; ".join(obj["mean_cn"].split("\n"))] if "mean_cn" in obj else [] 204 | 205 | 206 | class BingDict(BaseTranslator): 207 | def __init__(self): 208 | super(BingDict, self).__init__("bing") 209 | self._url = "http://bing.com/dict/SerpHoverTrans" 210 | self._cnurl = "http://cn.bing.com/dict/SerpHoverTrans" 211 | 212 | def translate(self, sl, tl, text, options=None): 213 | url = self._cnurl if "zh" in tl else self._url 214 | url = url + "?q=" + url_quote(text) 215 | headers = { 216 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", 217 | "Accept-Language": "en-US,en;q=0.5", 218 | } 219 | resp = self.http_get(url, None, headers) 220 | if not resp: 221 | return None 222 | res = self.create_translation(sl, tl, text) 223 | res["phonetic"] = self.get_phonetic(resp) 224 | res["explains"] = self.get_explains(resp) 225 | return res 226 | 227 | def get_phonetic(self, html): 228 | if not html: 229 | return "" 230 | m = re.findall(r'\[(.*?)\] ', html) 231 | if not m: 232 | return "" 233 | return self.html_unescape(m[0].strip()) 234 | 235 | def get_explains(self, html): 236 | if not html: 237 | return [] 238 | m = re.findall( 239 | r'(.*?)(.*?)', html 240 | ) 241 | expls = [] 242 | for item in m: 243 | expls.append("%s %s" % item) 244 | return expls 245 | 246 | 247 | class GoogleTranslator(BaseTranslator): 248 | def __init__(self): 249 | super(GoogleTranslator, self).__init__("google") 250 | self._host = "translate.googleapis.com" 251 | self._cnhost = "translate.googleapis.com" 252 | 253 | def get_url(self, sl, tl, qry): 254 | http_host = self._cnhost if "zh" in tl else self._host 255 | qry = url_quote(qry) 256 | url = ( 257 | "https://{}/translate_a/single?client=gtx&sl={}&tl={}&dt=at&dt=bd&dt=ex&" 258 | "dt=ld&dt=md&dt=qca&dt=rw&dt=rm&dt=ss&dt=t&q={}".format( 259 | http_host, sl, tl, qry 260 | ) 261 | ) 262 | return url 263 | 264 | def translate(self, sl, tl, text, options=None): 265 | url = self.get_url(sl, tl, text) 266 | resp = self.http_get(url) 267 | if not resp: 268 | return None 269 | try: 270 | obj = json.loads(resp) 271 | except: 272 | return None 273 | 274 | res = self.create_translation(sl, tl, text) 275 | res["paraphrase"] = self.get_paraphrase(obj) 276 | res["explains"] = self.get_explains(obj) 277 | res["phonetic"] = self.get_phonetic(obj) 278 | res["detail"] = self.get_detail(obj) 279 | res["alternative"] = self.get_alternative(obj) 280 | return res 281 | 282 | def get_phonetic(self, obj): 283 | for x in obj[0]: 284 | if len(x) == 4: 285 | return x[3] 286 | return "" 287 | 288 | def get_paraphrase(self, obj): 289 | paraphrase = "" 290 | for x in obj[0]: 291 | if x[0]: 292 | paraphrase += x[0] 293 | return paraphrase 294 | 295 | def get_explains(self, obj): 296 | explains = [] 297 | if obj[1]: 298 | for x in obj[1]: 299 | expl = "[{}] ".format(x[0][0]) 300 | for i in x[2]: 301 | expl += i[0] + ";" 302 | explains.append(expl) 303 | return explains 304 | 305 | def get_detail(self, resp): 306 | if len(resp) < 13 or resp[12] is None: 307 | return [] 308 | result = [] 309 | for x in resp[12]: 310 | result.append("[{}]".format(x[0])) 311 | for y in x[1]: 312 | result.append("- {}".format(y[0])) 313 | if len(y) >= 3: 314 | result.append(" * {}".format(y[2])) 315 | return result 316 | 317 | def get_alternative(self, resp): 318 | if len(resp) < 6 or resp[5] is None: 319 | return [] 320 | definition = self.get_paraphrase(resp) 321 | result = [] 322 | for x in resp[5]: 323 | # result.append('- {}'.format(x[0])) 324 | for i in x[2]: 325 | if i[0] != definition: 326 | result.append(" * {}".format(i[0])) 327 | return result 328 | 329 | 330 | class HaiciDict(BaseTranslator): 331 | def __init__(self): 332 | super(HaiciDict, self).__init__("haici") 333 | 334 | def translate(self, sl, tl, text, options=None): 335 | url = "http://dict.cn/mini.php" 336 | req = {} 337 | req["q"] = url_quote(text) 338 | resp = self.http_get(url, req) 339 | if not resp: 340 | return 341 | 342 | res = self.create_translation(sl, tl, text) 343 | res["phonetic"] = self.get_phonetic(resp) 344 | res["explains"] = self.get_explains(resp) 345 | return res 346 | 347 | def get_phonetic(self, html): 348 | m = re.findall(r" \[(.*?)\]", html) 349 | return m[0] if m else "" 350 | 351 | def get_explains(self, html): 352 | m = re.findall(r'
(.*?)
', html) 353 | explains = [] 354 | for item in m: 355 | for e in item.split("
"): 356 | explains.append(e) 357 | return explains 358 | 359 | 360 | # NOTE: deprecated 361 | class ICibaTranslator(BaseTranslator): 362 | def __init__(self): 363 | super(ICibaTranslator, self).__init__("iciba") 364 | 365 | def translate(self, sl, tl, text, options=None): 366 | url = "http://www.iciba.com/index.php" 367 | req = {} 368 | req["a"] = "getWordMean" 369 | req["c"] = "search" 370 | req["word"] = url_quote(text) 371 | resp = self.http_get(url, req, None) 372 | if not resp: 373 | return None 374 | try: 375 | obj = json.loads(resp) 376 | obj = obj["baesInfo"]["symbols"][0] 377 | except: 378 | return None 379 | 380 | res = self.create_translation(sl, tl, text) 381 | res["paraphrase"] = self.get_paraphrase(obj) 382 | res["phonetic"] = self.get_phonetic(obj) 383 | res["explains"] = self.get_explains(obj) 384 | return res 385 | 386 | def get_paraphrase(self, obj): 387 | try: 388 | return obj["parts"][0]["means"][0] 389 | except: 390 | return "" 391 | 392 | def get_phonetic(self, obj): 393 | return obj["ph_en"] if "ph_en" in obj else "" 394 | 395 | def get_explains(self, obj): 396 | parts = obj["parts"] if "parts" in obj else [] 397 | explains = [] 398 | for part in parts: 399 | explains.append(part["part"] + ", ".join(part["means"])) 400 | return explains 401 | 402 | 403 | class YoudaoTranslator(BaseTranslator): 404 | def __init__(self): 405 | super(YoudaoTranslator, self).__init__("youdao") 406 | self.url = "https://fanyi.youdao.com/translate_o" 407 | self.D = "97_3(jkMYg@T[KZQmqjTK" 408 | # 备用 self.D = "n%A-rKaT5fb[Gy?;N5@Tj" 409 | 410 | def sign(self, text, salt): 411 | s = "fanyideskweb" + text + salt + self.D 412 | return self.md5sum(s) 413 | 414 | def translate(self, sl, tl, text, options=None): 415 | salt = str(int(time.time() * 1000) + random.randint(0, 10)) 416 | sign = self.sign(text, salt) 417 | header = { 418 | "Cookie": "OUTFOX_SEARCH_USER_ID=-2022895048@10.168.8.76;", 419 | "Referer": "http://fanyi.youdao.com/", 420 | "User-Agent": "Mozilla/5.0 (Windows NT 6.2; rv:51.0) Gecko/20100101 Firefox/51.0", 421 | } 422 | data = { 423 | "i": url_quote(text), 424 | "from": sl, 425 | "to": tl, 426 | "smartresult": "dict", 427 | "client": "fanyideskweb", 428 | "salt": salt, 429 | "sign": sign, 430 | "doctype": "json", 431 | "version": "2.1", 432 | "keyfrom": "fanyi.web", 433 | "action": "FY_BY_CL1CKBUTTON", 434 | "typoResult": "true", 435 | } 436 | resp = self.http_post(self.url, data, header) 437 | if not resp: 438 | return 439 | try: 440 | obj = json.loads(resp) 441 | except: 442 | return None 443 | 444 | res = self.create_translation(sl, tl, text) 445 | res["paraphrase"] = self.get_paraphrase(obj) 446 | res["explains"] = self.get_explains(obj) 447 | return res 448 | 449 | def get_paraphrase(self, obj): 450 | translation = "" 451 | t = obj.get("translateResult") 452 | if t: 453 | for n in t: 454 | part = [] 455 | for m in n: 456 | x = m.get("tgt") 457 | if x: 458 | part.append(x) 459 | if part: 460 | translation += ", ".join(part) 461 | return translation 462 | 463 | def get_explains(self, obj): 464 | explains = [] 465 | if "smartResult" in obj: 466 | smarts = obj["smartResult"]["entries"] 467 | for entry in smarts: 468 | if entry: 469 | entry = entry.replace("\r", "") 470 | entry = entry.replace("\n", "") 471 | explains.append(entry) 472 | return explains 473 | 474 | 475 | class TranslateShell(BaseTranslator): 476 | def __init__(self): 477 | super(TranslateShell, self).__init__("trans") 478 | 479 | def translate(self, sl, tl, text, options=None): 480 | if not options: 481 | options = [] 482 | 483 | if self._proxy_url: 484 | options.append("-proxy {}".format(self._proxy_url)) 485 | 486 | default_opts = [ 487 | "-no-ansi", 488 | "-no-theme", 489 | "-show-languages n", 490 | "-show-prompt-message n", 491 | "-show-translation-phonetics n", 492 | "-hl {}".format(tl), 493 | ] 494 | options = default_opts + options 495 | source_lang = "" if sl == "auto" else sl 496 | cmd = "trans {} {}:{} '{}'".format(" ".join(options), source_lang, tl, text) 497 | run = os.popen(cmd) 498 | lines = [] 499 | for line in run.readlines(): 500 | line = re.sub(r"[\t\n]", "", line) 501 | line = re.sub(r"\v.*", "", line) 502 | line = re.sub(r"^\s*", "", line) 503 | lines.append(line) 504 | res = self.create_translation(sl, tl, text) 505 | res["explains"] = lines 506 | run.close() 507 | return res 508 | 509 | 510 | class SdcvShell(BaseTranslator): 511 | def __init__(self): 512 | super(SdcvShell, self).__init__("sdcv") 513 | 514 | def get_dictionary(self, sl, tl, text): 515 | """get dictionary of sdcv 516 | 517 | :sl: source_lang 518 | :tl: target_lang 519 | :returns: dictionary 520 | 521 | """ 522 | dictionary = "" 523 | if sl == "": 524 | try: 525 | import langdetect 526 | except ImportError: 527 | sys.stderr.write("langdetect module should be installed\n") 528 | return None 529 | sl = langdetect.detect(text) 530 | 531 | if (sl == "en") & (tl == "zh"): 532 | dictionary = "朗道英汉字典5.0" 533 | elif (sl == "zh_cn") & (tl == "en"): 534 | dictionary = "朗道汉英字典5.0" 535 | elif (sl == "en") & (tl == "ja"): 536 | dictionary = "jmdict-en-ja" 537 | elif (sl == "ja") & (tl == "en"): 538 | dictionary = "jmdict-ja-en" 539 | return dictionary 540 | 541 | def translate(self, sl, tl, text, options=None): 542 | if not options: 543 | options = [] 544 | 545 | if self._proxy_url: 546 | options.append("-proxy {}".format(self._proxy_url)) 547 | 548 | source_lang = "" if sl == "auto" else sl 549 | dictionary = self.get_dictionary(source_lang, tl, text) 550 | if dictionary == "": 551 | default_opts = [] 552 | else: 553 | default_opts = [" ".join(["-u", dictionary])] 554 | options = default_opts + options 555 | cmd = "sdcv {} '{}'".format(" ".join(options), text) 556 | run = os.popen(cmd) 557 | lines = [] 558 | for line in run.readlines(): 559 | line = re.sub(r"^Found.*", "", line) 560 | line = re.sub(r"^-->.*", "", line) 561 | line = re.sub(r"^\s*", "", line) 562 | line = re.sub(r"^\*", "", line) 563 | lines.append(line) 564 | res = self.create_translation(sl, tl, text) 565 | res["explains"] = lines 566 | run.close() 567 | return res 568 | 569 | 570 | ENGINES = { 571 | "baicizhan": BaicizhanTranslator, 572 | "bing": BingDict, 573 | "haici": HaiciDict, 574 | "google": GoogleTranslator, 575 | "iciba": ICibaTranslator, 576 | "sdcv": SdcvShell, 577 | "trans": TranslateShell, 578 | "youdao": YoudaoTranslator, 579 | } 580 | 581 | 582 | def sanitize_input_text(text): 583 | while True: 584 | try: 585 | text.encode() 586 | break 587 | except UnicodeEncodeError: 588 | text = text[:-1] 589 | return text 590 | 591 | 592 | def main(): 593 | parser = argparse.ArgumentParser() 594 | parser.add_argument("--engines", nargs="+", required=False, default=["google"]) 595 | parser.add_argument("--target_lang", required=False, default="zh") 596 | parser.add_argument("--source_lang", required=False, default="en") 597 | parser.add_argument("--proxy", required=False) 598 | parser.add_argument("--options", type=str, default=None, required=False) 599 | parser.add_argument("text", nargs="+", type=str) 600 | args = parser.parse_args() 601 | 602 | args.text = [ sanitize_input_text(x) for x in args.text ] 603 | 604 | text = " ".join(args.text).strip("'").strip('"').strip() 605 | text = re.sub(r"([a-z])([A-Z][a-z])", r"\1 \2", text) 606 | text = re.sub(r"([a-zA-Z])_([a-zA-Z])", r"\1 \2", text).lower() 607 | engines = args.engines 608 | to_lang = args.target_lang 609 | from_lang = args.source_lang 610 | if args.options: 611 | options = args.options.split(",") 612 | else: 613 | options = [] 614 | 615 | translation = {} 616 | translation["text"] = text 617 | translation["status"] = 1 618 | translation["results"] = [] 619 | 620 | def runner(translator): 621 | res = translator.translate(from_lang, to_lang, text, options) 622 | if res: 623 | translation["results"].append(copy.deepcopy(res)) 624 | else: 625 | translation["status"] = 0 626 | 627 | threads = [] 628 | for e in engines: 629 | cls = ENGINES.get(e) 630 | if not cls: 631 | sys.stderr.write("Invalid engine name %s\n" % e) 632 | continue 633 | translator = cls() 634 | if args.proxy: 635 | translator.set_proxy(args.proxy) 636 | 637 | t = threading.Thread(target=runner, args=(translator,)) 638 | threads.append(t) 639 | 640 | list(map(lambda x: x.start(), threads)) 641 | list(map(lambda x: x.join(), threads)) 642 | 643 | sys.stdout.write(json.dumps(translation)) 644 | 645 | 646 | if __name__ == "__main__": 647 | 648 | def test0(): 649 | t = BaseTranslator("test_proxy") 650 | t.set_proxy("http://localhost:8087") 651 | t.test_request("https://www.google.com") 652 | 653 | def test1(): 654 | t = BaicizhanTranslator() 655 | r = t.translate("", "zh", "naive") 656 | print(r) 657 | 658 | def test2(): 659 | t = BingDict() 660 | r = t.translate("", "", "naive") 661 | print(r) 662 | 663 | def test3(): 664 | gt = GoogleTranslator() 665 | r = gt.translate("auto", "zh", "filencodings") 666 | print(r) 667 | 668 | def test4(): 669 | t = HaiciDict() 670 | r = t.translate("", "zh", "naive") 671 | print(r) 672 | 673 | def test5(): 674 | t = ICibaTranslator() 675 | r = t.translate("", "", "naive") 676 | print(r) 677 | 678 | def test6(): 679 | t = TranslateShell() 680 | r = t.translate("auto", "zh", "naive") 681 | print(r) 682 | 683 | def test7(): 684 | t = YoudaoTranslator() 685 | r = t.translate("auto", "zh", "naive") 686 | print(r) 687 | 688 | # test3() 689 | main() 690 | -------------------------------------------------------------------------------- /syntax/translator.vim: -------------------------------------------------------------------------------- 1 | " ============================================================================ 2 | " FileName: translator.vim 3 | " Author: voldikss 4 | " GitHub: https://github.com/voldikss 5 | " ============================================================================ 6 | 7 | scriptencoding utf-8 8 | 9 | if exists('b:current_syntax') 10 | finish 11 | endif 12 | 13 | syntax match TranslatorQuery /\v⟦.*⟧/ 14 | syntax match TranslatorDelimiter /\v\─.*\─/ 15 | 16 | hi def link TranslatorQuery Identifier 17 | hi def link TranslatorDelimiter Comment 18 | 19 | hi def link Translator Normal 20 | hi def link TranslatorBorder NormalFloat 21 | 22 | let b:current_syntax = 'translator' 23 | -------------------------------------------------------------------------------- /test/default_config.vader: -------------------------------------------------------------------------------- 1 | Before (Default configurations): 2 | let g:translator_target_lang = 'zh' 3 | let g:translator_source_lang = 'auto' 4 | let g:translator_default_engines = ['ciba', 'youdao'] 5 | let g:translator_proxy_url = '' 6 | let g:translator_history_enable = v:false 7 | let g:translator_translate_shell_options = [] 8 | let g:translator_window_max_width = 0.6 9 | let g:translator_window_max_height = 0.6 10 | let g:translator_window_borderchars = ['─', '│', '─', '│', '┌', '┐', '┘', '└'] 11 | -------------------------------------------------------------------------------- /test/test_translator.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sys 3 | import copy 4 | import os 5 | import unittest 6 | 7 | curr_filename = os.path.abspath(__file__) 8 | curr_dir = os.path.dirname(curr_filename) 9 | script_path = os.path.join(curr_dir, "../script") 10 | sys.path.append(script_path) 11 | 12 | from translator import BaicizhanTranslator 13 | from translator import BingDict 14 | from translator import GoogleTranslator 15 | from translator import HaiciDict 16 | from translator import ICibaTranslator 17 | from translator import YoudaoTranslator 18 | from translator import TranslateShell 19 | 20 | 21 | class TestTranslator(unittest.TestCase): 22 | def __init__(self, *args, **kwargs): 23 | super(TestTranslator, self).__init__(*args, **kwargs) 24 | 25 | @unittest.skip("Expired") 26 | def test_baicizhan(self): 27 | t = BaicizhanTranslator() 28 | r = t.translate("", "", "naive") 29 | self.assertTrue(len(r['paraphrase']) != 0 or len(r['explains'])) 30 | 31 | @unittest.skip("Skip for GitHub Action") 32 | def test_bing(self): 33 | t = BingDict() 34 | r = t.translate("", "", "naive") 35 | self.assertTrue(len(r['paraphrase']) != 0 or len(r['explains'])) 36 | 37 | def test_google(self): 38 | t = GoogleTranslator() 39 | r = t.translate("auto", "zh", "naive") 40 | self.assertTrue(len(r['paraphrase']) != 0 or len(r['explains'])) 41 | 42 | def test_haici(self): 43 | t = HaiciDict() 44 | r = t.translate("", "zh", "naive") 45 | self.assertTrue(len(r['paraphrase']) != 0 or len(r['explains'])) 46 | 47 | @unittest.skip("ciba api was deprecated") 48 | def test_iciba(self): 49 | t = ICibaTranslator() 50 | r = t.translate("", "", "naive") 51 | self.assertTrue(len(r['paraphrase']) != 0 or len(r['explains'])) 52 | 53 | def test_translate_shell(self): 54 | t = TranslateShell() 55 | r = t.translate("auto", "zh", "naive") 56 | self.maxDiff = None 57 | self.assertTrue(len(r['paraphrase']) != 0 or len(r['explains'])) 58 | 59 | def test_youdao(self): 60 | t = YoudaoTranslator() 61 | r = t.translate("auto", "zh", "naive") 62 | self.assertTrue(len(r['paraphrase']) != 0 or len(r['explains'])) 63 | 64 | 65 | if __name__ == "__main__": 66 | unittest.main() 67 | -------------------------------------------------------------------------------- /test/translator_history.vader: -------------------------------------------------------------------------------- 1 | Include: default_config.vader 2 | 3 | 4 | Before: 5 | let translations = { 6 | \ 'status': 1, 7 | \ 'results': [ 8 | \ { 9 | \ 'engine': 'ciba', 10 | \ 'paraphrase': '', 11 | \ 'phonetic': 'ˈɪmpɔ:t', 12 | \ 'explains': ['n. 进口,进口商品;输入;重要性;意义;', 'vt. 输入,进口;对…有重大关系;意味着;', 'vi. 具重要性;'] 13 | \ }, 14 | \ { 15 | \ 'engine': 'youdao', 16 | \ 'phonetic': '', 17 | \ 'paraphrase': '进口', 18 | \ 'explains': ['n. 进口,进口货;输入;意思,含义;重要性', 'vt. 输入,进口;含…的意思', 'vi. 输入,进口'] 19 | \ }], 20 | \ 'text': 'import' 21 | \ } 22 | 23 | 24 | Execute (Save history): 25 | let g:translator_history_enable = v:true 26 | call translator#history#save(translations) 27 | 28 | 29 | Execute (Export history): 30 | call translator#history#export() 31 | AssertEqual 'translator_history', &filetype 32 | AssertEqual 'import n. 进口,进口商品;输入;重要性;意义;', getline('.') 33 | normal! ^ 34 | AssertEqual 'TranslateHistoryQuery', SyntaxAt() 35 | normal! $ 36 | AssertEqual 'TranslateHistoryTrans', SyntaxAt() 37 | 38 | 39 | Execute (Exit): 40 | sleep 100m 41 | -------------------------------------------------------------------------------- /test/translator_ui.vader: -------------------------------------------------------------------------------- 1 | Include: default_config.vader 2 | 3 | 4 | Before: 5 | let translations = { 6 | \ 'status': 1, 7 | \ 'results': [ 8 | \ { 9 | \ 'engine': 'ciba', 10 | \ 'paraphrase': '', 11 | \ 'phonetic': 'ˈɪmpɔ:t', 12 | \ 'explains': ['n. 进口,进口商品;输入;重要性;意义;', 'vt. 输入,进口;对…有重大关系;意味着;', 'vi. 具重要性;'] 13 | \ }, 14 | \ { 15 | \ 'engine': 'youdao', 16 | \ 'phonetic': '', 17 | \ 'paraphrase': '进口', 18 | \ 'explains': ['n. 进口,进口货;输入;意思,含义;重要性', 'vt. 输入,进口;含…的意思', 'vi. 输入,进口'] 19 | \ }], 20 | \ 'text': 'import' 21 | \ } 22 | 23 | 24 | function! HasBuffer(filetype) 25 | let found_bufnr = -1 26 | for bufnr in range(1, bufnr('$')) 27 | if getbufvar(bufnr, '&filetype') == a:filetype 28 | let found_bufnr = bufnr 29 | endif 30 | endfor 31 | return found_bufnr 32 | endfunction 33 | 34 | 35 | Execute (Floating: Basic): 36 | call translator#action#window(translations) 37 | Assert HasBuffer('translatorborder') > -1 38 | Assert HasBuffer('translator') > -1 39 | doautocmd CursorMoved 40 | sleep 300m 41 | Assert HasBuffer('translatorborder') == -1 42 | Assert HasBuffer('translator') == -1 43 | 44 | 45 | Execute (Floating: Jump into floating window): 46 | call translator#action#window(translations) 47 | wincmd p 48 | AssertEqual 'translator', &filetype 49 | AssertEqual 1, &wrap 50 | AssertEqual 3, &conceallevel 51 | Assert HasBuffer('translator') > -1 52 | Assert HasBuffer('translatorborder') > -1 53 | wincmd p 54 | sleep 300m 55 | Assert HasBuffer('translator') == -1 56 | Assert HasBuffer('translatorborder') == -1 57 | 58 | 59 | Execute (Exit): 60 | sleep 100m 61 | -------------------------------------------------------------------------------- /test/translator_util.vader: -------------------------------------------------------------------------------- 1 | Include: default_config.vader 2 | 3 | 4 | Execute (Call-only functions): 5 | call translator#util#show_msg('test msg') 6 | call translator#util#show_msg('test msg', 'info') 7 | call translator#util#show_msg('test msg', 'warning') 8 | call translator#util#show_msg('test msg', 'error') 9 | 10 | 11 | Execute (Test translator#util#pad): 12 | AssertEqual '*abc*', translator#util#pad('abc', 5, '*') 13 | AssertEqual '*abc**', translator#util#pad('abc', 6, '*') 14 | 15 | 16 | Execute (Test translator#util#fit_lines): 17 | let linelist = [ 18 | \ '⟦ call ⟧', 19 | \ '', 20 | \ '─── ciba ───', 21 | \ '• [kɔ:l]', 22 | \ '• v. 呼唤,喊叫;召唤,叫来,召集;下令,命令;打电话给;', 23 | \ '• n. 喊叫,大声喊;电话联络;必要,理由;要求;', 24 | \ '', 25 | \ '─── youdao ───', 26 | \ '• 调用', 27 | \ '• n. 电话;呼叫;要求;访问', 28 | \ '• vi. 呼叫;拜访;叫牌', 29 | \ '• vt. 呼叫;称呼;召集' 30 | \ ] 31 | let fit_linelist = [ 32 | \ ' ⟦ call ⟧ ', 33 | \ '', 34 | \ '─────────────────────── ciba ────────────────────────', 35 | \ '• [kɔ:l]', 36 | \ '• v. 呼唤,喊叫;召唤,叫来,召集;下令,命令;打电话给;', 37 | \ '• n. 喊叫,大声喊;电话联络;必要,理由;要求;', 38 | \ '', 39 | \ '────────────────────── youdao ───────────────────────', 40 | \ '• 调用', 41 | \ '• n. 电话;呼叫;要求;访问', 42 | \ '• vi. 呼叫;拜访;叫牌', 43 | \ '• vt. 呼叫;称呼;召集' 44 | \ ] 45 | AssertEqual fit_linelist, translator#util#fit_lines(linelist, 53) 46 | 47 | 48 | Execute (Test translator#util#safe_trim): 49 | AssertEqual 'abc', translator#util#safe_trim(" abc \n ") 50 | 51 | 52 | Given: 53 | 'How are you' 54 | Do: 55 | /are\ 56 | viw 57 | \ 58 | Then (Test translator#util#visual_select): 59 | AssertEqual 'are', translator#util#visual_select(0, 0, 0) 60 | 61 | 62 | Execute (Exit): 63 | sleep 100m 64 | -------------------------------------------------------------------------------- /test/vimrc: -------------------------------------------------------------------------------- 1 | filetype off 2 | let &runtimepath .= ',' . expand(':p:h:h') 3 | let &runtimepath .= ',' . expand(':p:h:h') . '/vader.vim' 4 | filetype plugin indent on 5 | syntax enable 6 | --------------------------------------------------------------------------------