LibreOffice plugin to pipe whole Writer documents through Google Translate, that ought to keep most of the page formatting.

⌈⌋ branch:  PageTranslate


Check-in [ead6fe2706]

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Minor documentation updates, yield errors for DeepL failures.
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk | 1.8
Files: files | file ages | folders
SHA1: ead6fe2706eb683827e8d2ba54a8eb6e772e8222
User & Date: mario 2021-05-15 05:37:16
Context
2021-05-15
05:42
Add PONS Text Translation backend. check-in: 4620700b36 user: mario tags: trunk
05:37
Minor documentation updates, yield errors for DeepL failures. check-in: ead6fe2706 user: mario tags: trunk, 1.8
2021-05-14
03:19
Simplified params["backend"] string instead of individual flags, shorten mapping and parameterization in deep_translator backend, abbreviate D-T and T-P in new config dialog, add DT duplicates, minor manual updates. check-in: c28be6ec87 user: mario tags: trunk
Changes
Hide Diffs Unified Diffs Ignore Whitespace Patch

Changes to Addons.xcu.

726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
                <node oor:name="M5SELECT" oor:op="replace">
                  <prop oor:name="Context" oor:type="xs:string"><value/></prop>
                  <prop oor:name="URL" oor:type="xs:string"><value>service:org.openoffice.comp.pyuno.pagetranslate?trigger&amp;from=select&amp;lang=select</value></prop>
                  <prop oor:name="Title" oor:type="xs:string"><value/><value xml:lang="en-US">From ➜ To 🗺</value></prop>
                  <prop oor:name="Target" oor:type="xs:string"><value>_self</value></prop>
                </node>
                <node oor:name="M5Z-CONFIG" oor:op="replace">
                  <prop oor:name="Context" oor:type="xs:string"><value/></prop>
                  <prop oor:name="URL" oor:type="xs:string"><value>.uno:OptionsTreeDialog?leaf=LanguageSettings?</value></prop>
                  <prop oor:name="Title" oor:type="xs:string"><value/><value xml:lang="en-US">Settings→Language→...</value></prop>
                  <prop oor:name="Target" oor:type="xs:string"><value>_self</value></prop>
                </node>
              </node>
            </node>
          </node>
        </node>







|
|







726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
                <node oor:name="M5SELECT" oor:op="replace">
                  <prop oor:name="Context" oor:type="xs:string"><value/></prop>
                  <prop oor:name="URL" oor:type="xs:string"><value>service:org.openoffice.comp.pyuno.pagetranslate?trigger&amp;from=select&amp;lang=select</value></prop>
                  <prop oor:name="Title" oor:type="xs:string"><value/><value xml:lang="en-US">From ➜ To 🗺</value></prop>
                  <prop oor:name="Target" oor:type="xs:string"><value>_self</value></prop>
                </node>
                <node oor:name="M5Z-CONFIG" oor:op="replace">
                  <prop oor:name="Context" oor:type="xs:string"><value/></prop>                      <!--vnd.sun.star.extension://vnd.include-once.pagetranslate/OptionsDialog.xdl-->
                  <prop oor:name="URL" oor:type="xs:string"><value>.uno:OptionsTreeDialog?OptionsPageURL=%origin%/OptionsDialog.xdl</value></prop>
                  <prop oor:name="Title" oor:type="xs:string"><value/><value xml:lang="en-US">Settings→Language→...</value></prop>
                  <prop oor:name="Target" oor:type="xs:string"><value>_self</value></prop>
                </node>
              </node>
            </node>
          </node>
        </node>

Changes to Makefile.

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
all:	xhp version oxt install

install:
	unopkg remove vnd.include-once.pagetranslate
	unopkg add --log-file log.txt -s pagetranslate.oxt

version: # https://fossil.include-once.org/versionnum/
	which version  &&  version  --read pagetranslate.py  --incr  --write pagetranslate.py  --write description.xml  ||  true

