Skip to content

Translator on Emacs. Supports multiple engines such as Google, Bing, deepL, ChatGPT, StarDict, Youdao and so on.

License

Notifications You must be signed in to change notification settings

lorniu/go-translate

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

https://melpa.org/packages/go-translate-badge.svg https://stable.melpa.org/packages/go-translate-badge.svg

Go Translate

This is a translation framework on Emacs, with high configurability and extensibility.

点击查看《中文版文档》

As a translation framework, it offers many advantages:

  • Supports multiple translation engines, including ChatGPT, Bing, Google, DeepL, YoudaoDict, StarDict, LibreTranslate and more.
  • Rich rendering components, such as rendering to Buffer, Posframe, Overlay, Kill Ring, and others. With stream output support.
  • Flexible retrieval of content and language for translation, with the help of the built-in Taker component.
  • Support for word and sentence translation, as well as translation of multiple paragraphs. It can use multiple engines concurrently to translate multiple paragraphs into multiple languages.
  • Support for different HTTP backends (url.el, curl) with asynchronous and non-blocking requests, providing a smooth user experience. Support independent proxy configs.
  • Implemented based on eieio (CLOS), allowing users to flexibly configure and extend the various components.

It’s more than just a translation framework.It’s flexible, and can easily be extended to various Text-to-Text conversion scenarios:

  • For example, the built-in Text-Utility component integrates text encryption/decryption, hashing, QR code generation, etc.
  • For example, it can be extended as a client for ChatGPT (TODO)

Basic Usage

First, you need to download and load this package via MELPA or other ways.

For the most basic use, add the following code to the configuration file:

