summaryrefslogtreecommitdiff
path: root/src/ui/translation.c
diff options
context:
space:
mode:
authorJaakko Keränen <jaakko.keranen@iki.fi>2021-03-15 12:47:20 +0200
committerJaakko Keränen <jaakko.keranen@iki.fi>2021-03-15 19:03:41 +0200
commiteeb9f8b6306524782e7dcc8ef90797dd5bdae1bf (patch)
treeacfde64b0304d563769f73a7288cacb0568bb179 /src/ui/translation.c
parenteb8da869cf87692a5cbb38803644643cd2e192f6 (diff)
Added a page translation service
This is quite experimental. The page contents are sent to an instance of LibreTranslate (powered by Argos Translate), which may or may not successfully translate the contents without mangling the gemtext markup.
Diffstat (limited to 'src/ui/translation.c')
-rw-r--r--src/ui/translation.c360
1 files changed, 360 insertions, 0 deletions
diff --git a/src/ui/translation.c b/src/ui/translation.c
new file mode 100644
index 00000000..cfb6eb79
--- /dev/null
+++ b/src/ui/translation.c
@@ -0,0 +1,360 @@
1/* Copyright 2021 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
23#include "translation.h"
24
25#include "app.h"
26#include "gmdocument.h"
27#include "ui/command.h"
28#include "ui/documentwidget.h"
29#include "ui/labelwidget.h"
30#include "ui/paint.h"
31#include "ui/util.h"
32
33#include <the_Foundation/regexp.h>
34#include <SDL_timer.h>
35#include <math.h>
36
37/*----------------------------------------------------------------------------------------------*/
38
39iDeclareWidgetClass(TranslationProgressWidget)
40iDeclareObjectConstruction(TranslationProgressWidget)
41
42iDeclareType(Sprite)
43
44struct Impl_Sprite {
45 iInt2 pos;
46 iInt2 size;
47 int color;
48 int xoff;
49 iString text;
50};
51
52struct Impl_TranslationProgressWidget {
53 iWidget widget;
54 uint32_t startTime;
55 int font;
56 iArray sprites;
57};
58
59void init_TranslationProgressWidget(iTranslationProgressWidget *d) {
60 iWidget *w = &d->widget;
61 init_Widget(w);
62 setId_Widget(w, "xlt.progress");
63 init_Array(&d->sprites, sizeof(iSprite));
64 d->startTime = SDL_GetTicks();
65 /* Set up some letters to animate. */
66 const char *chars = "ARGOS";
67 const size_t n = strlen(chars);
68 resize_Array(&d->sprites, n);
69 d->font = uiContentBold_FontId;
70 const int width = lineHeight_Text(d->font);
71 const int gap = gap_Text / 2;
72 int x = (int) (n * width + (n - 1) * gap) / -2;
73 const int y = -lineHeight_Text(d->font) / 2;
74 for (size_t i = 0; i < n; i++) {
75 iSprite *spr = at_Array(&d->sprites, i);
76 spr->pos = init_I2(x, y);
77 spr->color = 0;
78 init_String(&spr->text);
79 appendChar_String(&spr->text, chars[i]);
80 spr->xoff = (width - advanceRange_Text(d->font, range_String(&spr->text)).x) / 2;
81 spr->size = init_I2(width, lineHeight_Text(d->font));
82 x += width + gap;
83 }
84}
85
86void deinit_TranslationProgressWidget(iTranslationProgressWidget *d) {
87 iForEach(Array, i, &d->sprites) {
88 iSprite *spr = i.value;
89 deinit_String(&spr->text);
90 }
91 deinit_Array(&d->sprites);
92}
93
94iDefineObjectConstruction(TranslationProgressWidget)
95
96static void draw_TranslationProgressWidget_(const iTranslationProgressWidget *d) {
97 const iWidget *w = &d->widget;
98 const float t = (float) (SDL_GetTicks() - d->startTime) / 1000.0f;
99 const iRect bounds = bounds_Widget(w);
100 iPaint p;
101 init_Paint(&p);
102 const iInt2 mid = mid_Rect(bounds);
103 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_BLEND);
104 iConstForEach(Array, i, &d->sprites) {
105 const int index = index_ArrayConstIterator(&i);
106 const float angle = (float) index;
107 const iSprite *spr = i.value;
108 const float opacity = iClamp(t - index * 0.5f, 0.0, 1.0f);
109 int bg = uiBackgroundSelected_ColorId;
110 int fg = uiTextSelected_ColorId;
111 iInt2 pos = add_I2(mid, spr->pos);
112 pos.y += sin(angle + t) * spr->size.y * iClamp(t * 0.25f - 0.3f, 0.0f, 1.0f);
113 if (bg >= 0) {
114 p.alpha = opacity * 255;
115 fillRect_Paint(&p, (iRect){ pos, spr->size }, bg);
116 }
117 if (fg >= 0) {
118 setOpacity_Text(opacity * 2);
119 drawRange_Text(d->font, addX_I2(pos, spr->xoff), fg, range_String(&spr->text));
120 }
121 }
122 setOpacity_Text(1.0f);
123 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE);
124}
125
126static iBool processEvent_TranslationProgressWidget_(iTranslationProgressWidget *d,
127 const SDL_Event *ev) {
128 iUnused(d, ev);
129 return iFalse;
130}
131
132iBeginDefineSubclass(TranslationProgressWidget, Widget)
133 .draw = (iAny *) draw_TranslationProgressWidget_,
134 .processEvent = (iAny *) processEvent_TranslationProgressWidget_,
135 iEndDefineSubclass(TranslationProgressWidget)
136
137/*----------------------------------------------------------------------------------------------*/
138
139iDefineTypeConstructionArgs(Translation, (iDocumentWidget *doc), doc)
140
141static const char * translationServiceHost = "xlt.skyjake.fi";
142static const uint16_t translationServicePort = 443;
143
144static const char *doubleArrowSymbol = "\u20e2"; /* prevent getting mangled */
145static const char *tripleBacktickSymbol = "\u20e3";
146static const char *h1Symbol = "\u20e4";
147static const char *h2Symbol = "\u20e5";
148static const char *h3Symbol = "\u20e6";
149
150static iString *quote_String_(const iString *d) {
151 iString *quot = new_String();
152 iConstForEach(String, i, d) {
153 const iChar ch = i.value;
154 if (ch == '"') {
155 appendCStr_String(quot, "\\\"");
156 }
157 else if (ch == '\\') {
158 appendCStr_String(quot, "\\\\");
159 }
160 else if (ch == '\n') {
161 appendCStr_String(quot, "\\n");
162 }
163 else if (ch == '\r') {
164 appendCStr_String(quot, "\\r");
165 }
166 else if (ch == '\t') {
167 appendCStr_String(quot, "\\t");
168 }
169 else if (ch >= 0x100) {
170 appendFormat_String(quot, "\\u%04x", ch);
171 }
172 else {
173 appendChar_String(quot, ch);
174 }
175 }
176 return quot;
177}
178
179static iString *unquote_String_(const iString *d) {
180 iString *unquot = new_String();
181 iConstForEach(String, i, d) {
182 const iChar ch = i.value;
183 if (ch == '\\') {
184 next_StringConstIterator(&i);
185 const iChar esc = i.value;
186 if (esc == '\\') {
187 appendChar_String(unquot, esc);
188 }
189 else if (esc == 'n') {
190 appendChar_String(unquot, '\n');
191 }
192 else if (esc == 'r') {
193 appendChar_String(unquot, '\r');
194 }
195 else if (esc == 't') {
196 appendChar_String(unquot, '\t');
197 }
198 else if (esc == '"') {
199 appendChar_String(unquot, '"');
200 }
201 else if (esc == 'u') {
202 char digits[5];
203 iZap(digits);
204 iForIndices(j, digits) {
205 next_StringConstIterator(&i);
206 digits[j] = *i.pos;
207 }
208 iChar codepoint = strtoul(digits, NULL, 16);
209 if (codepoint) {
210 appendChar_String(unquot, codepoint);
211 }
212 }
213 else {
214 iAssert(0);
215 }
216 }
217 else {
218 appendChar_String(unquot, ch);
219 }
220 }
221 return unquot;
222}
223
224static void finished_Translation_(iTlsRequest *d, iTlsRequest *req) {
225 iUnused(req);
226 postCommandf_App("translation.finished ptr:%p", userData_Object(d));
227}
228
229void init_Translation(iTranslation *d, iDocumentWidget *doc) {
230 d->dlg = makeTranslation_Widget(as_Widget(doc));
231 d->startTime = 0;
232 d->doc = doc; /* owner */
233 d->request = new_TlsRequest();
234 d->timer = 0;
235 setUserData_Object(d->request, d->doc);
236 setHost_TlsRequest(d->request,
237 collectNewCStr_String(translationServiceHost),
238 translationServicePort);
239 iConnect(TlsRequest, d->request, finished, d->request, finished_Translation_);
240}
241
242void deinit_Translation(iTranslation *d) {
243 if (d->timer) {
244 SDL_RemoveTimer(d->timer);
245 }
246 cancel_TlsRequest(d->request);
247 iRelease(d->request);
248 destroy_Widget(d->dlg);
249}
250
251static uint32_t animate_Translation_(uint32_t interval, iAny *ptr) {
252 postCommandf_App("translation.update ptr:%p", ((iTranslation *) ptr)->doc);
253 return interval;
254}
255
256void submit_Translation(iTranslation *d) {
257 /* Check the selected languages from the dialog. */
258 const char *idFrom = languageId_String(text_LabelWidget(findChild_Widget(d->dlg, "xlt.from")));
259 const char *idTo = languageId_String(text_LabelWidget(findChild_Widget(d->dlg, "xlt.to")));
260 iAssert(status_TlsRequest(d->request) != submitted_TlsRequestStatus);
261 iBlock *json = collect_Block(new_Block(0));
262 iString *docSrc = collect_String(copy_String(source_GmDocument(document_DocumentWidget(d->doc))));
263 replace_String(docSrc, "=>", doubleArrowSymbol);
264 replace_String(docSrc, "```", tripleBacktickSymbol);
265 replace_String(docSrc, "###", h3Symbol);
266 replace_String(docSrc, "##", h2Symbol);
267 replace_String(docSrc, "#", h1Symbol);
268 printf_Block(json,
269 "{\"q\":\"%s\",\"source\":\"%s\",\"target\":\"%s\"}",
270 cstrCollect_String(quote_String_(docSrc)),
271 idFrom,
272 idTo);
273 iBlock *msg = collect_Block(new_Block(0));
274 printf_Block(msg, "POST /translate HTTP/1.1\r\n"
275 "Host: xlt.skyjake.fi\r\n"
276 "Connection: close\r\n"
277 "Content-Type: application/json\r\n"
278 "Content-Length: %zu\r\n\r\n", size_Block(json));
279 append_Block(msg, json);
280 setContent_TlsRequest(d->request, msg);
281 submit_TlsRequest(d->request);
282 d->startTime = SDL_GetTicks();
283 d->timer = SDL_AddTimer(1000 / 30, animate_Translation_, d);
284}
285
286static void processResult_Translation_(iTranslation *d) {
287 SDL_RemoveTimer(d->timer);
288 d->timer = 0;
289 if (status_TlsRequest(d->request) == error_TlsRequestStatus) {
290 return;
291 }
292 iBlock *resultData = collect_Block(readAll_TlsRequest(d->request));
293 printf("result(%zu):\n%s\n", size_Block(resultData), cstr_Block(resultData));
294 fflush(stdout);
295 iRegExp *pattern = iClob(new_RegExp(".*translatedText\":\"(.*)\"\\}", caseSensitive_RegExpOption));
296 iRegExpMatch m;
297 init_RegExpMatch(&m);
298 if (matchRange_RegExp(pattern, range_Block(resultData), &m)) {
299 iString *translation = unquote_String_(collect_String(captured_RegExpMatch(&m, 1)));
300 replace_String(translation, tripleBacktickSymbol, "```");
301 replace_String(translation, doubleArrowSymbol, "=>");
302 replace_String(translation, h3Symbol, "###");
303 replace_String(translation, h2Symbol, "##");
304 replace_String(translation, h1Symbol, "#");
305 setSource_DocumentWidget(d->doc, translation);
306 postCommand_App("sidebar.update");
307 delete_String(translation);
308 }
309 else {
310 /* TODO: Report failure! */
311 }
312}
313
314static iLabelWidget *acceptButton_Translation_(const iTranslation *d) {
315 return (iLabelWidget *) lastChild_Widget(findChild_Widget(d->dlg, "dialogbuttons"));
316}
317
318iBool handleCommand_Translation(iTranslation *d, const char *cmd) {
319 iWidget *w = as_Widget(d->doc);
320 if (equalWidget_Command(cmd, w, "translation.submit")) {
321 if (status_TlsRequest(d->request) == initialized_TlsRequestStatus) {
322 iWidget *langs = findChild_Widget(d->dlg, "xlt.langs");
323 setFlags_Widget(langs, hidden_WidgetFlag, iTrue);
324 iLabelWidget *acceptButton = acceptButton_Translation_(d);
325 updateTextCStr_LabelWidget(acceptButton, "00:00");
326 setFlags_Widget(as_Widget(acceptButton), disabled_WidgetFlag, iTrue);
327 iTranslationProgressWidget *prog = new_TranslationProgressWidget();
328 setPos_Widget(as_Widget(prog), langs->rect.pos);
329 setSize_Widget(as_Widget(prog), langs->rect.size);
330 addChild_Widget(d->dlg, iClob(prog));
331 submit_Translation(d);
332 }
333 return iTrue;
334 }
335 if (equalWidget_Command(cmd, w, "translation.update")) {
336 const uint32_t elapsed = SDL_GetTicks() - d->startTime;
337 const unsigned seconds = (elapsed / 1000) % 60;
338 const unsigned minutes = (elapsed / 60000);
339 updateText_LabelWidget(acceptButton_Translation_(d),
340 collectNewFormat_String("%02u:%02u", minutes, seconds));
341 return iTrue;
342 }
343 if (equalWidget_Command(cmd, w, "translation.finished")) {
344 if (!isFinished_Translation(d)) {
345 processResult_Translation_(d);
346 destroy_Widget(d->dlg);
347 d->dlg = NULL;
348 }
349 return iTrue;
350 }
351 if (equalWidget_Command(cmd, d->dlg, "translation.cancel")) {
352 cancel_TlsRequest(d->request);
353 return iTrue;
354 }
355 return iFalse;
356}
357
358iBool isFinished_Translation(const iTranslation *d) {
359 return d->dlg == NULL;
360}