oxt:
	zip pagetranslate.oxt -r META-INF pagetranslate.py *xcu *xcs *xdl *desc* icons pythonpath/ help

aoo:	oxt
	/opt/openoffice4/program/unopkg remove pagetranslate.oxt
	/opt/openoffice4/program/unopkg add pagetranslate.oxt
	# Shift+F1 for UNO names of widgets
	HELP_DEBUG=true /opt/openoffice4/program/soffice -writer
	
lo7:
	/opt/libreoffice7.1/program/unopkg add pagetranslate.oxt
	/opt/libreoffice7.1/program/soffice --writer

xhp:
	make -C help/en/vnd.include-once.pagetranslate/

pip:
	make -C pythonpath












|
|




|








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
all:	xhp version oxt install

install:
	unopkg remove vnd.include-once.pagetranslate
	unopkg add --log-file log.txt -s pagetranslate.oxt

version: # https://fossil.include-once.org/versionnum/
	which version  &&  version  --read pagetranslate.py  --incr  --write pagetranslate.py  --write description.xml  ||  true

oxt:
	zip pagetranslate.oxt -r META-INF pagetranslate.py *xcu *xcs *xdl *desc* icons pythonpath/ help

aoo:	version oxt
	#/opt/openoffice4/program/unopkg remove pagetranslate.oxt
	/opt/openoffice4/program/unopkg add pagetranslate.oxt
	# Shift+F1 for UNO names of widgets
	HELP_DEBUG=true /opt/openoffice4/program/soffice -writer
	
lo7:	version oxt
	/opt/libreoffice7.1/program/unopkg add pagetranslate.oxt
	/opt/libreoffice7.1/program/soffice --writer

xhp:
	make -C help/en/vnd.include-once.pagetranslate/

pip:
	make -C pythonpath

Changes to OptionsDialog.xdl.

17
18
19
20
21
22
23
24
25

26
27
28
29
30
31
32
  <dlg:fixedline dlg:id="FixedLine3" dlg:tab-index="8" dlg:left="15" dlg:top="15" dlg:width="110" dlg:height="8" dlg:value="Service"/>
  <dlg:fixedline dlg:id="FixedLine2" dlg:tab-index="9" dlg:left="137" dlg:top="15" dlg:width="110" dlg:height="8" dlg:value="Parameters"/>
  <dlg:fixedline dlg:id="Label1" dlg:tab-index="10" dlg:left="140" dlg:top="30" dlg:width="23" dlg:height="8" dlg:value="API key "/>
  <dlg:fixedline dlg:id="Label2" dlg:tab-index="11" dlg:left="140" dlg:top="52" dlg:width="28" dlg:height="8" dlg:printable="false" dlg:value="Email adr "/>
  <dlg:fixedline dlg:id="Label3" dlg:tab-index="12" dlg:left="140" dlg:top="74" dlg:width="30" dlg:height="8" dlg:value="Command "/>
  <dlg:menulist dlg:id="backend" dlg:tab-index="13" dlg:left="20" dlg:top="30" dlg:width="105" dlg:height="14" dlg:help-text="Which translation service to use. (Some might require an API key, or email address.)" dlg:spin="true" dlg:linecount="16">
   <dlg:menupopup>
    <dlg:menuitem dlg:value="Google Translate"/>
    <dlg:menuitem dlg:value="MyMemory"/>

    <dlg:menuitem dlg:value="command line tool"/>
    <dlg:menuitem dlg:value="DeepL API"/>
    <dlg:menuitem dlg:value="DeepL Free API"/>
    <dlg:menuitem dlg:value="DeepL web interface"/>
    <dlg:menuitem dlg:value="Microsoft Translator (T-P)"/>
    <dlg:menuitem dlg:value="QCRI Machine Translation (D-T)"/>
    <dlg:menuitem dlg:value="Yandex Translation (D-T)"/>







|

>







