-
Notifications
You must be signed in to change notification settings - Fork 38
/
go-translate.el
347 lines (298 loc) · 14.9 KB
/
go-translate.el
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
;;; go-translate.el --- Translation framework, configurable and scalable -*- lexical-binding: t -*-
;; Copyright (C) 2024 lorniu <[email protected]>
;; Author: lorniu <[email protected]>
;; URL: https://github.com/lorniu/go-translate
;; Package-Requires: ((emacs "28.1"))
;; Keywords: convenience
;; Version: 3.0.8
;; SPDX-License-Identifier: GPL-3.0-or-later
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;; This file is NOT part of GNU Emacs.
;;; Commentary:
;; Translation framework on Emacs, with high configurability and extensibility.
;;
;; - Support multiple translation engines, such as Google, Bing, DeepL, ChatGPT...
;; - With variety of output styles, such as Buffer, Overlay, Childframe and so on.
;; - With a flexible taker for easy retrieval of translated content and targets.
;; - Support multiple paragraphs/parts and multi-language translation.
;; - Support different http backends, such as url.el, curl. Async and non-blocking.
;; - Support caches, proxy and more.
;;
;; Notice, it is not limited to just being a translation framework. It can fulfill
;; any text transformation tasks, such as ChatGPT and more.
;;
;; Custom it as you need, extend it using your creativity.
;; You can install it via MELPA or from github. Make sure it is on your `load-path'.
;;
;; For the most basic use, add the following configuration:
;;
;; (require 'go-translate)
;;
;; (setq gt-default-translator
;; (gt-translator
;; :taker (gt-taker :langs '(en zh))
;; :engines (list (gt-google-engine) (gt-bing-engine))
;; :render (gt-buffer-render)))
;;
;; Then start your translate with command `gt-do-translate'.
;;
;; See README.org for details.
;;; Code:
(require 'transient)
(require 'gt-core)
(require 'gt-extension)
(require 'gt-engine-bing)
(require 'gt-engine-google)
(require 'gt-engine-google-rpc)
(require 'gt-engine-deepl)
(require 'gt-engine-stardict)
(require 'gt-engine-youdao)
(require 'gt-engine-chatgpt)
(require 'gt-engine-libre)
(require 'gt-engine-echo)
(require 'gt-text-utility)
;;; Mask these commands in M-x
(dolist (cmd '(gt-prompt-next-target
gt-buffer-render--cycle-next
gt-buffer-render--refresh
gt-buffer-render--browser
gt-buffer-render--keyboard-quit
gt-buffer-render--toggle-readonly
gt-buffer-render--toggle-polyglot
gt-buffer-render--delete-cache
gt-buffer-render--show-tips
gt-buffer-render--unfold-source-text
gt-posframe-render-auto-close-handler
gt-stardict-switch-dict
gt-overlay-render-save-to-kill-ring))
(put cmd 'completion-predicate #'ignore))
;;; Presets
(defun gt-define-custom-taker ()
(let ((text (completing-read "Initial text: "
(gt-make-completion-table `(,@gt-taker-text-things nil))))
(pick (completing-read "Pick style: "
(gt-make-completion-table `(nil ,@gt-taker-pick-things))))
(prompt (completing-read "Prompt style: "
(gt-make-completion-table (list 'disable 'buffer 'minibuffer)) nil t)))
(gt-taker :text (intern text) :pick (intern pick)
:prompt (pcase prompt ("buffer" 'buffer) ("minibuffer" t) (_ nil)))))
(defcustom gt-preset-takers
(lambda ()
`((,(gt-face-lazy "new..." 'bold) . ,#'gt-define-custom-taker)
(default . ,(gt-taker))
(interactively . ,(gt-taker :text t :pick t :prompt t))
(paragraph-at-point . ,(gt-taker :text 'paragraph :pick nil))
(whole-buffer . ,(gt-taker :text 'buffer :pick 'nil))
(whole-buffer-by-paragraph . ,(gt-taker :text 'buffer :pick 'paragraph))))
"Preset takers.
It is an alist or a function return the alist, which the value is a valid
instance of `gt-taker' and the key is a string or symbol, representing the
display label of the taker.
Custom your own takers and put them into this list, then change the taker
of `gt-default-translator' at any time in `gt-do-setup'."
:type '(choice function
(alist :key-type (choice string symbol)
:value-type (sexp :tag "Instance of gt-taker")))
:group 'gt-do-translate)
(defcustom gt-preset-engines
(lambda ()
`((Bing . ,(gt-bing-engine))
(DeepL . ,(gt-deepl-engine))
(Google . ,(gt-google-engine))
(ChatGPT . ,(gt-chatgpt-engine))
(ChatGPT-Stream . ,(gt-chatgpt-engine :stream t))
(Youdao-Dict . ,(gt-youdao-dict-engine))
(Youdao-Suggest . ,(gt-youdao-suggest-engine))
(StarDict . ,(gt-stardict-engine))
(LibreTranslate . ,(gt-libre-engine))
(GoogleRPC . ,(gt-google-rpc-engine))
(Google-Summary . ,(gt-google-engine :parse (gt-google-summary-parser)))
(Bionic_Reading . ,(gt-echo-engine :do '(clean br) :tag "Bionic Reading"))))
"Preset engines.
It is an alist or a function return the alist, which the value is a valid
instance of `gt-engine' and the key is a string or symbol, representing the
display label of the engine.
Custom your own engines and put them into this list, then change the engines
of `gt-default-translator' at any time in `gt-do-setup'."
:type '(choice function
(alist :key-type (choice string symbol)
:value-type (sexp :tag "Instance of gt-engine")))
:group 'gt-do-translate)
(defcustom gt-preset-renders
(lambda ()
`((,gt-buffer-render-buffer-name . ,(gt-buffer-render))
(insert/after . ,(gt-insert-render :type 'after))
(insert/replace . ,(gt-insert-render :type 'replace))
(overlay/after . ,(gt-overlay-render :type 'after))
(overlay/help-echo . ,(gt-overlay-render :type 'help-echo))
(message->echo-area . ,(gt-render))
(save->kill-ring . ,(gt-kill-ring-render))
(Pop-Posframe . ,(gt-posframe-pop-render))
(Pin-Posframe . ,(gt-posframe-pin-render))
(overlay-or-insert . ,(lambda ()
(if buffer-read-only
(gt-overlay-render :type 'after :then (gt-kill-ring-render))
(gt-insert-render :type 'after))))
(system-notification . ,(gt-alert-render))))
"Preset renders.
It is an alist or a function return the alist, which the value is a valid
instance of `gt-render' and the key is a string or symbol, representing the
display label of the render.
Custom your own render and put them into this list, then change the render
of `gt-default-translator' at any time in `gt-do-setup'."
:type '(choice function
(alist :key-type (choice string symbol)
:value-type (sexp :tag "Instance of gt-render")))
:group 'gt-do-translate)
(defcustom gt-preset-translators
(lambda ()
`((default . ,(gt-translator :taker (cdar (gt-ensure-plain gt-preset-takers))
:engines (cdar (gt-ensure-plain gt-preset-engines))
:render (cdar (gt-ensure-plain gt-preset-renders))))
(Text-Utility . ,(gt-text-utility
:taker (gt-taker :pick nil)
:render (gt-buffer-render)))))
"Preset translators.
It is an alist or a function return the alist, which the value is a valid
instance of `gt-translator' and the key is a string or symbol, representing the
display label of the translator.
Custom your own translator and put them into this list, then change the the
default translator to one of them at any time in `gt-do-setup'."
:type '(choice function
(alist :key-type (choice string symbol)
:value-type (sexp :tag "Instance of gt-translator")))
:group 'gt-do-translate)
(defcustom gt-default-translator nil
"The translator used by `gt-do-translate'.
If you leave this nil, then the first translator in `gt-preset-translators'
will be used as the default translator."
:type '(restricted-sexp :match-alternatives (gt-translator-p 'nil))
:group 'gt-do-translate)
(defun gt-ensure-default-translator ()
"Initial `gt-default-translator' if possible and make sure it's valid."
(unless gt-default-translator
(setq gt-default-translator (cdar (gt-ensure-plain gt-preset-translators))))
(if (cl-typep gt-default-translator 'gt-translator)
(let (gt-debug-p) (gt-reset gt-default-translator))
(user-error "The `gt-default-translator' is unavailable"))
gt-default-translator)
(defun gt-translator-info (translator)
"Return TRANSLATOR's basic info for displaying."
(with-slots (taker engines render _taker _engines _render) translator
(cl-macrolet ((desc1 (name &rest body)
`(if (not (slot-boundp translator ',(intern (format "_%s" name)))) "unbound"
(when-let (,name (or ,name ,(intern (format "_%s" name))))
(if (gt-functionp ,name)
(replace-regexp-in-string "[ \n\t]+" " " (format "%s" ,name))
,@body)))))
(list (desc1 taker (if (consp taker) (format "%s" taker)
(cl-flet ((desc2 (slot) (when (slot-boundp taker slot)
(format "%s: %s" slot (slot-value taker slot)))))
(format "<%s> %s" (gt-desc taker)
(string-join (remove nil (mapcar #'desc2 '(langs text pick prompt))) ", ")))))
(desc1 engines (mapconcat (lambda (en) (concat (format "%s" (oref en tag)) (if (gt-stream-p en) " (stream)")))
(ensure-list (gt-ensure-plain engines)) ", "))
(desc1 render (if (consp render) (format "%s" render)
(gt-desc (gt-ensure-plain render))))))))
(defun gt-set-taker (&optional translator taker)
"Set TRANSLATOR's TAKER to one from `gt-preset-takers'."
(interactive)
(unless translator (setq translator gt-default-translator))
(unless taker
(let ((cands (gt-ensure-plain gt-preset-takers)))
(setq taker (gt-ensure-plain
(alist-get
(completing-read "Taker to use: " (gt-make-completion-table cands) nil t)
cands nil nil #'string-equal)))))
(oset translator taker nil)
(oset translator _taker taker)
(message "Changed taker done."))
(defun gt-set-engines (&optional translator engines)
"Set TRANSLATOR's ENGINES to ones from `gt-preset-engines'."
(interactive)
(unless translator (setq translator gt-default-translator))
(unless engines
(let ((cands (gt-ensure-plain gt-preset-engines)))
(setq engines
(mapcar (lambda (item)
(let ((engine (gt-ensure-plain (alist-get item cands nil nil #'string-equal))))
(if (cl-typep engine 'gt-engine) engine
(user-error "Invalid engine detected. Abort"))))
(completing-read-multiple "Engines to use (can choose multiple): "
(gt-make-completion-table cands))))))
(oset translator engines nil)
(oset translator _engines engines)
(message "Changed engines done."))
(defun gt-set-render (&optional translator render)
"Set TRANSLATOR's RENDER to one from `gt-preset-renders'."
(interactive)
(unless translator (setq translator gt-default-translator))
(unless render
(let ((cands (gt-ensure-plain gt-preset-renders)))
(setq render (gt-ensure-plain
(alist-get
(completing-read "Render to use: " (gt-make-completion-table cands) nil t)
cands nil nil #'string-equal)))))
(oset translator render nil)
(oset translator _render render)
(message "Changed render done."))
(defun gt-translator-copy-of-presets ()
(let* ((tss (gt-ensure-plain gt-preset-translators))
(tsn (completing-read "Preset translator: " tss nil t))
(translator (alist-get tsn tss nil nil #'string-equal)))
(list (clone translator) tsn)))
(defun gt-switch-translator ()
"Switch `gt-default-translator' to another defined in `gt-preset-translators'."
(interactive)
(cl-destructuring-bind (translator name)
(gt-translator-copy-of-presets)
(setq gt-default-translator translator)
(gt-ensure-default-translator)
(message "Switch default translator to: %s" name)))
(transient-define-prefix gt-do-setup ()
"Setup `gt-default-translator' in user interface provided by transient."
:transient-non-suffix #'transient--do-exit
[:description
(lambda ()
(format "Current Default Translator:\n\n %s\n"
(condition-case err
(apply #'format "Taker: %s\n Engines: %s\n Render: %s" (gt-translator-info (gt-ensure-default-translator)))
(error (format "%s" err)))))
[("t" "Set taker..." gt-set-taker :transient t)]
[("e" "Set engines..." gt-set-engines :transient t)]
[("r" "Set render..." gt-set-render :transient t)]
[("c" "Switch preset translator..." gt-switch-translator)]]
(interactive)
(gt-ensure-default-translator)
(transient-setup 'gt-do-setup))
;;; Entrance
;;;###autoload
(defun gt-do-translate (&optional arg)
"Translate using `gt-default-translator'.
Define your default translator like this:
(setq gt-default-translator
(gt-translator :engines (gt-bing-engine)))
(setq gt-default-translator
(gt-translator :taker (gt-taker :langs '(en fr) :text 'sentence :prompt t)
:engines (list (gt-google-engine) (gt-deepl-engine))
:render (gt-buffer-render)))
Or define several different translators and put them in `gt-preset-translators',
and switch with `gt-do-setup' at any time.
This is just a simple wrapper of `gt-start' method. Create other translate
commands in the same way using your creativity.
If ARG is not nil, translate with translator select by `gt-preset-translators'."
(interactive "P")
(let ((gt-default-translator (if arg (car (gt-translator-copy-of-presets)) gt-default-translator)))
(gt-ensure-default-translator)
(gt-start gt-default-translator)))
(provide 'go-translate)
;;; go-translate.el ends here