(setq gt-langs '(en fr))
(setq gt-default-translator (gt-translator :engines (gt-google-engine)))

;; This configuration means:
;; Initialize the default translator, let it translate between en and fr via Google Translate,
;; and the result will be displayed in the Echo Area.

Then select a certain text, and start translation with command gt-do-translate.

That’s it.

Of course, it is possible to specify more options for the translator, such as:

(setq gt-default-translator
      (gt-translator
       :taker   (gt-taker :text 'buffer :pick 'paragraph)  ; config the Taker
       :engines (list (gt-bing-engine) (gt-google-engine)) ; specify the Engines
       :render  (gt-buffer-render)))                       ; config the Render

;; This configuration means:
;; Initialize the default translator, let it send all paragraphs in the buffer to Bing and Google,
;; and output the results with a new Buffer.

Except config default translator with gt-default-translator, you can define several preset translators with gt-preset-translators.

The first translator in gt-preset-translators will be used as the default one if gt-default-translator is nil.

The preset translators are defined like this:

(setq gt-preset-translators
      `((ts-1 . ,(gt-translator
                  :taker (gt-taker :langs '(en fr) :text 'word)
                  :engines (gt-bing-engine)
                  :render (gt-overlay-render)))
        (ts-2 . ,(gt-translator
                  :taker (gt-taker :langs '(en fr ru) :text 'sentence)
                  :engines (gt-google-engine)
                  :render (gt-insert-render)))
        (ts-3 . ,(gt-translator
                  :taker (gt-taker :langs '(en fr) :text 'buffer
                                   :pick 'word :pick-pred (lambda (w) (length> w 6)))
                  :engines (gt-google-engine)
                  :render (gt-overlay-render :type 'help-echo)))))

This configuration presets three translators:

  • ts-1: translate word or selected region near the cursor between en and fr via Bing, display the translated result with Overlay
  • ts-2: translate sentence or selected region near the cursor between en, fr and ru via Google, insert the translated result into current buffer
  • ts-3: translate all words with length more than 6 in buffer between en and fr via Google, display the translated result with help echo

Then, translate with command gt-do-translate and switch between preset translators with command gt-do-setup.

highly recommended:

Install the curl program and the plz.el package. The request will then be sent through curl, which is much better than the built-in url.el!

See more configuration options via M-x customize-group go-translate, and read the following chapters for more configuration details.

More configurations

The core component of the translation framework is gt-translator, which contains the following components:

  • gt-taker: used to capture user input, including text and languages to be translated
  • gt-engine: used to translate the content captured by the taker into the corresponding target text
  • gt-render: used to aggregate results from engines and output them to the user

The flow of translation is [Input] -> [Translate/Transform] -> [Output], corresponding to the components [Taker] -> [Engine] -> [Render] above. Executing the method gt-start on the translator will complete a full translation flow.

Therefore, the essence of configuration is to create a translator instance and specify different components according to needs:

;; specify components with ':taker' ':engines' and ':render'; start translation with 'gt-start'
(gt-start (gt-translator :taker ... :engines ... :render ...))

;; command 'gt-do-translate' use the translator defined in 'gt-default-translator' to do its job
(setq gt-default-translator (gt-translator :taker ... :engines ... :render ..))
(call-interactively #'gt-do-translate)

Therefore, one needs to understand these components first for better configuration.

component gt-taker for capturing

slotdescvalue
textInitial textString or a function that returns a string, it can also be symbol like ‘buffer ‘word ‘paragraph ‘sentence etc
langsTranslate languagesList as ‘(en fr), ‘(en ru it fr), if empty, use the value of gt-langs instead
promptInteractive ConfirmIf t, confirm by minibuffer. If ‘buffer, confirm by opening a new buffer
pickPick paragraphs, sentences or words from initial textFunction or a symbol like ‘word ‘paragraph ‘sentence etc
pick-predUsed to filter the text pickedPass in a string and output a Boolean type
thenThe logic to be executed after take. HookA function that takes the current translator as argument. The final modification can be made to the content captured by Taker
ifValidateFunction or literal symbol, used to determine whether taker is available for current translation task

Currently there is only one built-in Taker implementation, which can be used in most scenarios:

Determine the initial text with 'text',
determine the translation languages with 'langs',
confirm with 'prompt', 
and extract certain paragraphs, sentences, or words with 'pick'.

If no Taker is specified or if Taker is specified but lacks options, the values ​​of the following variables will be used as default:

(setq gt-langs '(en fr))        ; Default translation languages, at least two ​​must be specified
(setq gt-taker-text 'word)      ; By default, the initial text is the word under the cursor. If there is active region, the selected text will be used first
(setq gt-taker-pick 'paragraph) ; By default, the initial text will be split by paragraphs. If you don't want to use multi-parts translation, set it to nil
(setq gt-taker-prompt nil)      ; By default, there is no confirm step. Set it to t or 'buffer if needed

It’s better to use :taker to explicitly specify a Taker for the translator:

(gt-translator :taker (gt-taker))
(gt-translator :taker (gt-taker :langs '(en fr) :text 'word :pick 'paragraph :prompt nil))
(gt-translator :taker (lambda () (gt-taker))) ; a function
(gt-translator :taker (list ; a list, use the first available one
                         (gt-taker :prompt t :if 'selection)
                         (gt-taker :text 'paragraph :if 'read-only)
                         (gt-taker :text 'line)))

Taker will use text to determine the initial text. If there is active region, the selected text is taken. Otherwise use the following rules:

;; It can be a symbol, then use logic like 'thing-at-thing' to take the text
(gt-translator :taker (gt-taker :text 'word))      ; current word (default)
(gt-translator :taker (gt-taker :text 'buffer))    ; current buffer
(gt-translator :taker (gt-taker :text 'paragraph)) ; current paragraph
(gt-translator :taker (gt-taker :text t))          ; interactively choose a symbol, then take by the symbol

;; If it's a string or a function that returns a string, use it as the initial text
(gt-translator :taker (gt-taker :text "hello world"))                        ; just the string
(gt-translator :taker (gt-taker :text (lambda () (buffer-substring 10 15)))) ; the returned string
(gt-translator :taker (gt-taker :text (lambda () '((10 . 15)))))             ; the returned bounds 

Taker determine the languages to translate from langs in the help of gt-lang-rules:

(gt-translator :taker (gt-taker :langs '(en fr)))    ; between English and French
(gt-translator :taker (gt-taker :langs '(en fr ru))) ; between English, French and Russian
(setq gt-polyglot-p t) ; If this is t, then multilingual translation will be performed, i.e., translated into multiple languages ​​at once and the output aggregated

By setting prompt to allow the user to modify and confirm the initial text and languages interactively:

;; Confirm by minibuffer
(gt-translator :taker (gt-taker :prompt t))

;; Confirm by new buffer
(gt-translator :taker (gt-taker :prompt 'buffer))

Finally, the initial text is cut and filtered based on pick and pick-pred. The content it returns is what will ultimately be translated:

;; It can be a symbol like those used by text slot
(gt-translator :taker (gt-taker ; translate all paragraphs in the buffer
                       :text 'buffer
                       :pick 'paragraph))
(gt-translator :taker (gt-taker ; translate all words longer than 6 in the paragraph
                       :text 'paragraph
                       :pick 'word :pick-pred (lambda (w) (length> w 6))))

;; It can be a function. The following example is also translating words longer than 6 in current paragraph.
;; More complex and intelligent pick logic can be implemented
(defun my-get-words-length>-6 (text)
  (cl-remove-if-not (lambda (bd) (> (- (cdr bd) (car bd)) 6))
                    (gt-pick-items-by-thing text 'word)))
(gt-translator :taker (gt-taker :text 'paragraph :pick #'my-get-words-length>-6))

;; Use ':pick 'fresh-word' to pick unknown word only for translation
;; With commands 'gt-record-words-as-known/unknown' to add word to known/unknown list
(gt-translator :taker (gt-taker :text 'paragraph :pick 'fresh-word))

component gt-engine for translating/transforming

slotdescvalue
parseSpecify parserA parser or a function
cacheConfigure cacheIf set to nil, cache is disabled for the current engine. You can also specify different cachers or cache strategies for different engines
streamWhether turn on stream queryBoolean. Works only when engines support stream, for example ChatGPT engine.
delimiterDelimiterIf not empty, the translation strategy of “join-translate-split” will be adopted
thenThe logic to be executed after the engine is completed. HookA function that takes current task as argument. Can be used to make final modifications to the translate result before rendering
ifFilterFunction or literal symbol, used to determine whether the current engine should work for current translation task

The built-in Engine implementations are:

  • gt-deepl-engine, DeepL Translate
  • gt-bing-engine, Bing Translate
  • gt-google-engine/gt-google-rpc-engine, Google Translate
  • gt-chatgpt-engine, translate with ChatGPT
  • gt-youdao-dict-engine/gt-youdao-suggest-engine, 有道翻译,有道近义词
  • gt-stardict-engine, StarDict,for offline translate
  • gt-libre-engine, LibreTranslate, support both online and offline translate

Specify engines for translator via :engines. A translator can have one or more engines, or you can specify a function that returns the engines:

(gt-translator :engines (gt-google-engine))
(gt-translator :engines (list (gt-google-engine) (gt-deepl-engine) (gt-chatgpt-engine)))
(gt-translator :engines (lambda () (gt-google-engine)))

If a engine has multiple parsers, you can specify one through parse to achieve specific parsing, such as:

(gt-translator :engines
               (list (gt-google-engine :parse (gt-google-parser))           ; detail results
                     (gt-google-engine :parse (gt-google-summary-parser)))) ; brief results

You can use if to filter the engines for current translation task. For example:

(gt-translator :engines
               (list (gt-google-engine :if 'word)                      ; Enabled only when translating a word
                     (gt-bing-engine :if '(and not-word parts))        ; Enabled only when translating single part sentence
                     (gt-deepl-engine :if 'not-word :cache nil)        ; Enabled only when translating sentence; disable cache
                     (gt-youdao-dict-engine :if '(or src:fr tgt:fr)))) ; Enabled only when translating French

You can specify different caching policies for different engines with cache:

(gt-translator :engines
               (list (gt-youdao-dict-engine)       ; use default cacher
                     (gt-google-engine :cache nil) ; disable cache
                     (gt-bing-engine :cache 'word) ; cache for word only
                     (gt-deepl-engine :cache (gt-xxx-cacher)))) ; use specify cacher

Notice:

If translate multiple parts text, the default strategy is:

  1. join the parts into a single string,
  2. translate the whole string through the engine,
  3. then split the result into parts.

The text passed to the Engine for translation should be a single string.

If delimiter is set to nil, then a list of strings will be passed to the engine, and the engine should have the ability to process the string list.

component gt-render for rendering

slotdescvalue
prefixCustomize the PrefixOverride the default Prefix format. Set to nil to disable prefix output
thenLogic to be executed after rendering is complete. Hookfunction or another Render. The rendering task can be passed to the next Render to achieve the effect of multi-renders output
ifValidateFunction or literal symbol, used to determine whether render is available for current translation task

The built-in Render implementations:

  • gt-render, the default implementation, will output the results to Echo Area
  • gt-buffer-render, open a new Buffer to render the results (recommended)
  • gt-posframe-pop-render, open a childframe at the current position to render the results
  • gt-posframe-pin-render, use a childframe window with fixed position on the screen to render the results
  • gt-insert-render, insert the results into current buffer
  • gt-overlay-render, displays the results through Overlay
  • gt-kill-ring-render, save the results to Kill Ring
  • gt-alert-render, display results as system notification with the help of alert package

Configure render for translator via :render. Multiple renders can be chained together with :then:

(gt-translator :render (gt-alert-render))
(gt-translator :render (gt-alert-render :then (gt-kill-ring-render))) ; display as system notification then save in kill ring
(gt-translator :render (lambda () (if buffer-read-only (gt-buffer-render) (gt-insert-render)))) ; a function return render

The first available render in the list (validate conjunction with :if) can be used as the final render. For example:

(gt-translator
 :render (list (gt-posframe-pop-render :if 'word) ; if current translation text is word, render with posframe
               (gt-alert-render :if '(and read-only not-word)) ; if text is not word and buffer is readonly, render with alert
               (gt-buffer-render)))               ; default, render with new buffer

Components (Supplementary Notes)

gt-memory-cacher (gt-default-cacher)

Component gt-memory-cacher is the built-in cache implementation. Just set gt-cache-p to t to use it.

You can configure the cacher or switch to another cacher by setting gt-default-cacher:

(setq gt-default-cacher (gt-memory-cacher :if 'word)) ; just cache for word
(setq gt-default-cacher (gt-memory-cacher :if '(or word src:en))) ; just cache for word or english
(setq gt-default-cacher (gt-xxxxxx-cacher)) ; use other cacher

Set gt-cache-p to nil to turn off all caches. Or turn off the cache for engine individually like this:

(gt-translator :engines (gt-google-engine :cache nil))

Translation results can be cached in files, SQLite or Redis through extensions. But maybe it’s unnecessary.

gt-url-http-client/gt-plz-http-client (gt-default-http-client)

Some engines need to fetch translation results over the network, which requires network processing with the help of the gt-http-client component.

By default, gt-url-http-client is used as the http client, which is inefficient.

The component gt-plz-http-client uses curl to send the request, which is much better.

Config gt-default-http-client to switch http client. Or just make sure curl and plz is exists in your system, then gt-plz-http-client will be used as the default http client without any other configs.

To send request with proxy, config like this:

;; for gt-url-http-client
(setq gt-default-http-client
      (gt-url-http-client :proxies '(("http" . "host:9999") ("https" . "host:9999"))))

;; for gt-plz-http-client
(setq gt-default-http-client
      (gt-plz-http-client :args '("--proxy" "socks5://127.0.0.1:9999")))

;; dynamic by host of request url
(setq gt-default-http-client
      (lambda (host)
        (if (string-match-p "deepl" host)
            (gt-plz-http-client :args '("--proxy" "socks5://127.0.0.1:9999"))
          (gt-plz-http-client))))

gt-taker

If prompt via minibuffer, the following keys exist in minibuffer:

  • C-n and C-p switch languages
  • C-l clear input
  • C-g abort translate

If prompt via buffer, the following keys exist in the taking buffer:

  • C-c C-c submit translate
  • C-c C-k abort translate
  • Other keys like switch languages and components please refer to tips on buffer mode line

gt-stardict-engine

This is an offline translation engine that supports plug-in dictionaries.

First, make sure sdcv has been installed on your system:

sudo pacman -S sdcv

In addition, download the dictionary files and put them to the correct location.

After that, configure and use the engine:

;; Basic configuration
(setq gt-default-translator
      (gt-translator :engines (gt-stardict-engine)
                     :render (gt-buffer-render)))

;; More options can be specified
(setq gt-default-translator
      (gt-translator :engines (gt-stardict-engine
                               :dir "~/.stardict/dic" ; specify data file location
                               :dict "dict-name"      ; specify a dict name
                               :exact t)              ; exact, do not fuzzy-search
                     :render (gt-buffer-render)))

NOTE: If rendering via Buffer-Render etc, you can switch between dictionaries by click dictionary name or error message (or press C-c C-c on it).

gt-deepl-engine

DeepL requires auth-key to work, please obtained it through the official website.

The auth-key can then be set in the following ways:

  1. Specify directly in the engine definition:
    (gt-translator :engines (gt-deepl-engine :key "***"))
        
  2. Save it in .authinfo file of OS:
    machine api.deepl.com login auth-key password ***
        

gt-chatgpt-engine

Please obtained the apikey through the official website first.

;; Provide apikey with one of following ways:
(setq gt-chatgpt-key "YOUR-KEY")
(gt-chatgpt-engine :key "YOUR_KEY")
(find-file "~/.authinfo") ; machine api.openai.com login apikey password [YOUR_KEY]

;; Others
(setq gt-chatgpt-model "gpt-3.5-turbo")
(setq gt-chatgpt-temperature 0.7)

Custom the translation prompt as you wish:

(setq gt-chatgpt-user-prompt-template
      (lambda (text lang)
        (format "Translate text to %s and return the first word. Text is: \n%s"
                (alist-get lang gt-lang-codes) text)))

Even can custom the prompt for other tasks. For example, for polish sentence:

(defun my-command-polish-using-ChatGPT ()
  (interactive)
  (let ((gt-chatgpt-system-prompt "You are a writer")
        (gt-chatgpt-user-prompt-template (lambda (text _)
                                           (read-string
                                            "Prompt: "
                                            (format "Polish the sentence below: %s" text)))))
    (gt-start (gt-translator
               :engines (gt-chatgpt-engine :cache nil)
               :render (gt-insert-render)))))

It support streaming output with some renders. Examples:

;; Three engines, one with streaming query, two for normal
;; The streaming result can be output with buffer render, posframe render and insert render
(setq gt-default-translator
      (gt-translator :taker (gt-taker :pick nil)
                     :engines (list (gt-chatgpt-engine :stream t)
                                    (gt-chatgpt-engine :stream nil)
                                    (gt-google-engine))
                     :render (gt-buffer-render)))

;; Translate and insert the streaming results to buffer
(setq gt-default-translator
      (gt-translator :taker (gt-taker :pick nil :prompt t)
                     :engines (gt-chatgpt-engine :stream t)
                     :render (gt-insert-render)))

After all, try text to speech with command gt-do-speak.

gt-buffer-render

Display the translation results with a new buffer. This is a very general way of displaying results.

In the result buffer, there are many shortcut keys (overview through ?), such as:

  • Switch languages via t
  • Switch multi-language mode via T
  • Clear caches with C
  • Refresh via g
  • Quit via q

Alternatively, play speech via y (command gt-do-speak). If the active region exists, then only speak current selection content. TTS requires that the engine have implemented gt-speak method. Command gt-do-speak can use anywhere else, then it will try to speak text via TTS service of system.

You can set the buffer window through buffer-name/window-config/split-threshold:

(gt-translator :render (gt-buffer-render
                        :buffer-name "abc"
                        :window-config '((display-buffer-at-bottom))
                        :then (lambda (_) (pop-to-buffer "abc"))))

Here are some usage examples:

;; Capture content under cursor, use Google to translate word, use DeepL to translate sentence, use Buffer to display the results
;; This is a very practical configuration
(setq gt-default-translator
      (gt-translator
       :taker (gt-taker :langs '(en fr) :text 'word)
       :engines (list (gt-google-engine :if 'word) (gt-deepl-engine :if 'not-word))
       :render (gt-buffer-render)))

;; A command for translating multiple paragraphs in the Buffer into multiple languages ​​and rendering into new Buffer
;; This shows the use of translation of multi-engines with multi-paragraphs and with multi-languages
(defun demo-translate-multiple-langs-and-multiple-parts ()
  (interactive)
  (let ((gt-polyglot-p t)
        (translator (gt-translator
                     :taker (gt-taker :langs '(en fr ru) :text 'buffer :pick 'paragraph)
                     :engines (list (gt-google-engine) (gt-deepl-engine))
                     :render (gt-buffer-render))))
    (gt-start translator)))

gt-posframe-pop-render/gt-posframe-pin-render

You need to install posframe before you use these renders.

The effect of these two Renders is similar to gt-buffer-render, except that the window is floating. The shortcut keys are similar too, such as q to quit.

You can pass any params to posframe-show with :frame-params:

(gt-posframe-pin-render :frame-params (list :border-width 20 :border-color "red"))

gt-insert-render

Insert the translation results into current buffer.

The following types can be specified (type):

  • after, the default type, insert the results after the cursor
  • replace, replace the translated source text with the results

If not satisfied with the default output format and style, adjust it with the following options:

  • sface, propertize the source text with this face after the translation is complete
  • rfmt, the output format of the translation result
  • rface, specify a specific face for the translation results

The option rfmt is a function or a string containing the control character %s:

;; %s is a placeholder for translation result
(gt-insert-render :rfmt " [%s]")
;; One argument, that is the translation result
(gt-insert-render :rfmt (lambda (res) (concat " [" res "]")))
;; Two arguments, the first one is the source text
(gt-insert-render :rfmt (lambda (stext res)
                          (if (length< stext 3)
                              (concat "\n" res)
                            (propertize res 'face 'font-lock-warning-face)))
                  :rface 'font-lock-doc-face)

Here are some usage examples:

;; Translate by paragraph and insert each result at the end of source paragraph
;; This configuration is suitable for translation work. That is: Translate -> Modify -> Save
(setq gt-default-translator
      (gt-translator
       :taker (gt-taker :text 'buffer :pick 'paragraph)
       :engines (gt-google-engine)
       :render (gt-insert-render :type 'after)))

;; Translate the current paragraph and replace it with the translation result
;; This configuration is suitable for scenes such as live chat. Type some text, translate it, and send it
(setq gt-default-translator
      (gt-translator
       :taker (gt-taker :text 'paragraph :pick nil)
       :engines (gt-google-engine)
       :render (gt-insert-render :type 'replace)))

;; Translate specific words in current paragraph and insert the result after each word
;; This configuration can help in reading articles with some words you don't know
(setq gt-default-translator
      (gt-translator
       :taker (gt-taker :text 'paragraph
                        :pick 'word
                        :pick-pred (lambda (w) (length> w 6)))
       :engines (gt-google-engine)
       :render (gt-insert-render :type 'after
                                 :rfmt " (%s)"
                                 :rface '(:foreground "grey"))))

gt-overlay-render

Use Overlays to display translation results.

Set the display mode through type:

  • after, the default type, displays the translation results after the source text
  • before, displays the translation results before the source text
  • replace, overlays the translation results on top of the source text
  • help-echo, display result only when the mouse is hovered over the source text

It is similar to gt-insert-render in many ways, including options:

  • sface, propertize the source text with this face after the translation is complete
  • rfmt, the output format of the translation result
  • rface/rdisp, specify face or display for the translation results
  • pface/pdisp, specify face or display for the translation prefix (language and engine prompts)

Here are some usage examples:

;; Translate all paragraphs in buffer and display the results after the original paragraphs in the specified format
;; This is a configuration suitable for reading read-only content such as Info, News, etc.
(setq gt-default-translator
      (gt-translator
       :taker (gt-taker :text 'buffer :pick 'paragraph)
       :engines (gt-google-engine)
       :render (gt-overlay-render :type 'after
                                  :sface nil
                                  :rface 'font-lock-doc-face)))

;; Mark all qualified words in the Buffer and display the translation results when hover over them
;; This is a practical configuration, suitable for reading articles that contains unfamiliar words
(setq gt-default-translator
      (gt-translator
       :taker (gt-taker :text 'buffer :pick 'word :pick-pred (lambda (w) (length> w 5)))
       :engines (gt-google-engine)
       :render (gt-overlay-render :type 'help-echo)))

;; Use overlays to overlay the translated results directly on top of the original text
;; Use this configuration for an article to get its general idea quickly
(setq gt-default-translator
      (gt-translator
       :taker (gt-taker :text 'buffer)
       :engines (gt-google-engine)
       :render (gt-overlay-render :type 'replace)))

It is flexible, even something like real-time translation can be implement with the help of hook or timer.

gt-text-utility

Derived from gt-translator, integrates a lot of text conversion and processing features.

This demonstrates the extensibility of the framework, shows that it can be used not only for translation.

To generate QR code for text, need to install the qrencode program or qrencode package first:

pacman -S qrencode
brew install qrencode

# or in Emacs
M-x package-install qrencode

In addition, other functionalities can be integrated by extending the generic method gt-text-util.

Here are some usage examples:

;; By default, interactivelly choose what to do with the text
;; Notice: you should not specify any engine for it
(setq gt-default-translator
      (gt-text-utility :render (gt-buffer-render)))

;; Generate QR Code for current text (specify the `utility' explicitly with :langs)
;; Very practical configuration for sharing text to Mobile phone
(setq gt-default-translator
      (gt-text-utility
       :taker (gt-taker :langs '(qrcode) :pick nil)
       :render (gt-buffer-render)))

;; Output text to speech label and MD5 sum
(setq gt-default-translator
      (gt-text-utility
       :taker (gt-taker :langs '(speak md5) :text 'buffer :pick 'paragraph)
       :render (gt-posframe-pin-render)))

gt-validator (:if)

Component gt-taker, gt-engine and gt-render are inherited from gt-validator, which provides a way to determine component availability through :if slot. This greatly simplifies the configuration of translator for different scenarios.

The value of the slot :if can be a function, a symbol or a list of forms linked by and/or. Symbol can be prefixed with not- or no- to indicate a reverse determination.

Some symbols built-in:

  • word translated text is word
  • src:en source language is English
  • tgt:en target language is English
  • parts multiple parts text to be translated
  • read-only current buffer is read only
  • selection current use region is active
  • emacs-lisp-mode suffix with -mode, that match with current mode
  • not-word or no-word reverse determination, translated text is not word

One simple config example:

;; for text selected, not pick, render with posframe
;; for buffer Info, translate current paragraph, render with overlay
;; for buffer readonly, translate all fresh word in buffer, render with overlay
;; for Magit commit buffer, insert the translated result into current position
;; for word, translate with google engine; for non-word, use deepl
(setq gt-default-translator
      (gt-translator
       :taker   (list (gt-taker :pick nil :if 'selection)
                      (gt-taker :text 'paragraph :if '(Info-mode help-mode))
                      (gt-taker :text 'buffer :pick 'fresh-word :if 'read-only)
                      (gt-taker :text 'word))
       :engines (list (gt-google-engine :if 'word)
                      (gt-deepl-engine :if 'no-word))
       :render  (list (gt-posframe-pop-render :if 'selection)
                      (gt-overlay-render :if 'read-only)
                      (gt-insert-render :if (lambda (translator) (member (buffer-name) '("COMMIT_EDITMSG"))))
                      (gt-alert-render :if '(and xxx-mode (or not-selection (and read-only parts))))
                      (gt-buffer-render))))

Customization and Extension

The code is based on eieio (CLOS), so almost every component can be extended or replaced.

For example, implement an engine that outputs the captured text in reverse order. It’s easy:

;; First, define the class, inherit from gt-engine
(defclass my-reverse-engine (gt-engine)
  ((delimiter :initform nil)))

;; Then, implement the method gt-translate
(cl-defmethod gt-translate ((_ my-reverse-engine) task next)
  (with-slots (text res) task
    (setf res (cl-loop for c in text collect (reverse c)))
    (funcall next task)))

;; At last, config and have a try
(setq gt-default-translator (gt-translator :engines (my-reverse-engine)))

For example, extend Taker to let it can capture all headlines in org mode:

;; [implement] make text slot of Taker support 'org-headline
(cl-defmethod gt-thing-at-point ((_ (eql 'org-headline)) (_ (eql 'org-mode)))
  (let (bds)
    (org-element-map (org-element-parse-buffer) 'headline
      (lambda (h)
        (save-excursion
          (goto-char (org-element-property :begin h))
          (skip-chars-forward "* ")
          (push (cons (point) (line-end-position)) bds))))))

;; [usage] config Taker with ':text org-headline' and that's it
(setq gt-default-translator (gt-translator
                             :taker (gt-taker :text 'org-headline)
                             :engines (gt-google-engine)
                             :render (gt-overlay-render :rfmt " (%s)" :sface nil)))

In this way, use your imagination, you can do a lot.

Miscellaneous

To enable debug, set gt-debug-p to t, then you will see the logs in buffer *gt-log*.

Welcome your PRs and sugguestions.

About

Translator on Emacs. Supports multiple engines such as Google, Bing, deepL, ChatGPT, StarDict, Youdao and so on.

Topics

Resources

License

Stars

Watchers

Forks