17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
  <dlg:fixedline dlg:id="FixedLine3" dlg:tab-index="8" dlg:left="15" dlg:top="15" dlg:width="110" dlg:height="8" dlg:value="Service"/>
  <dlg:fixedline dlg:id="FixedLine2" dlg:tab-index="9" dlg:left="137" dlg:top="15" dlg:width="110" dlg:height="8" dlg:value="Parameters"/>
  <dlg:fixedline dlg:id="Label1" dlg:tab-index="10" dlg:left="140" dlg:top="30" dlg:width="23" dlg:height="8" dlg:value="API key "/>
  <dlg:fixedline dlg:id="Label2" dlg:tab-index="11" dlg:left="140" dlg:top="52" dlg:width="28" dlg:height="8" dlg:printable="false" dlg:value="Email adr "/>
  <dlg:fixedline dlg:id="Label3" dlg:tab-index="12" dlg:left="140" dlg:top="74" dlg:width="30" dlg:height="8" dlg:value="Command "/>
  <dlg:menulist dlg:id="backend" dlg:tab-index="13" dlg:left="20" dlg:top="30" dlg:width="105" dlg:height="14" dlg:help-text="Which translation service to use. (Some might require an API key, or email address.)" dlg:spin="true" dlg:linecount="16">
   <dlg:menupopup>
    <dlg:menuitem dlg:value="Google Translate" dlg:help-text="default"/>
    <dlg:menuitem dlg:value="MyMemory"/>
    <!--dlg:menuitem dlg:value="PONS Text Translation"/-->
    <dlg:menuitem dlg:value="command line tool"/>
    <dlg:menuitem dlg:value="DeepL API"/>
    <dlg:menuitem dlg:value="DeepL Free API"/>
    <dlg:menuitem dlg:value="DeepL web interface"/>
    <dlg:menuitem dlg:value="Microsoft Translator (T-P)"/>
    <dlg:menuitem dlg:value="QCRI Machine Translation (D-T)"/>
    <dlg:menuitem dlg:value="Yandex Translation (D-T)"/>

Changes to description.xml.

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="UTF-8"?>
<description xmlns="http://openoffice.org/extensions/description/2006" xmlns:dep="http://openoffice.org/extensions/description/2006" xmlns:xlink="http://www.w3.org/1999/xlink">
  <identifier value="vnd.include-once.pagetranslate"/>
  <version value="1.7.5"/>
  <display-name>
    <name lang="en">PageTranslate</name>
  </display-name>
  <dependencies>
    <OpenOffice.org-minimal-version value="3.0" dep:name="OpenOffice.org 3.0"/>
  </dependencies>
  <registration>



|







1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="UTF-8"?>
<description xmlns="http://openoffice.org/extensions/description/2006" xmlns:dep="http://openoffice.org/extensions/description/2006" xmlns:xlink="http://www.w3.org/1999/xlink">
  <identifier value="vnd.include-once.pagetranslate"/>
  <version value="1.8"/>
  <display-name>
    <name lang="en">PageTranslate</name>
  </display-name>
  <dependencies>
    <OpenOffice.org-minimal-version value="3.0" dep:name="OpenOffice.org 3.0"/>
  </dependencies>
  <registration>

Changes to help/en/vnd.include-once.pagetranslate/config.page.

46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
    <item>
      <title>❏ Command line tool</title>
      <p>Allows to send each text paragraph to a local application.  To use
      it, set the command in the according input field again.  Placeholders
      are `{lang}` for the target language, and `{text}` for the paragaphs
      or current text section.  (Both get automatically escaped).  For
      <cmd>translate-cli</cmd> you might need the <var>-p</var> provider
      option as well.
      See also the <link href="https://pypi.org/project/translate/">translate-python
      documentation</link> on how to prepare a separate
      <file>~/.python-translate.cfg</file>.  Or use <link
      href="https://github.com/nidhaloff/deep-translator">deep-translator
      cli</link> with for example <cmd>deep_translator -trans "google" -src
      "auto" -tg {lang} -txt {text}</cmd>.  </p>
    </item>
    <item>
      <title>❏ DeepL API</title>
      <p>Utilizes the speedier <link href="https://www.deepl.com/pro">DeepL
      Pro API</link> to translate documents.  As of yet untested.  Requires
      an API key and paid subscription.  No XML mode (to retain full inline
      formatting) yet, still translates each text segment/paragraph/sentence







<
<
<
|
<
<
<







46
47
48
49
50
51
52



53



54
55
56
57
58
59
60
    <item>
      <title>❏ Command line tool</title>
      <p>Allows to send each text paragraph to a local application.  To use
      it, set the command in the according input field again.  Placeholders
      are `{lang}` for the target language, and `{text}` for the paragaphs
      or current text section.  (Both get automatically escaped).  For
      <cmd>translate-cli</cmd> you might need the <var>-p</var> provider



      option or a prepared <file>~/.python-translate.cfg</file> for API keys.



    </item>
    <item>
      <title>❏ DeepL API</title>
      <p>Utilizes the speedier <link href="https://www.deepl.com/pro">DeepL
      Pro API</link> to translate documents.  As of yet untested.  Requires
      an API key and paid subscription.  No XML mode (to retain full inline
      formatting) yet, still translates each text segment/paragraph/sentence
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
      href="https://www.deepl.com/translator/">DeepL online
      translator</link>.  Only suitable for testing and translating single
      paragraphs or text selection, because it quickly blocks with "error
      429 - too many requests".  It's also kinda redundant now that there's
      a Free API option.</p>
    </item>
  </terms>
  <p>Some provided via <cmd>pip install <link href="https://pypi.org/project/translate/">translate-python</link></cmd>:</p>
  <terms>
    <item>
      <title>❏ Microsoft Translator</title>
      <p>Requires an authorization key. There's also a free/test <link
      href="https://azure.microsoft.com/en-us/pricing/details/cognitive-services/translator/">subscription
      for an API key</link>.  Not tested within PageTranslate yet.</p>
    </item>
  </terms>
  <p>And more via <cmd>pip install <link href="https://pypi.org/project/deep-translator/">deep-translator</link></cmd>:</p>
  <terms>
    <item>
      <title>❏ QCRI Machine Translation</title>
      <p>Requires a <link href="https://mt.qcri.org/api/">free API
      key</link>, but is suitable for whole-document translations.  </p>
    </item>
    <item>







|








|







74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
      href="https://www.deepl.com/translator/">DeepL online
      translator</link>.  Only suitable for testing and translating single
      paragraphs or text selection, because it quickly blocks with "error
      429 - too many requests".  It's also kinda redundant now that there's
      a Free API option.</p>
    </item>
  </terms>
  <p>Some provided via <cmd>pip install <link href="https://pypi.org/project/translate/">translate-python</link></cmd> (TP):</p>
  <terms>
    <item>
      <title>❏ Microsoft Translator</title>
      <p>Requires an authorization key. There's also a free/test <link
      href="https://azure.microsoft.com/en-us/pricing/details/cognitive-services/translator/">subscription
      for an API key</link>.  Not tested within PageTranslate yet.</p>
    </item>
  </terms>
  <p>And more via <cmd>pip install <link href="https://pypi.org/project/deep-translator/">deep-translator</link></cmd> (DT). These won't work in OpenOffice 4.x due to its Python 2.7 runtime:</p>
  <terms>
    <item>
      <title>❏ QCRI Machine Translation</title>
      <p>Requires a <link href="https://mt.qcri.org/api/">free API
      key</link>, but is suitable for whole-document translations.  </p>
    </item>
    <item>
138
139
140
141
142
143
144
145



146


147


148
149
150
151
152
153
154
      <title>Email adr</title>
      <p>An email address is only required by MyMemory.  And strictly
      speaking it's not even required; it just allows for more
      translations.</p>
    </item>
    <item>
      <title>Command</title>
      <p>This field defines the CLI tool to use for translation.  You can



      use something other than `translate-cli` or `deep-translator` of


      course.  Placeholders like {lang} and {text} can be used here.</p>


    </item>
  </terms>
</section>

<section id="flags">
  <title>Options / Flags</title>
  <terms>







|
>
>
>
|
>
>
|
>
>







132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
      <title>Email adr</title>
      <p>An email address is only required by MyMemory.  And strictly
      speaking it's not even required; it just allows for more
      translations.</p>
    </item>
    <item>
      <title>Command</title>
      <p>This field defines the CLI tool to use for translating. Placeholders
      can be noted with {text} curly braces, or shell $lang and %from% percent
      syntax. The Python
      <link href="https://pypi.org/project/translate/">translate</link> or
      <link href="https://pypi.org/project/deep-translator/">deep-translator</link>
      packages provide following CLI wrappers:</p>
      <items>
        <item><p><cmd>translate-cli -o -f auto -t {lang} {text}</cmd></p></item>
        <item><p><cmd>deep_translator -trans "google" -src "auto" -tg {lang} -txt {text}</cmd></p></item>
      </items>
    </item>
  </terms>
</section>

<section id="flags">
  <title>Options / Flags</title>
  <terms>

Changes to pagetranslate.py.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/python
# encoding: utf-8
# api: uno
# type: callback
# category: language
# title: PageTranslate
# description: Action button to get whole Writer document translated
# version: 1.7.5
# state: beta
# author: mario
# url: https://fossil.include-once.org/pagetranslate/
# depends: python:requests (>= 2.5), python:uno
# pack: *.py, pythonpath/*.py, META-INF/*, pkg-desc, *.x*, icons/*
# license: GNU LGPL 2.1
# forked-from: TradutorLibreText (Claudemir de Almeida Rosa)







|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/python
# encoding: utf-8
# api: uno
# type: callback
# category: language
# title: PageTranslate
# description: Action button to get whole Writer document translated
# version: 1.8
# state: beta
# author: mario
# url: https://fossil.include-once.org/pagetranslate/
# depends: python:requests (>= 2.5), python:uno
# pack: *.py, pythonpath/*.py, META-INF/*, pkg-desc, *.x*, icons/*
# license: GNU LGPL 2.1
# forked-from: TradutorLibreText (Claudemir de Almeida Rosa)

Changes to pythonpath/httprequests.py.

58
59
60
61
62
63
64
65
66
67
68
69
            return self
    
    http = fake_requests()
    log.info("using fake_requests() for now")

# headers
http.headers.update({
    "User-Agent": "Mozilla/5.0 (X11; Linux; LibreOffice/6.3), TradutorLibreText/1.3+PageTranslate/1.6",
    "Accept-Language": "*; q=1.0",
    "Accept-Encoding": "utf-8"
})








|




58
59
60
61
62
63
64
65
66
67
68
69
            return self
    
    http = fake_requests()
    log.info("using fake_requests() for now")

# headers
http.headers.update({
    "User-Agent": "Mozilla/5.0 (X11; Linux; LibreOffice/7.0), TradutorLibreText/1.3+PageTranslate/1.8",
    "Accept-Language": "*; q=1.0",
    "Accept-Encoding": "utf-8"
})

Changes to pythonpath/translationbackends.py.

1
2
3
4
5
6
7
8
9
10
11
12
13
14







15
16
17
18
19
20
21
# encoding: utf-8
# api: pagetranslate
# type: classes
# category: language
# title: via_* translation backends
# description: hooks up the translation services (google, mymemory, deepl, ...)
# version: 1.8
# state: beta
# depends: python:requests (>= 2.5), python:langdetect, python:translate
# config: -
#
# Different online service backends and http interfaces are now coalesced here.
# Each class handles sentence/blockwise transfer to one of the online services,
# to get text snippets transformed.







#


# modules
import re, json, time, uuid, html
import os, subprocess, shlex
from random import randrange as rand








|



|
|
>
>
>
>
>
>
>







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
# encoding: utf-8
# api: pagetranslate
# type: classes
# category: language
# title: via_* translation backends
# description: hooks up the translation services (google, mymemory, deepl, ...)
# version: 1.8
# state: beta
# depends: python:requests (>= 2.5), python:langdetect, python:translate, python:deep-translator
# config: -
#
# Different online service backends and http interfaces are now coalesced here.
# Each class handles sentence/blockwise transfer to one of the online machine
# translators to get text snippets transformed.
#
# The primary function is .translate(), with .linebreakwise() being used for
# table-cell snippets. Language from/to are passed through .__init__(params).
#
# translate-python or deep-translator are loaded on demand, as to not impose
# a dependency unless the according backends are actually used. (Configuration
# now uses params["backend"] with some fuzzy title mapping in assign_service().)
#


# modules
import re, json, time, uuid, html
import os, subprocess, shlex
from random import randrange as rand
260
261
262
263
264
265
266



267
268
269
270
271
272
273
        )
        if r.status_code == 200:
            r = r.json().get("translations")
            if r:
                return r[0]["text"]
        else:
            log.error(repr(r))



        return text
    
    def linebreakwise(self, text):
        return self.translate(text, preserve=1)

# DeepL free API
#







>
>
>







267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
        )
        if r.status_code == 200:
            r = r.json().get("translations")
            if r:
                return r[0]["text"]
        else:
            log.error(repr(r))
            if r.status_code == 403:
                r.status = "Authorization/API key invalid"
            raise ConnectionRefusedError(r.status_code, r.status, r.headers)
        return text
    
    def linebreakwise(self, text):
        return self.translate(text, preserve=1)

# DeepL free API
#
478
479
480
481
482
483
484


485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
            "lang|target|to": params["lang"],
            "from|source": params["from"]
        }
        for k,v in repl.items():
            if re.match("""^["']?[\{%$]" + k + "[\}%$]?["']?$""", arg):
                return v
        return arg



# maps a pagetranslate.t.* object (in main module),
# according to configured backend (now a string)
def assign_service(params):
    w = params.get("backend", "Google")
    map = {
        "^google": google,
        "^deepl [\s_] web": deepl_web,
        "^deepl [\s_] (api|pro)": deepl_api,
        "^deepl \s free": deepl_free_api,
        "^mymemory | translated\.net": mymemory,
        "^command | ^CLI | tool | program": cli,
        "^microsoft | translate[_-]py | ^T-?P: | \(T-?P\)": translate_python,
        "^linguee | ^pons | QCRI | yandex | ^D-?T: | \(D-?T\)": deep_translator,
    }
    for rx, cls in map.items():
        if re.search(rx, w, re.I|re.X):
            break
    else:
        cls = google
    return cls(params)








>
>













|








488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
            "lang|target|to": params["lang"],
            "from|source": params["from"]
        }
        for k,v in repl.items():
            if re.match("""^["']?[\{%$]" + k + "[\}%$]?["']?$""", arg):
                return v
        return arg



# maps a pagetranslate.t.* object (in main module),
# according to configured backend (now a string)
def assign_service(params):
    w = params.get("backend", "Google")
    map = {
        "^google": google,
        "^deepl [\s_] web": deepl_web,
        "^deepl [\s_] (api|pro)": deepl_api,
        "^deepl \s free": deepl_free_api,
        "^mymemory | translated\.net": mymemory,
        "^command | ^CLI | tool | program": cli,
        "^microsoft | translate[_-]py | ^T-?P: | \(T-?P\)": translate_python,
        "linguee | pons | QCRI | yandex | ^D-?T: | \(D-?T\)": deep_translator,
    }
    for rx, cls in map.items():
        if re.search(rx, w, re.I|re.X):
            break
    else:
        cls = google
    return cls(params)