Compare commits

...

379 commits
v1.2.4 ... main

Author SHA1 Message Date
Piero Toffanin 753a33e7c5 install_models.py note 2022-12-20 12:39:36 -05:00
Piero Toffanin e559f899b8 Fix headings, move section 2022-12-20 12:37:59 -05:00
Piero Toffanin 01b892b96a Update notes 2022-12-20 12:36:12 -05:00
Piero Toffanin 282737519c Fix suggestions, move all sqlite databases to db 2022-12-20 12:25:31 -05:00
Piero Toffanin 5a2e0fe656
Merge pull request #366 from pierotofy/lt
Run helper script, misc improvements
2022-12-20 11:57:46 -05:00
Piero Toffanin 11324ab1f5 Bump version 2022-12-20 11:48:25 -05:00
Piero Toffanin 91ae57ad6c Add run.bat 2022-12-20 11:48:03 -05:00
Piero Toffanin 72aaa41d8f Add run.sh 2022-12-20 11:30:14 -05:00
Piero Toffanin b407ffee3c lt.sh --> run.sh 2022-12-20 11:29:51 -05:00
Piero Toffanin f2d6800f4c Readme tweaks 2022-12-20 11:17:34 -05:00
Piero Toffanin 959f4638cc Named volume for db, --update-models arg 2022-12-20 11:13:56 -05:00
Piero Toffanin 5285697203 Add db 2022-12-20 10:36:31 -05:00
Piero Toffanin 10f82d9a3e lt.sh helper script 2022-12-20 10:33:15 -05:00
Piero Toffanin c4d1b05b27
Merge pull request #364 from suuft/patch-1
add java library
2022-12-18 12:15:45 -05:00
suuft (valeriy) 034407ee15
add java library 2022-12-18 00:38:26 +03:00
Piero Toffanin b55d150f9c
Update link 2022-12-11 03:09:20 -05:00
Piero Toffanin cdfeb6d8fc
Update link 2022-12-11 02:58:41 -05:00
Piero Toffanin 34fceaacc7
Update README.md 2022-12-11 02:33:51 -05:00
Piero Toffanin c9ccbf6a25
Update README.md 2022-12-11 02:32:51 -05:00
Piero Toffanin 9a3b92caf1
Merge pull request #360 from pierotofy/m1
Apple M1 support, ARM docker images
2022-12-11 01:21:19 -05:00
Piero Toffanin 5974c022b6 Simplify mirrors table in readme 2022-12-11 01:09:22 -05:00
Piero Toffanin c21fedc6bb Remove libicu dep 2022-12-11 01:04:12 -05:00
Piero Toffanin 9629cb8888 Enable multi-platform docker images 2022-12-11 00:55:27 -05:00
Piero Toffanin b9293e911a Merge branch 'main' of https://github.com/LibreTranslate/LibreTranslate into m1 2022-12-11 00:55:14 -05:00
Piero Toffanin 4ab7e7d2f5 Fix auto-detect 2022-12-11 00:53:19 -05:00
Piero Toffanin 9deefbbc84 Builds/runs natively on Apple M1 2022-12-11 00:45:14 -05:00
Piero Toffanin 8be739a783
Merge pull request #358 from pierotofy/misc
Miscellaneous Fixes and Improvements
2022-12-10 00:37:41 -05:00
Piero Toffanin 2b0074b94f Only show target languages, check swap, bump version 2022-12-10 00:21:38 -05:00
Piero Toffanin 0875fdc433 Server-side check for target lang 2022-12-10 00:03:21 -05:00
Piero Toffanin 6621c51b52 Fix array 2022-12-09 16:38:58 -05:00
Piero Toffanin e79089b5c1 Extend /languages to include targets 2022-12-09 16:36:12 -05:00
Piero Toffanin 0478d4ee2c Handle empty translation 2022-12-09 14:35:39 -05:00
Piero Toffanin b87b210bfc Fix indent 2022-12-09 14:29:44 -05:00
Piero Toffanin 30b3382af8 Fix CSS indent on firefox 2022-12-09 14:28:14 -05:00
Piero Toffanin 7c5d798fd5
Bump version 2022-11-16 12:29:41 -05:00
P.J. Finlay ee638b407d Remove Polyglot transliteration library
The model server for the Polyglot transliteration library is currently
down and the project looks unmaintained; this currently is breaking
LibreTranslate installs. This commit removes Polyglot for
transliteration but keeps using Polyglot for language detection.

- https://github.com/LibreTranslate/LibreTranslate/issues/344
- https://community.libretranslate.com/t/improving-transliteration-in-libretranslate/400
2022-11-16 08:59:11 -06:00
Piero Toffanin 1c5b006c87
Clarify modified definition 2022-11-14 01:28:51 -05:00
Piero Toffanin 488aa6db97
Merge pull request #343 from LibreTranslate/PJ-Finlay-patch-8
Fix README typo
2022-11-13 17:11:49 -05:00
P.J. Finlay b59e8ffcab
Fix README typo 2022-11-13 15:17:32 -06:00
Piero Toffanin 7c753a33d4
Merge pull request #339 from Dafnik/patch-2
Add NGINX to reverse proxy section
2022-11-11 11:30:40 -05:00
Dafnik f96e77df2b Add NGINX to reverse proxy section
Adds an example nginx config to the README
2022-11-11 15:21:22 +01:00
Piero Toffanin 65e6a7f262
Merge pull request #337 from Kuchenmampfer/main
Added reverse proxy example for Caddy
2022-11-07 13:26:04 -05:00
Kuchenmampfer 968793d36e
Added reverse proxy example for Caddy 2022-11-07 17:53:38 +00:00
Piero Toffanin 8e2c6a9405
Merge pull request #336 from SethFalco/fix/style
style: fix drop down arrow on dark mode
2022-11-04 08:50:19 -04:00
Seth Falco dc7b1367f6
style: fix drop down arrow on dark mode
Signed-off-by: Seth Falco <seth@falco.fun>
2022-11-04 12:10:02 +00:00
Piero Toffanin a6a061d439
Merge pull request #333 from wacumov/patch-1
Add Swift binding to README.md
2022-10-30 12:09:53 -04:00
Mikhail Akopov db71da493b
Add Swift binding to README.md 2022-10-30 17:45:45 +02:00
Piero Toffanin 6cf1a931fa Fix swagger schema id for translate_file 2022-10-18 11:41:36 -04:00
Piero Toffanin aed684b666
Create FUNDING.yml 2022-10-18 11:34:31 -04:00
Piero Toffanin 07fafa2f4b Typo 2022-10-14 13:47:06 -04:00
Piero Toffanin 99672ef528 Merge branch 'redis' 2022-10-14 13:35:34 -04:00
Piero Toffanin 69762ce4f4
Merge pull request #330 from LibreTranslate/redis
Support for persistent request limit storage via redis
2022-10-14 13:35:02 -04:00
Piero Toffanin 85da156c39 Bump version 2022-10-14 13:28:59 -04:00
Piero Toffanin 70c4571067 Add --req-limit-storage redis support 2022-10-14 13:27:34 -04:00
Piero Toffanin bc8b331fe9 Do not decrease flood violations 2022-10-14 12:49:28 -04:00
Piero Toffanin f18dfcd60d
Merge pull request #329 from Hayao0819/main
Add shell binding to README
2022-10-05 18:48:19 +02:00
Yamada Hayao 0b34a4545c
Add shell binding to README 2022-10-05 23:42:13 +09:00
Dingedi ea3979ab0c
Merge pull request #327 from dingedi/main
auto focus in input textarea
2022-10-02 14:52:04 +02:00
dingedi 0e3b9a9ebd
auto focus in input textarea 2022-10-02 14:50:58 +02:00
Dingedi e68ec8411d
Merge pull request #326 from dingedi/feature/swap-langs-fix
fix swap lang
2022-10-02 14:43:11 +02:00
dingedi b833608f5c
fix swap lang 2022-10-02 14:40:25 +02:00
Dingedi eb0df4d0df
Merge pull request #325 from dingedi/feature/auto-translate-url
run translation if text is in url
2022-10-02 14:33:51 +02:00
dingedi 54079b0276
run translation if text is in url 2022-10-02 14:31:57 +02:00
Dingedi 9e8bcf6040
Merge pull request #324 from dingedi/main
better display of detected language
2022-10-02 14:21:37 +02:00
dingedi eee2cf2f33
show iso if language name is not found (prevent error) 2022-10-02 14:20:20 +02:00
dingedi 3072219085
better display of detected language 2022-10-01 17:34:17 +02:00
Dingedi 5d8e513d45
fix 2022-10-01 17:12:05 +02:00
Piero Toffanin 7c37681afc
Merge pull request #323 from AnTheMaker/patch-1
Show detected Language (#314)
2022-10-01 10:40:12 -04:00
An | Anton Röhm 99b6a1a2f2
Show detectedLangText 2022-10-01 14:25:29 +02:00
An | Anton Röhm b90d996965
introduce detectedLangText var to store auto-detected lang 2022-10-01 14:21:50 +02:00
Dingedi 36e05596aa
Merge pull request #320 from kianmeng/fix-typos-in-readme
Fix typo in README.md
2022-09-26 14:37:53 +02:00
Kian-Meng Ang 057e4b01c9 Fix typo in README.md
Found via `codespell -L ot,fo,te,ue,inout,optionel`
2022-09-26 20:14:01 +08:00
Piero Toffanin 052f0ae1cd
Merge pull request #317 from dingedi/main
move improve_translation in language.py and use it in transliteration
2022-09-23 22:06:13 -04:00
Piero Toffanin eb5aec7bdb
Merge pull request #318 from dingedi/upgrade-requirements
upgrade requirements
2022-09-23 22:04:29 -04:00
dingedi bb6536d786
upgrade requirements 2022-09-23 15:35:44 +02:00
dingedi c66a519751
restore formatting only if transliteration dont fail 2022-09-23 14:20:22 +02:00
dingedi 077d6e9efa
move and improve_translation in language.py, use it for transliteration 2022-09-23 13:59:13 +02:00
Dingedi 5aa3f1eee8
Merge pull request #313 from dingedi/main
remove use of deprecated method
2022-09-17 08:53:46 +02:00
dingedi 13d792b6b6
remove use of deprecated method 2022-09-17 08:45:59 +02:00
Piero Toffanin 7b7cd7768b
Merge pull request #312 from eugene-davis/non-root-user
Non-root User for Dockerfile and Multi-Stage Build
2022-09-14 01:45:32 -04:00
Eugene Davis cabb887212
Update base image 2022-09-14 00:35:04 +02:00
Eugene Davis 9d6d06ceeb
Minor fixes 2022-09-13 23:09:20 +02:00
Eugene Davis 81f667b214
Move initialization into final image build 2022-09-13 21:24:18 +02:00
Eugene Davis 2a5e84730a
Execute docker as non-root 2022-09-13 14:21:44 +02:00
Piero Toffanin 299e5c3aa0
Merge pull request #311 from viktorkalyniuk/added-ios-app
Update README.md
2022-09-11 19:44:28 -04:00
Viktor Kalyniuk 8f8473ed4a Added LiTranslate iOS app to the README 2022-09-11 23:56:39 +03:00
Piero Toffanin 6cc4f94da2
Merge pull request #304 from youngtrashbag/patch-1
Make switch-language-btn white in Dark Theme
2022-08-24 12:44:13 +02:00
youngtrashbag 6415ea98f2
fix(css): btn-switch-language white in dark theme 2022-08-24 10:09:49 +02:00
Piero Toffanin d11e2ea165
Update README.md 2022-08-24 09:56:31 +02:00
Piero Toffanin be03358e83
Merge pull request #300 from gi-yt/patch-1
Add ~vern's onion/i2p instances of libretranslate
2022-08-24 09:54:41 +02:00
Arya K d8bf8af169
Short links for i2p along with b32 addr 2022-08-20 19:05:02 +05:30
Arya K d7a536fdd0
Add ~vern's onion/i2p instances of libretranslate 2022-08-20 19:01:25 +05:30
Piero Toffanin 95405726a2
Merge pull request #294 from ecxod/preloading-stuff
Preloading stuff
2022-08-07 00:39:07 -04:00
Christian Eichert cebeca43cb preloading the stuff 2022-08-06 20:16:01 +02:00
Christian Eichert dd4b2cb0b9 Merge branch 'main' of https://github.com/ecxod/LibreTranslate into christian 2022-08-06 19:17:59 +02:00
Christian Eichert e0c4810246 https://github.com/LibreTranslate/LibreTranslate/pull/293 2022-08-06 19:05:39 +02:00
Piero Toffanin 85c569e124
Merge pull request #293 from ecxod/main
brightened the Realm of Darkness
2022-08-06 11:31:54 -04:00
Christian Eichert 982c412169
Merge branch 'LibreTranslate:main' into main 2022-08-06 17:09:39 +02:00
Christian Eichert 54bdc77bdc
brightened the Realm of Darkness
some CSS adjustments to make "Tanslate-Files" look like "Translate-Text
2022-08-06 17:08:11 +02:00
Piero Toffanin b2a4dd0a9f
Merge pull request #291 from ecx0d/main
Update dark-theme.css
2022-08-01 00:09:25 -04:00
Christian Eichert c36c9ade62
Merge pull request #1 from ecx0d/ecx0d-css-patch-1
Update dark-theme.css
2022-07-31 21:01:14 +02:00
Christian Eichert 537430f55d
Update dark-theme.css
just a bit css, making <select> and <option> more readable in dark theme
2022-07-31 20:53:48 +02:00
Piero Toffanin d1d8fbab89
Merge pull request #287 from jonwiggins/main
Fix Security Vulnerabilities
2022-07-26 00:07:36 -04:00
JonWiggins b6219cb605 Upgrade waitress to patch CVE-2022-31015
Remove pip to close IN1-PYTHON-PIP-1278135
apt-get upgrade to close CVE-2022-1664, CVE-2022-1304, and CVE-2022-2068
2022-07-25 16:47:19 -05:00
Piero Toffanin ed764ce81b
Merge pull request #286 from TheFrenchGhosty/patch-1
Remove libretranslate.pussthecat.org
2022-07-24 12:13:05 -04:00
TheFrenchGhosty da1c9af97d
Remove libretranslate.pussthecat.org 2022-07-24 18:10:48 +02:00
Piero Toffanin be88f289c7 More forgiving 2022-07-15 13:03:13 -04:00
Piero Toffanin 1dddcea794 Add --threads 2022-07-15 12:12:20 -04:00
Piero Toffanin e4fe0e8919
Merge pull request #285 from Minosity-VR/main
Add --api-keys-db-path argument
2022-07-15 10:10:30 -04:00
Minosity-VR 752d2aae2c
Add api keys db path as env var (#1)
* Add API Keys DB path to command & env vars

* Create API Keys DB directory if not exists before sqlite connection & Add docker-compose example
2022-07-15 13:22:04 +02:00
Piero Toffanin 393eebe235
Merge pull request #283 from jonwiggins/main
Expand CUDA Support to more GPUs
2022-07-03 12:50:32 -04:00
jonwiggins 482a1c65c6 Add cuda pytorch install
Add info on adding cuda to the container path if needed
2022-07-03 11:35:29 -05:00
Piero Toffanin 727b9192e8
Merge pull request #279 from SethFalco/chores-2
fix: fix race condition in settings and langs
2022-06-22 19:50:22 -04:00
Seth Falco 6e2df73a7f
fix: fix race condition in settings and langs 2022-06-22 23:21:19 +01:00
Piero Toffanin c2b82e9303 Use get_api_key link in link 2022-06-21 17:00:27 -04:00
Piero Toffanin 79b9af9071 Say something on startup 2022-06-21 16:56:48 -04:00
Piero Toffanin 78d2bc7be4 Better error message when API key link is available 2022-06-21 16:39:08 -04:00
Piero Toffanin 47fc85fdec return None on failed remote key lookup 2022-06-21 16:37:06 -04:00
Piero Toffanin ed68f1bcf9 Add --get-api-key-link 2022-06-21 15:17:42 -04:00
Piero Toffanin 8f8087f8ae Add --api-keys-remote 2022-06-21 14:57:32 -04:00
Piero Toffanin 647379aea5 Fix check_and_install_transliteration 2022-06-21 14:22:12 -04:00
Piero Toffanin 3c1be4e731
Merge pull request #275 from jonwiggins/main
Add CUDA docker version
2022-06-20 19:00:46 -04:00
Jon Wiggins 89dde2d468 Add CUDA docker version 2022-06-20 16:23:32 -06:00
Piero Toffanin 8247444fb2
Merge pull request #273 from gi-yt/main
Add lt.vern.cc instance to instance list
2022-06-08 15:30:13 -04:00
Arya K 9670777484
Add lt.vern.cc instance to instance list 2022-06-08 21:51:40 +05:30
Piero Toffanin ef8ccc231c
Merge pull request #268 from dingedi/main
improve translation of punctuation
2022-05-30 12:33:07 -04:00
dingedi 9831ba88a6
improve translation of punctuation 2022-05-30 09:14:45 +02:00
Piero Toffanin d5f6276e7a
Merge pull request #266 from dingedi/main
Upgrade argos-translate-files
2022-05-27 12:58:28 -04:00
Dingedi e81d119603
Upgrade argos-translate-files
Add support for .epub and .html
2022-05-27 10:41:27 +02:00
Piero Toffanin d6ef04ba3e Extract JSONL from suggestions.db script 2022-05-22 16:17:41 -04:00
Piero Toffanin 0136d8808c Bump version (2) 2022-05-19 11:09:20 -04:00
Piero Toffanin 07b42d89d4 Bump version 2022-05-19 10:49:25 -04:00
Piero Toffanin aec1da8951
Update README.md 2022-05-18 10:07:23 -04:00
Dingedi eb9cb07a5d
Merge pull request #263 from dingedi/main
Upgrade argostranslate
2022-05-15 21:18:04 +02:00
Dingedi 3e8e3a07e4
Upgrade argostranslate 2022-05-15 21:10:43 +02:00
Piero Toffanin b0b446b11c
Merge pull request #261 from Anomalion/main
Include `dark-theme.css`
2022-05-10 11:16:49 -04:00
Anomalion 8eece0e984
Include dark-theme.css 2022-05-10 11:57:47 +02:00
Anomalion 6e7df35ae7
Merge pull request #1 from Anomalion/patch-1
Create dark-theme.css
2022-05-09 14:41:17 +02:00
Anomalion d28b684ee8
Create dark-theme.css 2022-05-09 14:15:18 +02:00
Piero Toffanin ee5829721f
Merge pull request #258 from pingufreak/patch-1
Update README.md
2022-05-06 10:01:39 -04:00
pingufreak 2cf9d2a028
Update README.md
File translation via Apache proxy needs "ProxyPreserveHost On", which is "Off" on some distributions (RHEL8 for instance). Else window.location.host will point to the container IP instead of the host.
2022-05-06 09:58:29 +02:00
Piero Toffanin e9bf9099d3
Merge pull request #257 from dingedi/main
improve translation formating
2022-05-03 11:47:44 -04:00
dingedi 3873d51c3c
improve translation formating 2022-05-03 16:48:06 +02:00
Dingedi 3c6fa1b01e
Merge pull request #256 from LibreTranslate/revert-255-main
Revert "improve translation formating"
2022-05-03 16:44:26 +02:00
Dingedi 5773adaabd
Revert "improve translation formating" 2022-05-03 16:43:05 +02:00
Dingedi adebc2a536
Merge pull request #255 from dingedi/main
improve translation formating
2022-05-03 16:42:50 +02:00
dingedi a91303d27e
improve translation formating 2022-05-03 16:30:20 +02:00
Piero Toffanin 8ea0fa7dca
Merge pull request #254 from dingedi/feature/add-python3-10
add python 3.10
2022-05-03 08:46:23 -04:00
Piero Toffanin e86365b44b
Merge pull request #253 from dingedi/main
upgrade requirements
2022-05-03 08:45:58 -04:00
dingedi 8d4ab72e05
upgrade pytest 2022-05-03 11:16:03 +02:00
dingedi ea63203aea
prevent github action error with python 3.10 recognized as python 3.1 2022-05-03 11:14:21 +02:00
dingedi 0c85b1a53c
add python 3.10 2022-05-03 11:09:55 +02:00
dingedi 4ac1ea13df
upgrade requirements 2022-05-03 11:04:21 +02:00
Piero Toffanin 9f73221d75
Merge pull request #252 from LibreTranslate/PJ-Finlay-patch-7
Upgrade deprecated Argos Translate call
2022-04-30 17:55:07 -04:00
P.J. Finlay 278a7057e4
Upgrade deprecated Argos Translate call
- load_installed_languages has been deprecated in favor of get_installed_languages
2022-04-30 06:15:54 -05:00
Piero Toffanin df491b0f91
Merge pull request #250 from vemonet/fix-readme-badges
Fix badges in the readme
2022-04-21 16:25:56 -04:00
Vincent Emonet f77c8bb8f1 Fix badges in the readme 2022-04-21 20:18:25 +02:00
Piero Toffanin aa8a0f0db5
Update TRADEMARK.md 2022-04-20 10:26:56 -04:00
Piero Toffanin f47cffc031
Usage of mark in instances 2022-04-19 18:36:24 -04:00
Piero Toffanin 46b15daec1
Clarify hosting name usage 2022-04-19 18:22:30 -04:00
Piero Toffanin 2fde4f7966
Merge pull request #249 from K-Francis-H/readme-api-examples-mirror-update
Readme api examples, mirror update
2022-04-19 18:19:05 -04:00
Kyle Harrity dc81d6c80a update auto detect example to show "detectedLanguage" object in response 2022-04-18 18:41:52 -04:00
Kyle Harrity 86cb815395 Add mirror for translate.terraprint.co 2022-04-18 18:39:19 -04:00
Piero Toffanin 059404f84a Add trademark policy 2022-04-17 13:32:01 -04:00
Piero Toffanin 42d41493d9
Merge pull request #248 from nuttolum/patch-1
Update README.md
2022-04-17 01:24:19 -04:00
nuttolum b20d06d679
Update README.md
added a link to my guide for running libretranslate on windows natively
2022-04-16 21:44:46 -04:00
Piero Toffanin 988ce9e7f9
Merge pull request #246 from DmitrySandalov/main
Add .gitattributes
2022-04-16 12:25:35 -04:00
Dmitry Sandalov 6396acd2bc
Add .gitattributes 2022-04-16 12:41:55 +03:00
Dingedi f5b5e801b3
Merge pull request #244 from dingedi/main
Remove instances not working
2022-04-08 09:48:51 +02:00
Dingedi aa49535a81
Remove instances not working 2022-04-08 09:47:49 +02:00
P.J. Finlay fc2ad8d637
Add translate.fortytwo-it.com mirror (#243)
https://community.libretranslate.com/t/scaling-libretranslate-distribution/78/16
2022-04-07 19:51:33 -05:00
Dingedi 8c2f3678cc
Merge pull request #242 from dingedi/main
update deprecated parameter name
2022-04-07 16:12:19 +02:00
Sébastien Thuret 51768e2d82
update deprecated parameter name 2022-04-07 15:56:57 +02:00
Dingedi 77ad23f630
Merge pull request #241 from dingedi/main
Upgrade requirements
2022-04-07 15:50:04 +02:00
Sébastien Thuret 0bbcc2ae83
Upgrade requirements 2022-04-07 15:43:13 +02:00
Piero Toffanin 9eb1bff261
Merge pull request #237 from dingedi/main
Run test for pull requests, add some functional tests
2022-04-06 09:41:30 -04:00
Sébastien Thuret 118924f897
fix 2022-04-06 14:27:55 +02:00
Sébastien Thuret ba32595a43
add another docker test 2022-04-06 14:22:15 +02:00
Sébastien Thuret 5c44eaff17
dont need to wait python tests 2022-04-06 14:17:19 +02:00
Sébastien Thuret c9886b736e
fix syntax error 2022-04-06 14:14:59 +02:00
Sébastien Thuret 5ea9f27ea5
add *.py 2022-04-06 14:13:31 +02:00
Sébastien Thuret c42740f6d1
Add test for docker build 2022-04-06 14:11:19 +02:00
Sébastien Thuret 8d602a80c3
add more tests for /translate 2022-04-05 17:12:15 +02:00
Sébastien Thuret 7113a30a0c
load only en & es models (avoid to download all models) 2022-04-05 16:39:31 +02:00
Sébastien Thuret 55b05992b5
fix function name 2022-04-04 08:26:58 +02:00
P.J. Finlay 8495211ac9
Merge pull request #239 from LibreTranslate/PJ-Finlay-patch-5
Add PHP bindings
2022-04-03 17:18:37 -05:00
P.J. Finlay 57707cb2a7
Add PHP bindings
https://community.libretranslate.com/t/php-class-for-libretranslate/195
2022-04-03 17:18:01 -05:00
Sébastien Thuret 97ff47bc2b
add translate test (test if it work & models are downloaded) 2022-04-03 20:41:59 +02:00
Sébastien Thuret 99c6ceba43
upgrade pytest 2022-04-03 19:50:11 +02:00
Sébastien Thuret e3a3db5996
remove python 3.6 2022-04-03 19:49:49 +02:00
Dingedi 7da51e8680
Merge pull request #238 from dingedi/focus-textarea
Focus textarea after delete text
2022-04-03 18:21:51 +02:00
Dingedi 5b45b9a53b
Focus textarea after delete text 2022-04-03 18:08:10 +02:00
Sébastien Thuret d7377d174b
/languages should be only available for GET 2022-04-03 13:17:13 +02:00
Dingedi 2bbf17b835
fix 2022-04-03 12:24:39 +02:00
Dingedi 8c7badff47
run pytest in verbose mode 2022-04-03 11:13:15 +02:00
Sébastien Thuret 0874dc062e
reorganization of tests (1 route per file), add some basic tests 2022-04-03 11:07:37 +02:00
Sébastien Thuret 00a6039289
Refactor & add the first test ! 2022-04-02 22:26:37 +02:00
Dingedi 9638b67f70
Remove python 3.6
Flask-Limiter 2.2 require >= python 3.7
2022-04-02 21:14:27 +02:00
Dingedi 70afa5e5e0
Run test for pull requests 2022-04-02 21:02:17 +02:00
Piero Toffanin a2bea5e300
Fix install_models.py (2) 2022-04-01 10:06:03 -04:00
Piero Toffanin 8cc03572e5
Fix install_models.py 2022-04-01 10:01:15 -04:00
Piero Toffanin 39c6cd6e8a
Fix build 2022-03-31 23:06:25 -04:00
Piero Toffanin 7f5e4db46b
Merge pull request #235 from setokesan/include-models-in-docker
Added models include option in docker build
2022-03-31 10:52:26 -04:00
Piti Cookie e828658332 Added models include option in docker build 2022-03-31 07:21:20 -07:00
Piero Toffanin 3a65bbce52
Merge pull request #234 from feliskio/main
Fix Docker build failing due to username casing
2022-03-30 12:21:23 -04:00
Felix Wotschofsky db34db1688 Fix Docker build failing due to username casing 2022-03-30 15:29:51 +00:00
Piero Toffanin c217c65358
Merge pull request #233 from feliskio/main
Improve container publishing workflow
2022-03-30 09:28:05 -04:00
Felix Wotschofsky aee96f077b Improve container building workflow 2022-03-30 12:29:33 +00:00
Piero Toffanin 5da194905a
Add new language notes 2022-03-26 11:56:50 -04:00
Piero Toffanin dba7fa5285
Removed jinja2 dep 2022-03-26 11:52:40 -04:00
Piero Toffanin b9e96fc30f
Merge pull request #231 from dingedi/dingedi-patch-1
Set jinja2 version
2022-03-26 11:32:53 -04:00
Piero Toffanin fa6d60c1e2
Merge branch 'main' into dingedi-patch-1 2022-03-26 11:28:42 -04:00
Piero Toffanin c19823344c
Merge pull request #232 from dingedi/feature/upgrade-to-flask2
Upgrade to flask 2
2022-03-26 11:27:19 -04:00
Sébastien Thuret a88841c174
upgrade-to-flask2 2022-03-26 09:02:54 +01:00
Dingedi 8b50afdc82
Set jinja2 version 2022-03-26 08:45:41 +01:00
Piero Toffanin 6b2ef99f78
Merge pull request #229 from K-Francis-H/translate-auto-show-langs
include language detected, confidence for /translate calls with auto detect
2022-03-25 10:37:54 -04:00
Kyle Harrity 2499c6c379 include language detected, confidence for /translate calls with auto detect 2022-03-25 02:19:56 -04:00
Piero Toffanin cd45641452
Merge pull request #223 from ewrenge/fix-broken-link
Fix broken link on README
2022-03-09 19:26:26 -05:00
ewreurei 349d5f1e4a
Fix broken link on README 2022-03-10 05:11:33 +09:00
Piero Toffanin bc67c21a41
Merge pull request #222 from LibreTranslate/PJ-Finlay-patch-4
Edit README documentation
2022-03-05 10:49:09 -05:00
P.J. Finlay 1219bbcd30
Edit README documentation
Mostly minor changes in phrasing
2022-03-05 07:54:55 -06:00
P.J. Finlay fae16e7e67
Merge pull request #221 from LibreTranslate/PJ-Finlay-patch-3
Improve links in mobile app documentation
2022-03-05 07:30:50 -06:00
P.J. Finlay f6fd24adcd
Improve links in mobile app documentation 2022-03-05 07:28:07 -06:00
Dingedi bf36738552
Merge pull request #220 from TheFrenchGhosty/patch-1
Add the PussTheCat.org instance
2022-03-05 11:58:50 +01:00
TheFrenchGhosty cdab73500b
Add the PussTheCat.org instance 2022-03-05 03:11:12 +01:00
Piero Toffanin c3548bffe1 Memoize 2022-03-04 10:24:29 -05:00
Piero Toffanin 45649c3340
Merge pull request #219 from ZenulAbidin/detect_fix
Fix language detection error
2022-03-04 10:09:46 -05:00
Ali Sherief e23b96f1da Fix language detection error
The root cause was load_installed_languages() of argostranslate
being called at the top of the file instead of inside a function,
this caused the list of installed languages to incorrectly be
returned as an empty list.
2022-03-04 08:23:11 +00:00
Piero Toffanin bcf051b7ff
Update README.md 2022-02-25 11:43:17 -05:00
Piero Toffanin 3480fbde01
Update README.md 2022-02-25 11:42:21 -05:00
Piero Toffanin ea332b11e6
Merge pull request #216 from fushinari/suggest-errors
suggestions: Abort when missing parameters
2022-02-20 12:26:55 -05:00
Mufeed Ali 9edcbe8bea
suggestions: Abort when missing parameters 2022-02-20 22:52:12 +05:30
Piero Toffanin 124e736685 Downgrade flask 2022-02-20 11:41:33 -05:00
Piero Toffanin 0d33e251c1 Pin itsdangerous 2022-02-20 11:40:34 -05:00
Piero Toffanin b87305b375 Update flask 2022-02-20 11:35:43 -05:00
Piero Toffanin a85b645550
Merge pull request #215 from fushinari/api-key-access
app: Fail when giving invalid API keys
2022-02-20 11:29:40 -05:00
Mufeed Ali 2ddb415bba
cleanup: Whitespaces and imports
- Remove unnecessary import
- Reorder imports
- Fix whitespace issues
2022-02-20 13:39:02 +05:30
Mufeed Ali 933c96914b
app: Fail when giving invalid API keys
When an API key is passed, fail in the case of an invalid API key even
if an API key is not required. This allows the user to know that the API
key is invalid. Otherwise, they work under the assumption that the API
key is correct, even though it is not.
2022-02-20 13:36:29 +05:30
Piero Toffanin 8962de8755
Add auto-detect example 2022-02-10 12:46:02 -05:00
Dingedi a13a714b89
Merge pull request #211 from EsmailELBoBDev2/patch-1
added my instance :)
2022-02-10 16:01:55 +01:00
Esmail EL BoB 6932fed0b6
added my instance :) 2022-02-10 06:25:25 +00:00
Piero Toffanin 1df501db7f
Merge pull request #209 from dingedi/feature/keep-selected-langs-in-url
keep selected languages and text in url
2022-02-07 12:39:02 -05:00
Sébastien Thuret 54f5c5ed94
keep selected languages and text in url 2022-02-07 17:58:30 +01:00
Piero Toffanin 61a148d692
Use len 2022-02-07 11:22:08 -05:00
Piero Toffanin 0d122c5d53
Merge pull request #206 from dingedi/feature/set-default-language
set default language instead of raise error
2022-02-07 11:21:20 -05:00
Piero Toffanin 15dcfa12c6
Merge pull request #207 from dingedi/feature/disable-web-ui
Add --disable-web-ui
2022-02-07 11:18:59 -05:00
Piero Toffanin badff01b34
Merge pull request #208 from dingedi/feature/add-python-3.9
add python 3.9
2022-02-07 11:18:19 -05:00
Sébastien Thuret 8eb3644f02
add python 3.9 2022-02-07 11:15:52 +01:00
Sébastien Thuret 6b18b23b46
Add --disable-web-ui 2022-02-07 11:02:32 +01:00
Sébastien Thuret b8df98fdce
set default language instead of raise error 2022-02-07 10:45:31 +01:00
Piero Toffanin f75a294aa0
Merge pull request #204 from dingedi/unescape-html-entity-codes
unescape html entity codes (ex: &apos)
2022-02-06 12:07:39 -05:00
Piero Toffanin c11c8a499a
Merge pull request #205 from dingedi/upgrade-pyicu
Upgrade to pyicu>=2.8
2022-02-06 12:06:45 -05:00
Dingedi cfabcab016
Upgrade to pyicu>=2.8 2022-02-06 15:04:47 +01:00
Dingedi 79e5b0fb50
unescape html entity codes (ex: &apos) 2022-02-06 14:54:52 +01:00
Piero Toffanin 9e77f82470 Fix extra t 2022-01-31 10:39:55 -05:00
Piero Toffanin 7efe0b0c6f Bump version 2022-01-31 10:21:25 -05:00
Piero Toffanin 74e2823658 Fix frontendTimeout 2022-01-31 10:19:14 -05:00
Piero Toffanin 5eccc20daf
Update README.md 2022-01-26 12:25:51 -05:00
Piero Toffanin a35722ea60
Update README.md 2022-01-26 12:23:54 -05:00
Piero Toffanin 85de14fe95
Reference to training video 2022-01-25 18:32:22 -05:00
Dingedi 252adc25f0
Upgrade translatehtml
To have HTML tags exclusions with attribute translate="no"
2022-01-25 14:22:43 +01:00
Piero Toffanin ae9a12f14c
Merge pull request #198 from idotj/main
Closes #197
2022-01-17 15:20:40 -05:00
idotj 8ecf708bdd
Apply a white background main <body> 2022-01-17 21:19:46 +01:00
idotj 7afce9ae2a Improve color contrast for WCAG standards + add underline to links 2022-01-17 20:09:20 +01:00
Piero Toffanin 27d076a1b4
Update README.md 2022-01-13 20:58:01 -05:00
Dingedi 6dd7d6f336
Merge pull request #196 from fushinari/patch-1
README: remove non-working instances
2022-01-06 17:33:41 +01:00
Mufeed Ali f0f4351e71
README: remove non-working instances
Remove instances that don't work anymore
2022-01-06 21:56:03 +05:30
Piero Toffanin 2c2dd99d28
Merge pull request #195 from SethFalco/download-btn
bug: optimize download btn for mobile
2022-01-04 23:07:13 -05:00
Seth Falco 558f58e4b2
bug: optimize download btn for mobile 2022-01-04 21:13:43 +01:00
Piero Toffanin 5176c18625
Merge pull request #194 from SethFalco/translate-type-buttons
bug: fix translate text/file buttons
2022-01-04 14:36:04 -05:00
Seth Falco da36cd77cd
fix translate text/file buttons 2022-01-04 20:20:02 +01:00
Piero Toffanin 27c4c12c1f
Merge pull request #191 from SethFalco/chore
chore: update url to avoid redirect
2021-12-31 10:57:43 -05:00
Piero Toffanin 3c8ef04049
Merge pull request #192 from SethFalco/editorconfig
chore: add editorconfig
2021-12-31 10:57:04 -05:00
Seth Falco 70c86a6903
chore: add editorconfig 2021-12-31 11:52:48 +01:00
Seth Falco 9734852fb5
chore: update url to avoid redirect 2021-12-31 11:35:03 +01:00
Dingedi f19e52a392
Upgrade to argos-translate-files v1.0.5
Fix security issues in lxml
2021-12-26 10:20:41 +00:00
Piero Toffanin 6bb5aa5a75
Merge pull request #190 from dingedi/main
Upgrade to Argos Translate 1.6.1
2021-12-23 18:26:59 -05:00
Dingedi cdefbc75f2
Upgrade to Argos Translate 1.6.1
Remove PyQt5
2021-12-23 21:57:52 +00:00
P.J. Finlay aa5defca22
Upgrade to Argos Translate 1.5.4 2021-12-16 19:17:07 -06:00
Piero Toffanin 714bc0fe8f
Merge pull request #182 from qgustavor/patch-1
Properly escape data in the request code
2021-12-09 09:37:03 -05:00
Gustavo Rodrigues 64ae20e932
Properly escape data in the request code
It was only escaping the first quote, all other quotes and other characters that require to be escaped (like line breaks) were not being escaped. JSON.stringify is a good function to handle this.
2021-12-09 10:31:48 -03:00
Piero Toffanin 94c27e3645 Merge branch 'main' of https://github.com/uav4geo/LibreTranslate into main 2021-12-06 22:15:35 +01:00
Piero Toffanin 0cb437083e Slower forgiveness 2021-12-06 22:15:16 +01:00
Piero Toffanin fe5fbe9cd1
Update README.md 2021-12-04 00:36:46 -05:00
Piero Toffanin 8076183ae8
Merge pull request #180 from destined2fail1990/main
Update README.md
2021-11-28 00:13:21 -05:00
destined2fail1990 1ab1c13e1e
Update README.md
Added Skitzen.com as a API host
2021-11-27 18:15:22 -06:00
Piero Toffanin 66f62efcbb Tweak ban logic, update config description 2021-11-24 12:49:07 -05:00
Piero Toffanin 3a7527c418 Improve banned forgiveness logic 2021-11-24 12:41:12 -05:00
P.J. Finlay cda2d3ed96
Merge pull request #177 from ChillerDragon/pr_zillyhuhn_instance
Add trans.zillyhuhn.com
2021-11-17 18:23:20 -06:00
Chiller Dragon df806e2c5b
Add trans.zillyhuhn.com 2021-11-18 00:37:09 +01:00
Piero Toffanin 6163443d1b
Keep license 2021-11-09 10:14:14 -05:00
Piero Toffanin 264a19c423
Merge pull request #173 from thomas-mc-work/main
Reduce Docker image size
2021-11-09 10:13:38 -05:00
Thomas McWork bdff7c3ff9
reduce Docker image size 2021-11-09 16:08:05 +01:00
Thomas McWork 30ea49e963
reduce Docker image size 2021-11-09 16:00:38 +01:00
Piero Toffanin ffc0c1dcda
Merge pull request #165 from fushinari/key-support-frontend
Expose api_keys in frontend settings
2021-11-07 15:09:47 -05:00
Piero Toffanin 8b0e04c8d1
Merge pull request #169 from dingedi/main
decrease counter if the user wait after the slowdown notice
2021-11-05 11:40:16 -04:00
Sébastien Thuret df0c64450a
remove debug print 2021-11-05 14:59:16 +01:00
Sébastien Thuret adfdc8c9e5
decrease counter if the user wait after the slowdown notice 2021-11-05 14:56:04 +01:00
Piero Toffanin ec382054db
Merge pull request #166 from dingedi/main
Upgrade to Argos Translate 1.5.3
2021-11-04 13:02:18 -04:00
Dingedi a546b8be41
Upgrade to Argos Translate 1.5.3 2021-11-04 17:41:06 +01:00
Mufeed Ali 2375cea3ca
Expose api_keys in frontend settings
This is useful for applications that want to know beforehand if usage of
an API key is supported.
2021-11-03 20:26:04 +05:30
Piero Toffanin 922ba5de40
Merge pull request #164 from fushinari/suggest-req-key
Require API key for submitting suggestions
2021-10-31 12:19:37 -04:00
Mufeed Ali 29eca1fbd4
Forward API key in suggestions client event 2021-10-31 12:57:36 +05:30
Mufeed Ali 3af4ce16e1
Require API key for submitting suggestions
Also removes limiter exemption
2021-10-31 11:23:25 +05:30
Piero Toffanin 56e81856b2
Merge pull request #162 from fushinari/key-req-frontend
Expose require_api_key_origin in frontend settings
2021-10-30 14:36:56 -04:00
Mufeed Ali 915b912c29
Expose require_api_key_origin in frontend settings
This is useful for applications that want to know beforehand if an API
key is necessary before accessing the running instance.
2021-10-30 21:01:50 +05:30
Dingedi 015e77fbf6
Merge pull request #160 from dingedi/main
Upgrade to argos-translate-files v1.0.4
2021-10-27 09:01:23 +02:00
Dingedi 1d3b4424a1
Update image to show new features 2021-10-27 09:00:10 +02:00
Dingedi bbf46aaf5f
Upgrade to argos-translate-files v1.0.4 2021-10-27 08:40:19 +02:00
Dingedi f7a38e2fdb
Upgrade to argos-translate-files v1.0.3 2021-10-27 08:33:08 +02:00
Piero Toffanin f3029d65d5 Remove api_key param from download url 2021-10-26 17:28:59 -04:00
Piero Toffanin c778aa9960 Remove access check for download endpoint 2021-10-26 17:27:33 -04:00
Piero Toffanin 3af5db1db2 Merge branch 'main' of https://github.com/uav4geo/LibreTranslate 2021-10-26 17:13:57 -04:00
Piero Toffanin a82b851a3d Fix API key logic, add some error handling 2021-10-26 17:13:51 -04:00
Piero Toffanin 7727d8ddc3
Merge pull request #157 from dingedi/main
[WIP] Add files translation
2021-10-26 16:06:59 -04:00
Piero Toffanin 93b711aa74 Update spec version 2021-10-26 16:06:12 -04:00
Piero Toffanin 9d95e07d82 Style fix 2021-10-26 16:04:50 -04:00
Piero Toffanin a1244b9e3e Path traversal check 2021-10-26 15:41:14 -04:00
Piero Toffanin d12c81b773 Add VERSION 2021-10-26 15:32:06 -04:00
Sébastien Thuret 42c44a2ba1
fix 2021-10-25 17:24:03 +02:00
Sébastien Thuret b3c089b246
Dont show switch type if files translation disabled 2021-10-25 17:20:57 +02:00
Sébastien Thuret aac45a60ab
remove scheduler if files translation is disabled 2021-10-25 17:12:09 +02:00
Sébastien Thuret 909deccd3f
disable routes 2021-10-25 17:09:23 +02:00
Sébastien Thuret bbc1d61836
Add flag in readme 2021-10-25 17:09:15 +02:00
Dingedi 49f18277f7
Merge branch 'LibreTranslate:main' into main 2021-10-25 17:06:55 +02:00
Sébastien Thuret 5bb8ed7375
Add --disable-files-translation flag 2021-10-25 17:00:44 +02:00
Sébastien Thuret 9e272cb80c
remove uuid and fix downloaded filename 2021-10-25 12:05:39 +02:00
Sébastien Thuret c5f47f0917
add scheduler to remove files after 30 minutes instead of after download 2021-10-25 11:46:49 +02:00
Sébastien Thuret 738dba7476
fix 2021-10-25 11:46:23 +02:00
Sébastien Thuret 5f2d0435f2
change tmp dir 2021-10-25 11:06:39 +02:00
Sébastien Thuret 8d5a418bee
format 2021-10-25 10:56:17 +02:00
Sébastien Thuret bcbaf52777
format 2021-10-25 10:52:55 +02:00
Sébastien Thuret 6a304df2e8
add download link & auto download translated file 2021-10-25 10:50:55 +02:00
Sébastien Thuret b97134ac07
fix 2021-10-25 10:33:49 +02:00
Piero Toffanin 18ea0bae91
Update ISSUE_TEMPLATE.md 2021-10-25 01:40:35 -04:00
Sébastien Thuret 73ebb96171
fix 2021-10-24 19:14:09 +02:00
Sébastien Thuret 4f0d19dbc4
download work 2021-10-24 18:44:35 +02:00
Sébastien Thuret f0d6c92db7
create download_file route 2021-10-24 18:38:35 +02:00
Sébastien Thuret 0b14600199
Add file translation 2021-10-24 18:27:37 +02:00
Sébastien Thuret 82c2f4396d
Add files restriction for input 2021-10-24 17:41:29 +02:00
Sébastien Thuret aaa3c83d4f
add file and translate button display 2021-10-24 17:38:53 +02:00
Sébastien Thuret 50ce3720eb
remove console.log 2021-10-24 17:11:39 +02:00
Sébastien Thuret b6cdc7f35a
Fix missing " = " in requirement 2021-10-24 16:59:09 +02:00
Sébastien Thuret ae5d67d966
Show supported file format and add it to settings 2021-10-24 16:57:45 +02:00
Sébastien Thuret 0da21d96d2
Fix requirement 2021-10-24 16:57:25 +02:00
Sébastien Thuret e0ecf41526
Upgrade to VueJS v2.6.14 2021-10-24 16:57:17 +02:00
Sébastien Thuret 6f63bab8a1
add file input 2021-10-24 13:22:44 +02:00
Sébastien Thuret f906a39f66
make switch type work 2021-10-24 13:15:36 +02:00
Sébastien Thuret 4e1bfd398c
fix background color 2021-10-24 13:13:52 +02:00
Sébastien Thuret e58b8ff97d
Add switch type (text or files ) button 2021-10-24 12:53:52 +02:00
Sébastien Thuret 3e9d4c2c24
Add argos-translate-files in requirements.txt 2021-10-24 12:09:24 +02:00
Piero Toffanin 3c3ff752c6
Merge pull request #156 from rffontenelle/patch-1
Add mention to mobile app in F-Droid store
2021-10-18 14:08:10 -04:00
Rafael Fontenelle 9e25c74ea5
Add mention to mobile app in F-Droid store 2021-10-18 15:03:35 -03:00
Piero Toffanin 59e1be95df
Merge pull request #152 from dingedi/main
Fix display issue on libretranslate.com
2021-10-09 15:31:02 -04:00
Sébastien Thuret 7b3d9f286e
Added version number for assets that may change 2021-10-09 18:25:28 +02:00
Sébastien Thuret 7df304d955
Move the javascript in his own file 2021-10-09 18:16:50 +02:00
Piero Toffanin 2bd7947006
Update README.md 2021-10-09 10:33:21 -04:00
Piero Toffanin c1da77fbeb
Merge pull request #151 from dingedi/main
[WIP] Suggesting corrections to translations
2021-10-09 10:11:11 -04:00
Piero Toffanin 9960baa9c8 Swagger docs definition 2021-10-09 10:04:16 -04:00
Sébastien Thuret 0ff3ca65fb
Disable suggestions by default 2021-10-09 15:45:58 +02:00
Sébastien Thuret 0561deb1b4
Save suggestions in a database 2021-10-09 11:44:07 +02:00
Sébastien Thuret c02658984e
send good parameter 2021-10-09 11:43:22 +02:00
Sébastien Thuret b8f41c8d94
Adding a notification after sending 2021-10-09 11:25:56 +02:00
Sébastien Thuret 130aaecb76
Merge remote-tracking branch 'origin/main' into main 2021-10-09 11:04:58 +02:00
Sébastien Thuret deaa95e3c6
Dont show edit button when loading translations 2021-10-09 11:04:10 +02:00
Dingedi b08cd2cc4c
Merge branch 'LibreTranslate:main' into main 2021-10-09 10:53:22 +02:00
Piero Toffanin 071b1f399b
Update index.html 2021-10-09 00:32:36 -04:00
Sébastien Thuret 5dcb55cad9
fix button state 2021-10-08 19:12:56 +02:00
Sébastien Thuret f5bc2bc3f5
fix 2021-10-08 19:02:30 +02:00
Sébastien Thuret 0214d7bfd6
add buttons 2021-10-08 18:36:24 +02:00
P.J. Finlay cb86d289d3
Merge pull request #150 from dingedi/main
add a mirror that no longer works which works again
2021-10-07 05:59:21 -05:00
Dingedi b2f02dd235
add a mirror that no longer works which works again 2021-10-07 08:50:26 +02:00
Piero Toffanin f9245c56e9
Merge pull request #145 from pierotofy/htmlfix
Fix HTML Translation Formatting, Auto detect HTML in UI
2021-09-24 11:32:28 -04:00
Piero Toffanin a776c21c2a Bump version 2021-09-24 11:31:40 -04:00
Piero Toffanin b73e146baf Fix HTML translation formatting, automatically detect HTML in translation UI 2021-09-24 11:30:19 -04:00
Piero Toffanin d207ed3b88
Merge pull request #142 from dingedi/dingedi-patch-1
patch 1
2021-09-23 11:37:24 -04:00
Dingedi 85b9451a9b
Remove not working mirror 2021-09-23 17:28:09 +02:00
Dingedi 144f60eee3
Upgrade argostranslate to 1.5.2 2021-09-23 17:27:33 +02:00
Dingedi c2b650015b
Avoir useless redirection 2021-09-23 17:24:44 +02:00
51 changed files with 2571 additions and 732 deletions

View file

@ -1 +1,3 @@
.git*
Dockerfile
.Dockerfile.swp

20
.editorconfig Normal file
View file

@ -0,0 +1,20 @@
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
curly_bracket_next_line = false
spaces_around_operators = true
[*.{js,py}]
indent_size = 4
[*.{js,ts}]
quote_type = double
[*.{markdown,md}]
trim_trailing_whitespace = false

1
.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
*.py text eol=lf

3
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1,3 @@
# These are supported funding model platforms
custom: ['https://portal.libretranslate.com']

View file

@ -1,73 +0,0 @@
name: Publish to GitHub Container Registry
# To uav4geo GitHub Container Registry
# https://github.com/orgs/uav4geo/packages
on:
push:
# Publish `main` as Docker `latest` image.
branches:
- main
# Publish `v1.2.3` tags as releases.
tags:
- v*
# Run tests for any PRs.
pull_request:
# Enable manual trigger.
workflow_dispatch:
env:
IMAGE_NAME: libretranslate
# The image ID is generated using the GitHub user ID
# In our case IMAGE_ID: ghcr.io/uav4geo/libretranslate
jobs:
# Test if the docker image build successfully
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run tests
run: |
docker build . --file Dockerfile
# Push image to GitHub Packages.
# See also https://docs.docker.com/docker-hub/builds/
publish:
needs: test
runs-on: ubuntu-latest
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v2
- name: Build image
run: docker build . --file Dockerfile --tag $IMAGE_NAME
# Create an access token with `read:packages` and `write:packages` scopes
# https://github.com/settings/tokens
- name: Log into GitHub Container Registry
run: echo "${{ secrets.CONTAINER_REGISTRY_GITHUB_TOKEN }}" | docker login https://ghcr.io -u ${{ github.actor }} --password-stdin
- name: Push image to GitHub Container Registry
run: |
IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME
# Change all uppercase to lowercase
IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
# Strip git ref prefix from version
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
# Strip "v" prefix from tag name
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
# Use Docker `latest` tag convention
[ "$VERSION" == "main" ] && VERSION=latest
echo IMAGE_ID=$IMAGE_ID
echo VERSION=$VERSION
docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
docker push $IMAGE_ID:$VERSION

53
.github/workflows/publish-docker.yml vendored Normal file
View file

@ -0,0 +1,53 @@
name: Build and Publish Docker Image
on:
push:
branches:
- main
tags:
- v*
jobs:
main:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get Variables
id: get-variables
run: |
echo ::set-output name=version::${GITHUB_REF#refs/tags/}
echo ::set-output name=gh-username-lower::$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]')
- name: Build and push Image
uses: docker/build-push-action@v2
with:
platforms: linux/amd64,linux/arm64
tags: |
${{ steps.get-variables.outputs.gh-username-lower }}/libretranslate:${{ env.TAG }},
ghcr.io/${{ steps.get-variables.outputs.gh-username-lower }}/libretranslate:${{ env.TAG }}
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
env:
TAG: ${{ startsWith(github.ref, 'refs/tags/') && steps.get-variables.outputs.version || 'latest' }}

View file

@ -1,31 +0,0 @@
name: Publish to DockerHub
on:
push:
branches: main
jobs:
main:
runs-on: ubuntu-latest
steps:
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
push: true
tags: libretranslate/libretranslate:latest
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}

View file

@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.6, 3.7, 3.8]
python-version: ['3.7', '3.8', '3.9', '3.10']
steps:
- uses: actions/checkout@v2
@ -29,7 +29,7 @@ jobs:
- name: Check code style with flake8 (lint)
run: |
# warnings if there are Python syntax errors or undefined names
# warnings if there are Python syntax errors or undefined names
# (remove --exit-zero to fail when syntax error)
flake8 . --count --exit-zero --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
@ -38,7 +38,7 @@ jobs:
- name: Test with pytest
run: pytest
publish:
needs: [ tests ]
runs-on: ubuntu-latest

View file

@ -2,9 +2,12 @@ name: Run tests
# Run test at each push to main, if changes to package or tests files
on:
workflow_dispatch:
pull_request:
branches: [ main ]
push:
branches: [ main ]
paths:
- '*.py'
- 'requirements.txt'
- 'app/**'
- 'tests/**'
@ -12,11 +15,11 @@ on:
jobs:
tests:
tests_python:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.6, 3.7, 3.8]
python-version: ['3.7', '3.8', '3.9', '3.10']
steps:
- uses: actions/checkout@v2
@ -33,12 +36,23 @@ jobs:
- name: Check code style with flake8 (lint)
run: |
# warnings if there are Python syntax errors or undefined names
# warnings if there are Python syntax errors or undefined names
# (remove --exit-zero to fail when syntax error)
flake8 . --count --exit-zero --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
run: pytest
run: pytest -v
test_docker_build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Docker build
run: docker build -t libretranslate .
- name: Docker build with some models
run: docker build -t libretranslate --build-arg models=en,es .

1
.gitignore vendored
View file

@ -131,3 +131,4 @@ installed_models/
# Misc
api_keys.db
suggestions.db

View file

@ -1,23 +1,43 @@
FROM python:3.8
ARG with_models=false
FROM python:3.8.14-slim-bullseye as builder
WORKDIR /app
RUN pip install --upgrade pip
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update -qq \
&& apt-get -qqq install --no-install-recommends -y pkg-config gcc g++ \
&& apt-get clean \
&& rm -rf /var/lib/apt
RUN apt-get update && apt-get upgrade --assume-yes
RUN python -mvenv venv && ./venv/bin/pip install --upgrade pip
COPY . .
# check for offline build
RUN if [ "$with_models" = "true" ]; then \
# install only the dependencies first
pip install -e .; \
# initialize the language models
./install_models.py; \
fi
# Install package from source code
RUN pip install .
RUN ./venv/bin/pip install . \
&& ./venv/bin/pip cache purge
FROM python:3.8.14-slim-bullseye
ARG with_models=false
ARG models=
RUN addgroup --system --gid 1032 libretranslate && adduser --system --uid 1032 libretranslate && mkdir -p /home/libretranslate/.local && chown -R libretranslate:libretranslate /home/libretranslate/.local
USER libretranslate
COPY --from=builder --chown=1032:1032 /app /app
WORKDIR /app
RUN if [ "$with_models" = "true" ]; then \
# initialize the language models
if [ ! -z "$models" ]; then \
./venv/bin/python install_models.py --load_only_lang_codes "$models"; \
else \
./venv/bin/python install_models.py; \
fi \
fi
EXPOSE 5000
ENTRYPOINT [ "libretranslate", "--host", "0.0.0.0" ]
ENTRYPOINT [ "./venv/bin/libretranslate", "--host", "0.0.0.0" ]

View file

@ -1,9 +1,9 @@
****************************************
First of all, thank you for taking the time to report an issue.
Please do NOT ask question about LibreTranslate here! We have a forum at https://community.libretranslate.com for questions.
For installation problems, questions, server configuration problems and feature requests, please open a topic on http://community.libretranslate.com
Please open an issue only to report faults and bugs.
Please open an issue only to report faults and bugs!
For feature requests, please open pull requests directly! We accept all kinds of contributions. If you don't know how to code, you can still provide feedback, but please offer to contribute something (how are you going to help the project develop the feature?)
PLEASE REMOVE THIS NOTE AFTER READING IT!
Remove these lines after reading it
****************************************

315
README.md
View file

@ -2,18 +2,19 @@
[Try it online!](https://libretranslate.com) | [API Docs](https://libretranslate.com/docs) | [Community Forum](https://community.libretranslate.com/)
[![Python versions](https://img.shields.io/pypi/pyversions/libretranslate)](https://pypi.org/project/libretranslate) [![Run tests](https://github.com/uav4geo/LibreTranslate/workflows/Run%20tests/badge.svg)](https://github.com/uav4geo/LibreTranslate/actions?query=workflow%3A%22Run+tests%22) [![Publish to DockerHub](https://github.com/uav4geo/LibreTranslate/workflows/Publish%20to%20DockerHub/badge.svg)](https://hub.docker.com/r/libretranslate/libretranslate) [![Publish to GitHub Container Registry](https://github.com/uav4geo/LibreTranslate/workflows/Publish%20to%20GitHub%20Container%20Registry/badge.svg)](https://github.com/uav4geo/LibreTranslate/actions?query=workflow%3A%22Publish+to+GitHub+Container+Registry%22) [![Awesome Humane Tech](https://raw.githubusercontent.com/humanetech-community/awesome-humane-tech/main/humane-tech-badge.svg?sanitize=true)](https://github.com/humanetech-community/awesome-humane-tech)
[![Python versions](https://img.shields.io/pypi/pyversions/libretranslate)](https://pypi.org/project/libretranslate) [![Run tests](https://github.com/LibreTranslate/LibreTranslate/workflows/Run%20tests/badge.svg)](https://github.com/LibreTranslate/LibreTranslate/actions?query=workflow%3A%22Run+tests%22) [![Build and Publish Docker Image](https://github.com/LibreTranslate/LibreTranslate/actions/workflows/publish-docker.yml/badge.svg)](https://github.com/LibreTranslate/LibreTranslate/actions/workflows/publish-docker.yml) [![Publish package](https://github.com/LibreTranslate/LibreTranslate/actions/workflows/publish-package.yml/badge.svg)](https://github.com/LibreTranslate/LibreTranslate/actions/workflows/publish-package.yml) [![Awesome Humane Tech](https://raw.githubusercontent.com/humanetech-community/awesome-humane-tech/main/humane-tech-badge.svg?sanitize=true)](https://github.com/humanetech-community/awesome-humane-tech)
Free and Open Source Machine Translation API, entirely self-hosted. Unlike other APIs, it doesn't rely on proprietary providers such as Google or Azure to perform translations. Instead, its translation engine is powered by the open source [Argos Translate][argo] library.
Free and Open Source Machine Translation API, entirely self-hosted. Unlike other APIs, it doesn't rely on proprietary providers such as Google or Azure to perform translations. Instead, its translation engine is powered by the open source [Argos Translate](https://github.com/argosopentech/argos-translate) library.
![image](https://user-images.githubusercontent.com/64697405/139015751-279f31ac-36f1-4950-9ea7-87e76bf65f51.png)
![image](https://user-images.githubusercontent.com/1951843/121782367-23f90080-cb77-11eb-87fd-ed23a21b730f.png)
[Try it online!](https://libretranslate.com) | [API Docs](https://libretranslate.com/docs)
## API Examples
### Plain Text
### Simple
Request:
@ -39,6 +40,36 @@ Response:
}
```
### Auto Detect Language
Request:
```javascript
const res = await fetch("https://libretranslate.com/translate", {
method: "POST",
body: JSON.stringify({
q: "Ciao!",
source: "auto",
target: "en"
}),
headers: { "Content-Type": "application/json" }
});
console.log(await res.json());
```
Response:
```javascript
{
"detectedLanguage": {
"confidence": 83,
"language": "it"
},
"translatedText": "Bye!"
}
```
### HTML (beta)
Request:
@ -68,9 +99,9 @@ Response:
## Install and Run
You can run your own API server in just a few lines of setup!
You can run your own API server with just a few lines of setup!
Make sure you have installed Python (3.8 or higher is recommended), then simply issue:
Make sure you have Python installed (3.8 or higher is recommended), then simply run:
```bash
pip install libretranslate
@ -79,16 +110,14 @@ libretranslate [args]
Then open a web browser to http://localhost:5000
If you're on Windows, we recommend you [Run with Docker](#run-with-docker) instead.
On Ubuntu 20.04 you can also use the install script available on https://github.com/argosopentech/LibreTranslate-init
On Ubuntu 20.04 you can also use the install script available at https://github.com/argosopentech/LibreTranslate-init
## Build and Run
If you want to make some changes to the code, you can build from source, and run the API:
If you want to make changes to the code, you can build from source, and run the API:
```bash
git clone https://github.com/uav4geo/LibreTranslate
git clone https://github.com/LibreTranslate/LibreTranslate
cd LibreTranslate
pip install -e .
libretranslate [args]
@ -101,21 +130,19 @@ Then open a web browser to http://localhost:5000
### Run with Docker
Simply run:
```bash
docker run -ti --rm -p 5000:5000 libretranslate/libretranslate
```
Linux/MacOS: `./run.sh [args]`
Windows: `run.bat [args]`
Then open a web browser to http://localhost:5000
### Build with Docker
```bash
docker build [--build-arg with_models=true] -t libretranslate .
```
If you want to run the Docker image in a complete offline environment, you need to add the `--build-arg with_models=true` parameter. Then the language models get downloaded during the build process of the image. Otherwise these models get downloaded on the first run of the image/container.
If you want to run the Docker image in a complete offline environment, you need to add the `--build-arg with_models=true` parameter. Then the language models are downloaded during the build process of the image. Otherwise these models get downloaded on the first run of the image/container.
Run the built image:
@ -129,30 +156,69 @@ Or build and run using `docker-compose`:
docker-compose up -d --build
```
> Feel free to change the [`docker-compose.yml`](https://github.com/uav4geo/LibreTranslate/blob/main/docker-compose.yml) file to adapt it to your deployment needs, or use an extra `docker-compose.prod.yml` file for your deployment configuration.
> Feel free to change the [`docker-compose.yml`](https://github.com/LibreTranslate/LibreTranslate/blob/main/docker-compose.yml) file to adapt it to your deployment needs, or use an extra `docker-compose.prod.yml` file for your deployment configuration.
> The models are stored inside the container under `/home/libretranslate/.local/share` and `/home/libretranslate/.local/cache`. Feel free to use volumes if you do not want to redownload the models when the container is destroyed. To update the models, use the `--update-models` argument.
### CUDA
You can use hardware acceleration to speed up translations on a GPU machine with CUDA 11.2 and [nvidia-docker](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html) installed.
Run this version with:
```bash
docker-compose -f docker-compose.cuda.yml up -d --build
```
## Arguments
| Argument | Description | Default | Env. name |
| ------------- | ------------------------------ | -------------------- | ---------------------- |
| --host | Set host to bind the server to | `127.0.0.1` | LT_HOST |
| --port | Set port to bind the server to | `5000` | LT_PORT |
| --char-limit | Set character limit | `No limit` | LT_CHAR_LIMIT |
| --req-limit | Set maximum number of requests per minute per client | `No limit` | LT_REQ_LIMIT |
| --batch-limit | Set maximum number of texts to translate in a batch request | `No limit` | LT_BATCH_LIMIT |
| --ga-id | Enable Google Analytics on the API client page by providing an ID | `No tracking` | LT_GA_ID |
| --debug | Enable debug environment | `False` | LT_DEBUG |
| --ssl | Whether to enable SSL | `False` | LT_SSL |
| --frontend-language-source | Set frontend default language - source | `en` | LT_FRONTEND_LANGUAGE_SOURCE |
| --frontend-language-target | Set frontend default language - target | `es` | LT_FRONTEND_LANGUAGE_TARGET |
| --frontend-timeout | Set frontend translation timeout | `500` | LT_FRONTEND_TIMEOUT |
| --api-keys | Enable API keys database for per-user rate limits lookup | `Don't use API keys` | LT_API_KEYS |
| --require-api-key-origin | Require use of an API key for programmatic access to the API, unless the request origin matches this domain | `No restrictions on domain origin` | LT_REQUIRE_API_KEY_ORIGIN |
| --load-only | Set available languages | `all from argostranslate` | LT_LOAD_ONLY |
| Argument | Description | Default | Env. name |
|-----------------------------|-------------------------------------------------------------------------------------------------------------| -------------------- |------------------------------|
| --host | Set host to bind the server to | `127.0.0.1` | LT_HOST |
| --port | Set port to bind the server to | `5000` | LT_PORT |
| --char-limit | Set character limit | `No limit` | LT_CHAR_LIMIT |
| --req-limit | Set maximum number of requests per minute per client | `No limit` | LT_REQ_LIMIT |
| --req-limit-storage | Storage URI to use for request limit data storage. See [Flask Limiter](https://flask-limiter.readthedocs.io/en/stable/configuration.html) | `memory://` | LT_REQ_LIMIT_STORAGE |
| --batch-limit | Set maximum number of texts to translate in a batch request | `No limit` | LT_BATCH_LIMIT |
| --ga-id | Enable Google Analytics on the API client page by providing an ID | `No tracking` | LT_GA_ID |
| --debug | Enable debug environment | `False` | LT_DEBUG |
| --ssl | Whether to enable SSL | `False` | LT_SSL |
| --frontend-language-source | Set frontend default language - source | `en` | LT_FRONTEND_LANGUAGE_SOURCE |
| --frontend-language-target | Set frontend default language - target | `es` | LT_FRONTEND_LANGUAGE_TARGET |
| --frontend-timeout | Set frontend translation timeout | `500` | LT_FRONTEND_TIMEOUT |
| --api-keys | Enable API keys database for per-user rate limits lookup | `Don't use API keys` | LT_API_KEYS |
| --api-keys-db-path | Use a specific path inside the container for the local database. Can be absolute or relative | `db/api_keys.db` | LT_API_KEYS_DB_PATH |
| --api-keys-remote | Use this remote endpoint to query for valid API keys instead of using the local database | `Use local API key database` | LT_API_KEYS_REMOTE |
| --get-api-key-link | Show a link in the UI where to direct users to get an API key | `Don't show a link` | LT_GET_API_KEY_LINK |
| --require-api-key-origin | Require use of an API key for programmatic access to the API, unless the request origin matches this domain | `No restrictions on domain origin` | LT_REQUIRE_API_KEY_ORIGIN |
| --load-only | Set available languages | `all from argostranslate` | LT_LOAD_ONLY |
| --threads | Set number of threads | `4` | LT_THREADS |
| --suggestions | Allow user suggestions | `false` | LT_SUGGESTIONS |
| --disable-files-translation | Disable files translation | `false` | LT_DISABLE_FILES_TRANSLATION |
| --disable-web-ui | Disable web ui | `false` | LT_DISABLE_WEB_UI |
| --update-models | Update language models at startup | `false` | LT_UPDATE_MODELS |
Note that each argument has an equivalent env. variable that can be used instead. The env. variables overwrite the default values but have lower priority than the command aguments. They are particularly useful if used with Docker. Their name is the upper-snake case of the command arguments' ones, with a `LT` prefix.
Note that each argument has an equivalent environment variable that can be used instead. The env. variables overwrite the default values but have lower priority than the command arguments and are particularly useful if used with Docker. The environment variable names are the upper-snake-case of the equivalent command argument's name with a `LT` prefix.
## Run with Gunicorn
## Update
### Software
If you installed with pip:
`pip install -U libretranslate`
If you're using docker:
`docker pull libretranslate/libretranslate`
### Language Models
Start the program with the `--update-models` argument. For example: `libretranslate --update-models` or `./run.sh --update-models`.
Alternatively you can also run the `install_models.py` script.
## Run with WSGI and Gunicorn
```
pip install gunicorn
@ -166,11 +232,15 @@ You can pass application arguments directly to Gunicorn via:
gunicorn --bind 0.0.0.0:5000 'wsgi:app(api_keys=True)'
```
## Run with Kubernetes
See ["LibreTranslate: your own translation service on Kubernetes" by JM Robles](https://jmrobles.medium.com/libretranslate-your-own-translation-service-on-kubernetes-b46c3e1af630)
## Manage API Keys
LibreTranslate supports per-user limit quotas, e.g. you can issue API keys to users so that they can enjoy higher requests limits per minute (if you also set `--req-limit`). By default all users are rate-limited based on `--req-limit`, but passing an optional `api_key` parameter to the REST endpoints allows a user to enjoy higher request limits.
To use API keys simply start LibreTranslate with the `--api-keys` option.
To use API keys simply start LibreTranslate with the `--api-keys` option. If you modified the API keys database path with the option `--api-keys-db-path`, you must specify the path with the same argument flag when using the `ltmanage keys` command.
### Add New Keys
@ -180,6 +250,11 @@ To issue a new API key with 120 requests per minute limits:
ltmanage keys add 120
```
If you changed the API keys database path:
```bash
ltmanage keys --api-keys-db-path path/to/db/dbName.db add 120
```
### Remove Keys
```bash
@ -201,11 +276,13 @@ You can use the LibreTranslate API using the following bindings:
- .Net: https://github.com/sigaloid/LibreTranslate.Net
- Go: https://github.com/SnakeSel/libretranslate
- Python: https://github.com/argosopentech/LibreTranslate-py
- PHP: https://github.com/jefs42/libretranslate
- C++: https://github.com/argosopentech/LibreTranslate-cpp
- Swift: https://github.com/wacumov/libretranslate
- Unix: https://github.com/argosopentech/LibreTranslate-sh
More coming soon!
- Shell: https://github.com/Hayao0819/Hayao-Tools/tree/master/libretranslate-sh
- Java: https://github.com/suuft/libretranslate-java
-
## Discourse Plugin
You can use this [discourse translator plugin](https://github.com/LibreTranslate/discourse-translator) to translate [Discourse](https://discourse.org) topics. To install it simply modify `/var/discourse/containers/app.yml`:
@ -227,24 +304,41 @@ Then issue `./launcher rebuild app`. From the Discourse's admin panel then selec
## Mobile Apps
- [LibreTranslater](https://gitlab.com/BeowuIf/libretranslater) is an Android app available on the Play Store (https://play.google.com/store/apps/details?id=de.beowulf.libretranslater) that uses the LibreTranslate API.
- [LibreTranslater](https://gitlab.com/BeowuIf/libretranslater) is an Android app [available on the Play Store](https://play.google.com/store/apps/details?id=de.beowulf.libretranslater) and [in the F-Droid store](https://f-droid.org/packages/de.beowulf.libretranslater/) that uses the LibreTranslate API.
- [LiTranslate](https://github.com/viktorkalyniuk/LiTranslate-iOS) is an iOS app [available on the App Store](https://apps.apple.com/us/app/litranslate/id1644385339) that uses the LibreTranslate API.
## Web browser
- [minbrowser](https://minbrowser.org/) is a web browser with [integrated LibreTranslate support](https://github.com/argosopentech/argos-translate/discussions/158#discussioncomment-1141551).
- A LibreTranslate Firefox addon is [currently a work in progress](https://github.com/LibreTranslate/LibreTranslate/issues/55).
## Mirrors
This is a list of online resources that serve the LibreTranslate API. Some require an API key. If you want to add a new URL, please open a pull request.
This is a list of public LibreTranslate instances, some require an API key. If you want to add a new URL, please open a pull request.
URL |API Key Required|Contact|Cost
URL |API Key Required | Links
--- | --- | ---
[libretranslate.com](https://libretranslate.com)|:heavy_check_mark:|[Get API Key](https://portal.libretranslate.com)
[libretranslate.de](https://libretranslate.de)|-
[translate.argosopentech.com](https://translate.argosopentech.com/)|-
[translate.api.skitzen.com](https://translate.api.skitzen.com/)|-
[translate.fortytwo-it.com](https://translate.fortytwo-it.com/)|-
[translate.terraprint.co](https://translate.terraprint.co/)|-
[lt.vern.cc](https://lt.vern.cc)|-
## TOR/i2p Mirrors
URL |API Key Required|Payment Link|Cost
--- | --- | --- | ---
[libretranslate.com](https://libretranslate.com)|:heavy_check_mark:|[UAV4GEO](https://uav4geo.com/contact)| [$9 / month](https://buy.stripe.com/28obLvdgGcIE5AQfYY), 80 requests / minute limit
[libretranslate.de](https://libretranslate.de/)|-|-
[translate.mentality.rip](https://translate.mentality.rip)|-|-
[translate.astian.org](https://translate.astian.org/)|-|-
[translate.argosopentech.com](https://translate.argosopentech.com/)|-|-
[lt.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion](http://lt.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion/)|-|-
[lt.vern.i2p](http://vernf45n7mxwqnp5riaax7p67pwcl7wcefdcnqqvim7ckdx4264a.b32.i2p/)|-|-
## Adding New Languages
To add new languages you first need to train an Argos Translate model. See [this video](https://odysee.com/@argosopentech:7/training-an-Argos-Translate-model-tutorial-2022:2?r=DMnK7NqdPNHRCfwhmKY9LPow3PqVUUgw) for details.
First you need to collect data, for example from [Opus](http://opus.nlpl.eu/), then you need to add the data to [data-index.json](https://github.com/argosopentech/argos-train/blob/master/data-index.json) in the [Argos Train](https://github.com/argosopentech/argos-train) repo.
## Roadmap
Help us by opening a pull request!
@ -253,17 +347,17 @@ Help us by opening a pull request!
- [x] Auto-detect input language (thanks [@vemonet](https://github.com/vemonet) !)
- [X] User authentication / tokens
- [ ] Language bindings for every computer language
- [ ] [Improved translations](https://github.com/argosopentech/argos-parallel-corpus)
- [ ] [Improved translations](https://community.libretranslate.com/t/the-best-way-to-train-models/172)
## FAQ
### Can I use your API server at libretranslate.com for my application in production?
The API on libretranslate.com should be used for testing, personal or infrequent use. If you're going to run an application in production, please [get in touch](https://uav4geo.com/contact) to get an API key or discuss other options.
In short, no. [You need to buy an API key](https://portal.libretranslate.com). You can always run LibreTranslate for free on your own server of course.
### Can I use this behind a reverse proxy, like Apache2?
### Can I use LibreTranslate behind a reverse proxy, like Apache2 or Caddy?
Yes, here is an example Apache2 config that redirects a subdomain (with HTTPS certificate) to LibreTranslate running on a docker at localhost.
Yes, here are config examples for Apache2 and Caddy that redirect a subdomain (with HTTPS certificate) to LibreTranslate running on a docker at localhost.
```
sudo docker run -ti --rm -p 127.0.0.1:5000:5000 libretranslate/libretranslate
```
@ -274,7 +368,7 @@ Add `--restart unless-stopped` if you want this docker to start on boot, unless
<details>
<summary>Apache config</summary>
<br>
Replace [YOUR_DOMAIN] with your full domain; for example, `translate.domain.tld` or `libretranslate.domain.tld`.
Remove `#` on the ErrorLog and CustomLog lines to log requests.
@ -296,7 +390,8 @@ Remove `#` on the ErrorLog and CustomLog lines to log requests.
ProxyPass / http://127.0.0.1:5000/
ProxyPassReverse / http://127.0.0.1:5000/
ProxyPreserveHost On
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/[YOUR_DOMAIN]/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/[YOUR_DOMAIN]/privkey.pem
@ -312,12 +407,120 @@ Add this to an existing site config, or a new file in `/etc/apache2/sites-availa
To get a HTTPS subdomain certificate, install `certbot` (snap), run `sudo certbot certonly --manual --preferred-challenges dns` and enter your information (with `subdomain.domain.tld` as the domain). Add a DNS TXT record with your domain registrar when asked. This will save your certificate and key to `/etc/letsencrypt/live/{subdomain.domain.tld}/`. Alternatively, comment the SSL lines out if you don't want to use HTTPS.
</details>
<details>
<summary>Caddy config</summary>
<br>
Replace [YOUR_DOMAIN] with your full domain; for example, `translate.domain.tld` or `libretranslate.domain.tld`.
```Caddyfile
#Libretranslate
[YOUR_DOMAIN] {
reverse_proxy localhost:5000
}
```
Add this to an existing Caddyfile or save it as `Caddyfile` in any directory and run `sudo caddy reload` in that same directory.
</details>
<details>
<summary>NGINX config</summary>
<br>
Replace [YOUR_DOMAIN] with your full domain; for example, `translate.domain.tld` or `libretranslate.domain.tld`.
Remove `#` on the `access_log` and `error_log` lines to disable logging.
```NginxConf
server {
listen 80;
server_name [YOUR_DOMAIN];
return 301 https://$server_name$request_uri;
}
server {
listen 443 http2 ssl;
server_name [YOUR_DOMAIN];
#access_log off;
#error_log off;
# SSL Section
ssl_certificate /etc/letsencrypt/live/[YOUR_DOMAIN]/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/[YOUR_DOMAIN]/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
# Using the recommended cipher suite from: https://wiki.mozilla.org/Security/Server_Side_TLS
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';
ssl_session_timeout 10m;
ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
ssl_session_tickets off;
# Specifies a curve for ECDHE ciphers.
ssl_ecdh_curve prime256v1;
# Server should determine the ciphers, not the client
ssl_prefer_server_ciphers on;
# Header section
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header Referrer-Policy "strict-origin" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Download-Options "noopen" always;
add_header X-Robots-Tag "none" always;
add_header Feature-Policy "microphone 'none'; camera 'none'; geolocation 'none';" always;
# Newer header but not everywhere supported
add_header Permissions-Policy "microphone=(), camera=(), geolocation=()" always;
# Remove X-Powered-By, which is an information leak
fastcgi_hide_header X-Powered-By;
# Do not send nginx server header
server_tokens off;
# GZIP Section
gzip on;
gzip_disable "msie6";
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_min_length 256;
gzip_types text/xml text/javascript font/ttf font/eot font/otf application/x-javascript application/atom+xml application/javascript application/json application/manifest+json application/rss+xml application/x-web-app-manifest+json application/xhtml+xml application/xml image/svg+xml image/x-icon text/css text/plain;
location / {
proxy_pass http://127.0.0.1:5000/;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 0;
}
}
```
Add this to an existing NGINX config or save it as `libretranslate` in the `/etc/nginx/site-enabled` directory and run `sudo nginx -s reload`.
</details>
## Credits
This work is largely possible thanks to [Argos Translate][argo], which powers the translation engine.
This work is largely possible thanks to [Argos Translate](https://github.com/argosopentech/argos-translate), which powers the translation engine.
## License
[GNU Affero General Public License v3](https://www.gnu.org/licenses/agpl-3.0.en.html)
[argo]: https://github.com/argosopentech/argos-translate
## Trademark
See [Trademark Guidelines](https://github.com/LibreTranslate/LibreTranslate/blob/main/TRADEMARK.md)

86
TRADEMARK.md Normal file
View file

@ -0,0 +1,86 @@
# LibreTranslate Trademark Guidelines
LibreTranslate is an open source organization. Because we make our code available to download and modify, proper use of our trademarks is essential to inform users whether or not LibreTranslate stands behind a product or service. When using LibreTranslate trademarks (including some product names that are part of our organization) you must comply with these LibreTranslate Trademark Guidelines.
Some of our trademark names include:
* LibreTranslate
Some of our trademark logos include:
![LibreTranslate](https://avatars.githubusercontent.com/u/77352747?s=200&v=4)
However, this is not a complete list of our names, logos, and brand features, all of which are subject to these guidelines.
If you want to report misuse of an LibreTranslate trademark, please contact us via https://uav4geo.com/contact
## When do I need specific permission to use an LibreTranslate trademark?
Noting that LibreTranslate software combined with, or integrated into, any other software program, including but not limited to automation software for offering LibreTranslate as a cloud service or orchestration software for offering LibreTranslate in containers is considered "modified" LibreTranslate software, you may do the following without receiving specific permission from LibreTranslate (or its affiliates):
* Use LibreTranslate wordmarks and/or logos in unmodified versions of LibreTranslate programs, products, services and technologies.
* Use LibreTranslate wordmarks in text to truthfully refer to and/or link to unmodified LibreTranslate programs, products, services and technologies.
* Use LibreTranslate logos in visuals to truthfully refer to and/or to link to the applicable programs, products, services and technologies hosted on LibreTranslate servers.
* Use LibreTranslate wordmarks to explain that your software is based on LibreTranslate's open source code, or is compatible with LibreTranslate's software.
* Describe a social media account, page, or community in accordance with the [Social Media Guidelines](#social-media-guidelines).
All other uses of an LibreTranslate trademark require our prior written permission. This includes any use of a LibreTranslate trademark in a domain name. Contact us at https://uav4geo.com/contact for more information.
## When allowed, how should I use an LibreTranslate trademark?
### General Guidelines
#### Do:
* Use the LibreTranslate trademark exactly as shown in the list above.
* Use LibreTranslate wordmarks only as an adjective, never as a noun or verb. Do not use them in plural or possessive forms. Instead, use the generic term for the LibreTranslate product or service following the trademark, for example: LibreTranslate translation software.
#### Don't:
* Don't use LibreTranslate trademarks in the name of your business, product, service, app, domain name, publication, or other offering.
* Don't use marks, logos, company names, slogans, domain names, or designs that are confusingly similar to LibreTranslate trademarks.
* Don't use LibreTranslate trademarks in a way that incorrectly implies affiliation with, or sponsorship, endorsement, or approval by LibreTranslate of your products or services.
* Don't display LibreTranslate trademarks more prominently than your product, service, or company name.
* Don't use LibreTranslate trademarks on merchandise for sale (e.g., selling t-shirts, mugs, etc.)
* Don't use LibreTranslate trademarks for any other form of commercial use (e.g. offering technical support services), unless such use is limited to a truthful and descriptive reference (e.g. Independent technical support for LibreTranslate's software).
* Don't modify LibreTranslate's trademarks, abbreviate them, or combine them with any other symbols, words, or images, or incorporate them into a tagline or slogan.
### Social Media Guidelines
In addition to the General Guidelines above, the name and handle of your social media account and any and all pages cannot begin with an LibreTranslate trademark. In addition, LibreTranslate logos cannot be used in a way that might suggest affiliation with LibreTranslate, including, but not limited to, the account, profile, or header images. The only exception to these requirements is if you've received prior permission from LibreTranslate.
For example, you cannot name your account, page, or community "LibreTranslate Representatives" or "LibreTranslate Software". However, it would be acceptable to name your account, page, or community "Fans of LibreTranslate" or "Information about LibreTranslate Software" as long as you do not use the LibreTranslate trademarks or LibreTranslate logos or otherwise suggest any affiliation with LibreTranslate.
### Open Source Project Guidelines
The specific license for each of LibreTranslate's software products and code says what you can and cannot do with the code itself but does not give permission to use LibreTranslate's trademarks. If you choose to build on or modify LibreTranslate's open source code for your own project,
#### You Must:
* Follow the terms of the Open Source License(s) for LibreTranslate software products and code.
* Choose branding, logos, and trademarks that denotes your own unique identity so as to clearly signal to users that there is no affiliation with or endorsement by LibreTranslate.
* Follow the General Guidelines, above.
#### You Must NOT:
* Use any LibreTranslate trademark in connection with the user-facing name or branding of your project.
* Use any LibreTranslate trademark or any part of any LibreTranslate trademark to incorrectly suggest or give the impression your software is actually published by, affiliated with, or endorsed by LibreTranslate.
For example, please do not name your project, [Something]-LibreTranslate, or LibreTranslate-[Something]
#### You May:
* State in words (not using logos or images) that your product "works with" or "is compatible" with certain LibreTranslate software products, if that is true.
* State in words (not using logos or images) that your project is based on LibreTranslate open source technology, if that is true, as long as you also include a statement that your project is not officially associated with LibreTranslate or its products.
For instance, you may state that your project:
"is proudly built from LibreTranslate's open source software"
as long as you also include the statement equally prominently:
"[Brand Name] and [Product Name] are not officially associated with LibreTranslate or its products."
### LibreTranslate Community Guidelines
Various permissions to use LibreTranslate Trademarks have been provided to various members of the LibreTranslate Community, and these LibreTranslate Trademark Guidelines do not alter any such previously granted permissions.

1
VERSION Normal file
View file

@ -0,0 +1 @@
1.3.4

View file

@ -1,13 +1,3 @@
import os
# override polyglot path
import polyglot
from appdirs import user_data_dir
polyglot.polyglot_path = os.path.join(
user_data_dir(appname="LibreTranslate", appauthor="uav4geo"), "polyglot_data"
)
from .main import main
from .manage import manage

View file

@ -1,13 +1,26 @@
import os
import sqlite3
import uuid
import requests
from expiringdict import ExpiringDict
from app.default_values import DEFAULT_ARGUMENTS as DEFARGS
DEFAULT_DB_PATH = "api_keys.db"
DEFAULT_DB_PATH = DEFARGS['API_KEYS_DB_PATH']
class Database:
def __init__(self, db_path=DEFAULT_DB_PATH, max_cache_len=1000, max_cache_age=30):
# Legacy check - this can be removed at some point in the near future
if os.path.isfile("api_keys.db") and not os.path.isfile("db/api_keys.db"):
print("Migrating %s to %s" % ("api_keys.db", "db/api_keys.db"))
try:
os.rename("api_keys.db", "db/api_keys.db")
except Exception as e:
print(str(e))
db_dir = os.path.dirname(db_path)
if not db_dir == "" and not os.path.exists(db_dir):
os.makedirs(db_dir)
self.db_path = db_path
self.cache = ExpiringDict(max_len=max_cache_len, max_age_seconds=max_cache_age)
@ -61,3 +74,27 @@ class Database:
def all(self):
row = self.c.execute("SELECT api_key, req_limit FROM api_keys")
return row.fetchall()
class RemoteDatabase:
def __init__(self, url, max_cache_len=1000, max_cache_age=600):
self.url = url
self.cache = ExpiringDict(max_len=max_cache_len, max_age_seconds=max_cache_age)
def lookup(self, api_key):
req_limit = self.cache.get(api_key)
if req_limit is None:
try:
r = requests.post(self.url, data={'api_key': api_key})
res = r.json()
except Exception as e:
print("Cannot authenticate API key: " + str(e))
return None
if res.get('error', None) is None:
req_limit = res.get('req_limit', None)
else:
req_limit = None
self.cache[api_key] = req_limit
return req_limit

View file

@ -1,16 +1,52 @@
import io
import os
import tempfile
import uuid
from functools import wraps
from html import unescape
from flask import Flask, abort, jsonify, render_template, request
import argostranslatefiles
from argostranslatefiles import get_supported_formats
from flask import (Flask, abort, jsonify, render_template, request, send_file,
url_for)
from flask_swagger import swagger
from flask_swagger_ui import get_swaggerui_blueprint
from app import flood
from app.language import detect_languages, transliterate
from .api_keys import Database
from translatehtml import translate_html
from werkzeug.utils import secure_filename
from app import flood, remove_translated_files, security
from app.language import detect_languages, improve_translation_formatting
from .api_keys import Database, RemoteDatabase
from .suggestions import Database as SuggestionsDatabase
def get_version():
try:
with open("VERSION") as f:
return f.read().strip()
except:
return "?"
def get_upload_dir():
upload_dir = os.path.join(tempfile.gettempdir(), "libretranslate-files-translate")
if not os.path.isdir(upload_dir):
os.mkdir(upload_dir)
return upload_dir
def get_req_api_key():
if request.is_json:
json = get_json_dict(request)
ak = json.get("api_key")
else:
ak = request.values.get("api_key")
return ak
def get_json_dict(request):
d = request.get_json()
@ -28,21 +64,17 @@ def get_remote_address():
return ip
def get_req_limits(default_limit, api_keys_db, multiplier = 1):
def get_req_limits(default_limit, api_keys_db, multiplier=1):
req_limit = default_limit
if api_keys_db:
if request.is_json:
json = get_json_dict(request)
api_key = json.get("api_key")
else:
api_key = request.values.get("api_key")
api_key = get_req_api_key()
if api_key:
db_req_limit = api_keys_db.lookup(api_key)
if db_req_limit is not None:
req_limit = db_req_limit * multiplier
return req_limit
@ -68,15 +100,22 @@ def get_routes_limits(default_req_limit, daily_req_limit, api_keys_db):
def create_app(args):
from app.init import boot
boot(args.load_only)
boot(args.load_only, args.update_models)
from app.language import languages
from app.language import load_languages
app = Flask(__name__)
if args.debug:
app.config["TEMPLATES_AUTO_RELOAD"] = True
if not args.disable_files_translation:
remove_translated_files.setup(get_upload_dir())
languages = load_languages()
language_pairs = {}
for lang in languages:
language_pairs[lang.code] = sorted([l.to_lang.code for l in lang.translations_from])
# Map userdefined frontend languages to argos language object.
if args.frontend_language_source == "auto":
frontend_argos_language_source = type(
@ -92,20 +131,30 @@ def create_app(args):
iter([l for l in languages if l.code == args.frontend_language_target]), None
)
frontend_argos_supported_files_format = []
for file_format in get_supported_formats():
for ff in file_format.supported_file_extensions:
frontend_argos_supported_files_format.append(ff)
# Raise AttributeError to prevent app startup if user input is not valid.
if frontend_argos_language_source is None:
raise AttributeError(
f"{args.frontend_language_source} as frontend source language is not supported."
)
frontend_argos_language_source = languages[0]
if frontend_argos_language_target is None:
raise AttributeError(
f"{args.frontend_language_target} as frontend target language is not supported."
)
if len(languages) >= 2:
frontend_argos_language_target = languages[1]
else:
frontend_argos_language_target = languages[0]
api_keys_db = None
if args.req_limit > 0 or args.api_keys or args.daily_req_limit > 0:
api_keys_db = Database() if args.api_keys else None
api_keys_db = None
if args.api_keys:
if args.api_keys_remote:
api_keys_db = RemoteDatabase(args.api_keys_remote)
else:
api_keys_db = Database(args.api_keys_db_path)
from flask_limiter import Limiter
@ -115,6 +164,7 @@ def create_app(args):
default_limits=get_routes_limits(
args.req_limit, args.daily_req_limit, api_keys_db
),
storage_uri=args.req_limit_storage,
)
else:
from .no_limiter import Limiter
@ -127,22 +177,31 @@ def create_app(args):
def access_check(f):
@wraps(f)
def func(*a, **kw):
if flood.is_banned(get_remote_address()):
ip = get_remote_address()
if flood.is_banned(ip):
abort(403, description="Too many request limits violations")
if args.api_keys and args.require_api_key_origin:
if request.is_json:
json = get_json_dict(request)
ak = json.get("api_key")
else:
ak = request.values.get("api_key")
if args.api_keys:
ak = get_req_api_key()
if (
api_keys_db.lookup(ak) is None and request.headers.get("Origin") != args.require_api_key_origin
ak and api_keys_db.lookup(ak) is None
):
abort(
403,
description="Please contact the server operator to obtain an API key",
description="Invalid API key",
)
elif (
args.require_api_key_origin
and api_keys_db.lookup(ak) is None
and request.headers.get("Origin") != args.require_api_key_origin
):
description = "Please contact the server operator to get an API key"
if args.get_api_key_link:
description = "Visit %s to get an API key" % args.get_api_key_link
abort(
403,
description=description,
)
return f(*a, **kw)
@ -169,20 +228,28 @@ def create_app(args):
@app.route("/")
@limiter.exempt
def index():
if args.disable_web_ui:
abort(404)
return render_template(
"index.html",
gaId=args.ga_id,
frontendTimeout=args.frontend_timeout,
api_keys=args.api_keys,
get_api_key_link=args.get_api_key_link,
web_version=os.environ.get("LT_WEB") is not None,
version=get_version()
)
@app.route("/javascript-licenses", methods=["GET"])
@app.get("/javascript-licenses")
@limiter.exempt
def javascript_licenses():
if args.disable_web_ui:
abort(404)
return render_template("javascript-licenses.html")
@app.route("/languages", methods=["GET", "POST"])
@app.get("/languages")
@limiter.exempt
def langs():
"""
@ -205,17 +272,13 @@ def create_app(args):
name:
type: string
description: Human-readable language name (in English)
429:
description: Slow down
schema:
id: error-slow-down
type: object
properties:
error:
type: string
description: Reason for slow down
targets:
type: array
items:
type: string
description: Supported target language codes
"""
return jsonify([{"code": l.code, "name": l.name} for l in languages])
return jsonify([{"code": l.code, "name": l.name, "targets": language_pairs.get(l.code, [])} for l in languages])
# Add cors
@app.after_request
@ -230,7 +293,7 @@ def create_app(args):
response.headers.add("Access-Control-Max-Age", 60 * 60 * 24 * 20)
return response
@app.route("/translate", methods=["POST"])
@app.post("/translate")
@access_check
def translate():
"""
@ -358,7 +421,7 @@ def create_app(args):
abort(
400,
description="Invalid request: Request (%d) exceeds text limit (%d)"
% (batch_size, args.batch_limit),
% (batch_size, args.batch_limit),
)
if args.char_limit != -1:
@ -371,40 +434,40 @@ def create_app(args):
abort(
400,
description="Invalid request: Request (%d) exceeds character limit (%d)"
% (chars, args.char_limit),
% (chars, args.char_limit),
)
if source_lang == "auto":
source_langs = []
if batch:
auto_detect_texts = q
auto_detect_texts = q
else:
auto_detect_texts = [q]
auto_detect_texts = [q]
overall_candidates = detect_languages(q)
for text_to_check in auto_detect_texts:
if len(text_to_check) > 40:
candidate_langs = detect_languages(text_to_check)
else:
# Unable to accurately detect languages for short texts
candidate_langs = overall_candidates
source_langs.append(candidate_langs[0]["language"])
if len(text_to_check) > 40:
candidate_langs = detect_languages(text_to_check)
else:
# Unable to accurately detect languages for short texts
candidate_langs = overall_candidates
source_langs.append(candidate_langs[0])
if args.debug:
print(text_to_check, candidate_langs)
print("Auto detected: %s" % candidate_langs[0]["language"])
if args.debug:
print(text_to_check, candidate_langs)
print("Auto detected: %s" % candidate_langs[0]["language"])
else:
if batch:
source_langs = [source_lang for text in q]
else:
source_langs = [source_lang]
if batch:
source_langs = [ {"confidence": 100.0, "language": source_lang} for text in q]
else:
source_langs = [ {"confidence": 100.0, "language": source_lang} ]
src_langs = [next(iter([l for l in languages if l.code == source_lang["language"]]), None) for source_lang in source_langs]
src_langs = [next(iter([l for l in languages if l.code == source_lang]), None) for source_lang in source_langs]
for idx, lang in enumerate(src_langs):
if lang is None:
abort(400, description="%s is not supported" % source_langs[idx])
if lang is None:
abort(400, description="%s is not supported" % source_langs[idx])
tgt_lang = next(iter([l for l in languages if l.code == target_lang]), None)
@ -417,40 +480,220 @@ def create_app(args):
if text_format not in ["text", "html"]:
abort(400, description="%s format is not supported" % text_format)
try:
if batch:
results = []
for idx, text in enumerate(q):
translator = src_langs[idx].get_translation(tgt_lang)
translator = src_langs[idx].get_translation(tgt_lang)
if translator is None:
abort(400, description="%s (%s) is not available as a target language from %s (%s)" % (tgt_lang.name, tgt_lang.code, src_langs[idx].name, src_langs[idx].code))
if text_format == "html":
translated_text = str(translate_html(translator, transliterate(text, target_lang=source_langs[idx])))
else:
translated_text = translator.translate(transliterate(text, target_lang=source_langs[idx]))
if text_format == "html":
translated_text = str(translate_html(translator, text))
else:
translated_text = improve_translation_formatting(text, translator.translate(text))
results.append(translated_text)
return jsonify(
{
"translatedText": results
}
)
results.append(unescape(translated_text))
if source_lang == "auto":
return jsonify(
{
"translatedText": results,
"detectedLanguage": source_langs
}
)
else:
return jsonify(
{
"translatedText": results
}
)
else:
translator = src_langs[0].get_translation(tgt_lang)
if translator is None:
abort(400, description="%s (%s) is not available as a target language from %s (%s)" % (tgt_lang.name, tgt_lang.code, src_langs[0].name, src_langs[0].code))
if text_format == "html":
translated_text = str(translate_html(translator, transliterate(q, target_lang=source_langs[0])))
translated_text = str(translate_html(translator, q))
else:
translated_text = translator.translate(transliterate(q, target_lang=source_langs[0]))
return jsonify(
{
"translatedText": translated_text
}
)
translated_text = improve_translation_formatting(q, translator.translate(q))
if source_lang == "auto":
return jsonify(
{
"translatedText": unescape(translated_text),
"detectedLanguage": source_langs[0]
}
)
else:
return jsonify(
{
"translatedText": unescape(translated_text)
}
)
except Exception as e:
abort(500, description="Cannot translate text: %s" % str(e))
@app.route("/detect", methods=["POST"])
@app.post("/translate_file")
@access_check
def translate_file():
"""
Translate file from a language to another
---
tags:
- translate
consumes:
- multipart/form-data
parameters:
- in: formData
name: file
type: file
required: true
description: File to translate
- in: formData
name: source
schema:
type: string
example: en
required: true
description: Source language code
- in: formData
name: target
schema:
type: string
example: es
required: true
description: Target language code
- in: formData
name: api_key
schema:
type: string
example: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
required: false
description: API key
responses:
200:
description: Translated file
schema:
id: translate-file
type: object
properties:
translatedFileUrl:
type: string
description: Translated file url
400:
description: Invalid request
schema:
id: error-response
type: object
properties:
error:
type: string
description: Error message
500:
description: Translation error
schema:
id: error-response
type: object
properties:
error:
type: string
description: Error message
429:
description: Slow down
schema:
id: error-slow-down
type: object
properties:
error:
type: string
description: Reason for slow down
403:
description: Banned
schema:
id: error-response
type: object
properties:
error:
type: string
description: Error message
"""
if args.disable_files_translation:
abort(403, description="Files translation are disabled on this server.")
source_lang = request.form.get("source")
target_lang = request.form.get("target")
file = request.files['file']
if not file:
abort(400, description="Invalid request: missing file parameter")
if not source_lang:
abort(400, description="Invalid request: missing source parameter")
if not target_lang:
abort(400, description="Invalid request: missing target parameter")
if file.filename == '':
abort(400, description="Invalid request: empty file")
if os.path.splitext(file.filename)[1] not in frontend_argos_supported_files_format:
abort(400, description="Invalid request: file format not supported")
source_langs = [source_lang]
src_langs = [next(iter([l for l in languages if l.code == source_lang]), None) for source_lang in source_langs]
for idx, lang in enumerate(src_langs):
if lang is None:
abort(400, description="%s is not supported" % source_langs[idx])
tgt_lang = next(iter([l for l in languages if l.code == target_lang]), None)
if tgt_lang is None:
abort(400, description="%s is not supported" % target_lang)
try:
filename = str(uuid.uuid4()) + '.' + secure_filename(file.filename)
filepath = os.path.join(get_upload_dir(), filename)
file.save(filepath)
translated_file_path = argostranslatefiles.translate_file(src_langs[0].get_translation(tgt_lang), filepath)
translated_filename = os.path.basename(translated_file_path)
return jsonify(
{
"translatedFileUrl": url_for('download_file', filename=translated_filename, _external=True)
}
)
except Exception as e:
abort(500, description=e)
@app.get("/download_file/<string:filename>")
def download_file(filename: str):
"""
Download a translated file
"""
if args.disable_files_translation:
abort(400, description="Files translation are disabled on this server.")
filepath = os.path.join(get_upload_dir(), filename)
try:
checked_filepath = security.path_traversal_check(filepath, get_upload_dir())
if os.path.isfile(checked_filepath):
filepath = checked_filepath
except security.SuspiciousFileOperation:
abort(400, description="Invalid filename")
return_data = io.BytesIO()
with open(filepath, 'rb') as fo:
return_data.write(fo.read())
return_data.seek(0)
download_filename = filename.split('.')
download_filename.pop(0)
download_filename = '.'.join(download_filename)
return send_file(return_data, as_attachment=True, download_name=download_filename)
@app.post("/detect")
@access_check
def detect():
"""
@ -565,6 +808,20 @@ def create_app(args):
frontendTimeout:
type: integer
description: Frontend translation timeout
apiKeys:
type: boolean
description: Whether the API key database is enabled.
keyRequired:
type: boolean
description: Whether an API key is required.
suggestions:
type: boolean
description: Whether submitting suggestions is enabled.
supportedFilesFormat:
type: array
items:
type: string
description: Supported files format
language:
type: object
properties:
@ -591,6 +848,11 @@ def create_app(args):
{
"charLimit": args.char_limit,
"frontendTimeout": args.frontend_timeout,
"apiKeys": args.api_keys,
"keyRequired": bool(args.api_keys and args.require_api_key_origin),
"suggestions": args.suggestions,
"filesTranslation": not args.disable_files_translation,
"supportedFilesFormat": [] if args.disable_files_translation else frontend_argos_supported_files_format,
"language": {
"source": {
"code": frontend_argos_language_source.code,
@ -604,8 +866,85 @@ def create_app(args):
}
)
@app.post("/suggest")
@access_check
def suggest():
"""
Submit a suggestion to improve a translation
---
tags:
- feedback
parameters:
- in: formData
name: q
schema:
type: string
example: Hello world!
required: true
description: Original text
- in: formData
name: s
schema:
type: string
example: ¡Hola mundo!
required: true
description: Suggested translation
- in: formData
name: source
schema:
type: string
example: en
required: true
description: Language of original text
- in: formData
name: target
schema:
type: string
example: es
required: true
description: Language of suggested translation
responses:
200:
description: Success
schema:
id: suggest-response
type: object
properties:
success:
type: boolean
description: Whether submission was successful
403:
description: Not authorized
schema:
id: error-response
type: object
properties:
error:
type: string
description: Error message
"""
if not args.suggestions:
abort(403, description="Suggestions are disabled on this server.")
q = request.values.get("q")
s = request.values.get("s")
source_lang = request.values.get("source")
target_lang = request.values.get("target")
if not q:
abort(400, description="Invalid request: missing q parameter")
if not s:
abort(400, description="Invalid request: missing s parameter")
if not source_lang:
abort(400, description="Invalid request: missing source parameter")
if not target_lang:
abort(400, description="Invalid request: missing target parameter")
SuggestionsDatabase().add(q, s, source_lang, target_lang)
return jsonify({"success": True})
swag = swagger(app)
swag["info"]["version"] = "1.2"
swag["info"]["version"] = "1.3.1"
swag["info"]["title"] = "LibreTranslate"
@app.route("/spec")

View file

@ -2,16 +2,19 @@ import os
_prefix = 'LT_'
def _get_value_str(name, default_value):
env_value = os.environ.get(name)
return default_value if env_value is None else env_value
def _get_value_int(name, default_value):
try:
return int(os.environ[name])
except:
return default_value
def _get_value_bool(name, default_value):
env_value = os.environ.get(name)
if env_value in ['FALSE', 'False', 'false', '0']:
@ -19,7 +22,8 @@ def _get_value_bool(name, default_value):
if env_value in ['TRUE', 'True', 'true', '1']:
return True
return default_value
def _get_value(name, default_value, value_type):
env_name = _prefix + name
if value_type == 'str':
@ -30,6 +34,7 @@ def _get_value(name, default_value, value_type):
return _get_value_bool(env_name, default_value)
return default_value
_default_options_objects = [
{
'name': 'HOST',
@ -51,6 +56,11 @@ _default_options_objects = [
'default_value': -1,
'value_type': 'int'
},
{
'name': 'REQ_LIMIT_STORAGE',
'default_value': 'memory://',
'value_type': 'str'
},
{
'name': 'DAILY_REQ_LIMIT',
'default_value': -1,
@ -75,7 +85,7 @@ _default_options_objects = [
'name': 'DEBUG',
'default_value': False,
'value_type': 'bool'
},
},
{
'name': 'SSL',
'default_value': None,
@ -101,6 +111,21 @@ _default_options_objects = [
'default_value': False,
'value_type': 'bool'
},
{
'name': 'API_KEYS_DB_PATH',
'default_value': 'db/api_keys.db',
'value_type': 'str'
},
{
'name': 'API_KEYS_REMOTE',
'default_value': '',
'value_type': 'str'
},
{
'name': 'GET_API_KEY_LINK',
'default_value': '',
'value_type': 'str'
},
{
'name': 'REQUIRE_API_KEY_ORIGIN',
'default_value': '',
@ -110,8 +135,33 @@ _default_options_objects = [
'name': 'LOAD_ONLY',
'default_value': None,
'value_type': 'str'
}
},
{
'name': 'THREADS',
'default_value': 4,
'value_type': 'int'
},
{
'name': 'SUGGESTIONS',
'default_value': False,
'value_type': 'bool'
},
{
'name': 'DISABLE_FILES_TRANSLATION',
'default_value': False,
'value_type': 'bool'
},
{
'name': 'DISABLE_WEB_UI',
'default_value': False,
'value_type': 'bool'
},
{
'name': 'UPDATE_MODELS',
'default_value': False,
'value_type': 'bool'
},
]
DEFAULT_ARGUMENTS = { obj['name']:_get_value(**obj) for obj in _default_options_objects}
DEFAULT_ARGUMENTS = {obj['name']: _get_value(**obj) for obj in _default_options_objects}

72
app/detect.py Normal file
View file

@ -0,0 +1,72 @@
# Originally adapted from https://github.com/aboSamoor/polyglot/blob/master/polyglot/base.py
import pycld2 as cld2
class UnknownLanguage(Exception):
pass
class Language(object):
def __init__(self, choice):
name, code, confidence, bytesize = choice
self.code = code
self.name = name
self.confidence = float(confidence)
self.read_bytes = int(bytesize)
def __str__(self):
return ("name: {:<12}code: {:<9}confidence: {:>5.1f} "
"read bytes:{:>6}".format(self.name, self.code,
self.confidence, self.read_bytes))
@staticmethod
def from_code(code):
return Language(("", code, 100, 0))
class Detector(object):
""" Detect the language used in a snippet of text."""
def __init__(self, text, quiet=False):
""" Detector of the language used in `text`.
Args:
text (string): unicode string.
"""
self.__text = text
self.reliable = True
"""False if the detector used Best Effort strategy in detection."""
self.quiet = quiet
"""If true, exceptions will be silenced."""
self.detect(text)
@staticmethod
def supported_languages():
"""Returns a list of the languages that can be detected by pycld2."""
return [name.capitalize() for name,code in cld2.LANGUAGES if not name.startswith("X_")]
def detect(self, text):
"""Decide which language is used to write the text.
The method tries first to detect the language with high reliability. If
that is not possible, the method switches to best effort strategy.
Args:
text (string): A snippet of text, the longer it is the more reliable we
can detect the language used to write the text.
"""
reliable, index, top_3_choices = cld2.detect(text, bestEffort=False)
if not reliable:
self.reliable = False
reliable, index, top_3_choices = cld2.detect(text, bestEffort=True)
if not self.quiet:
if not reliable:
raise UnknownLanguage("Try passing a longer snippet of text")
self.languages = [Language(x) for x in top_3_choices]
self.language = self.languages[0]
return self.language
def __str__(self):
text = "Prediction is reliable: {}\n".format(self.reliable)
text += u"\n".join(["Language {}: {}".format(i+1, str(l))
for i,l in enumerate(self.languages)])
return text

View file

@ -7,9 +7,20 @@ active = False
threshold = -1
def clear_banned():
def forgive_banned():
global banned
banned = {}
global threshold
clear_list = []
for ip in banned:
if banned[ip] <= 0:
clear_list.append(ip)
else:
banned[ip] = min(threshold, banned[ip]) - 1
for ip in clear_list:
del banned[ip]
def setup(violations_threshold=100):
@ -20,7 +31,7 @@ def setup(violations_threshold=100):
threshold = violations_threshold
scheduler = BackgroundScheduler()
scheduler.add_job(func=clear_banned, trigger="interval", weeks=4)
scheduler.add_job(func=forgive_banned, trigger="interval", minutes=30)
scheduler.start()
# Shut down the scheduler when exiting the app
@ -33,6 +44,15 @@ def report(request_ip):
banned[request_ip] += 1
def decrease(request_ip):
if banned[request_ip] > 0:
banned[request_ip] -= 1
def has_violation(request_ip):
return request_ip in banned and banned[request_ip] > 0
def is_banned(request_ip):
# More than X offences?
return active and banned.get(request_ip, 0) >= threshold

View file

@ -1,15 +1,13 @@
from pathlib import Path
import polyglot
from argostranslate import package, translate
import app.language
def boot(load_only=None):
def boot(load_only=None, update_models=False):
try:
check_and_install_models(load_only_lang_codes=load_only)
check_and_install_transliteration()
check_and_install_models(force=update_models, load_only_lang_codes=load_only)
except Exception as e:
print("Cannot update models (normal if you're offline): %s" % str(e))
@ -21,7 +19,7 @@ def check_and_install_models(force=False, load_only_lang_codes=None):
package.update_package_index()
# Load available packages from local package index
available_packages = package.load_available_packages()
available_packages = package.get_available_packages()
print("Found %s models" % len(available_packages))
if load_only_lang_codes is not None:
@ -55,40 +53,8 @@ def check_and_install_models(force=False, load_only_lang_codes=None):
package.install_from_path(download_path)
# reload installed languages
app.language.languages = translate.load_installed_languages()
app.language.languages = translate.get_installed_languages()
print(
"Loaded support for %s languages (%s models total)!"
% (len(translate.load_installed_languages()), len(available_packages))
)
def check_and_install_transliteration(force=False):
# 'en' is not a supported transliteration language
transliteration_languages = [
l.code for l in app.language.languages if l.code != "en"
]
# check installed
install_needed = []
if not force:
t_packages_path = Path(polyglot.polyglot_path) / "transliteration2"
for lang in transliteration_languages:
if not (
t_packages_path / lang / f"transliteration.{lang}.tar.bz2"
).exists():
install_needed.append(lang)
else:
install_needed = transliteration_languages
# install the needed transliteration packages
if install_needed:
print(
f"Installing transliteration models for the following languages: {', '.join(install_needed)}"
)
from polyglot.downloader import Downloader
downloader = Downloader()
for lang in install_needed:
downloader.download(f"transliteration2.{lang}")
% (len(translate.get_installed_languages()), len(available_packages))
)

View file

@ -1,14 +1,17 @@
import string
from argostranslate import translate
from polyglot.detect.base import Detector, UnknownLanguage
from polyglot.transliteration.base import Transliterator
from app.detect import Detector, UnknownLanguage
languages = translate.load_installed_languages()
__languages = None
def load_languages():
global __languages
__lang_codes = [l.code for l in languages]
if __languages is None or len(__languages) == 0:
__languages = translate.get_installed_languages()
return __languages
def detect_languages(text):
# detect batch processing
@ -32,9 +35,13 @@ def detect_languages(text):
# total read bytes of the provided text
text_length_total = sum(c.text_length for c in candidates)
# Load language codes
languages = load_languages()
lang_codes = [l.code for l in languages]
# only use candidates that are supported by argostranslate
candidate_langs = list(
filter(lambda l: l.text_length != 0 and l.code in __lang_codes, candidates)
filter(lambda l: l.text_length != 0 and l.code in lang_codes, candidates)
)
# this happens if no language could be detected
@ -46,7 +53,7 @@ def detect_languages(text):
# calculate the average confidence for each language
if is_batch:
temp_average_list = []
for lang_code in __lang_codes:
for lang_code in lang_codes:
# get all candidates for a specific language
lc = list(filter(lambda l: l.code == lang_code, candidate_langs))
if len(lc) > 1:
@ -71,49 +78,40 @@ def detect_languages(text):
return [{"confidence": l.confidence, "language": l.code} for l in candidate_langs]
def __transliterate_line(transliterator, line_text):
new_text = []
def improve_translation_formatting(source, translation, improve_punctuation=True):
source = source.strip()
# transliteration is done word by word
for orig_word in line_text.split(" "):
# remove any punctuation on the right side
r_word = orig_word.rstrip(string.punctuation)
r_diff = set(char for char in orig_word) - set(char for char in r_word)
# and on the left side
l_word = orig_word.lstrip(string.punctuation)
l_diff = set(char for char in orig_word) - set(char for char in l_word)
if not len(source):
return ""
if not len(translation):
return source
if improve_punctuation:
source_last_char = source[len(source) - 1]
translation_last_char = translation[len(translation) - 1]
# the actual transliteration of the word
t_word = transliterator.transliterate(orig_word.strip(string.punctuation))
punctuation_chars = ['!', '?', '.', ',', ';']
if source_last_char in punctuation_chars:
if translation_last_char != source_last_char:
if translation_last_char in punctuation_chars:
translation = translation[:-1]
# if transliteration fails, default back to the original word
if not t_word:
t_word = orig_word
else:
# add back any stripped punctuation
if r_diff:
t_word = t_word + "".join(r_diff)
if l_diff:
t_word = "".join(l_diff) + t_word
translation += source_last_char
elif translation_last_char in punctuation_chars:
translation = translation[:-1]
new_text.append(t_word)
if source.islower():
return translation.lower()
# rebuild the text
return " ".join(new_text)
if source.isupper():
return translation.upper()
if source[0].islower():
return translation[0].lower() + translation[1:]
def transliterate(text, target_lang="en"):
# initialize the transliterator from polyglot
transliterator = Transliterator(target_lang=target_lang)
if source[0].isupper():
return translation[0].upper() + translation[1:]
# check for multiline string
if "\n" in text:
lines = []
# process each line separate
for line in text.split("\n"):
lines.append(__transliterate_line(transliterator, line))
return translation
# rejoin multiline string
return "\n".join(lines)
else:
return __transliterate_line(transliterator, text)

View file

@ -1,12 +1,12 @@
import argparse
import sys
import operator
import sys
from app.app import create_app
from app.default_values import DEFAULT_ARGUMENTS as DEFARGS
def main():
def get_args():
parser = argparse.ArgumentParser(
description="LibreTranslate - Free and Open Source Translation API"
)
@ -28,6 +28,13 @@ def main():
metavar="<number>",
help="Set the default maximum number of requests per minute per client (%(default)s)",
)
parser.add_argument(
"--req-limit-storage",
default=DEFARGS['REQ_LIMIT_STORAGE'],
type=str,
metavar="<Storage URI>",
help="Storage URI to use for request limit data storage. See https://flask-limiter.readthedocs.io/en/stable/configuration.html. (%(default)s)",
)
parser.add_argument(
"--daily-req-limit",
default=DEFARGS['DAILY_REQ_LIMIT'],
@ -40,7 +47,7 @@ def main():
default=DEFARGS['REQ_FLOOD_THRESHOLD'],
type=int,
metavar="<number>",
help="Set the maximum number of request limit offences per 4 weeks that a client can exceed before being banned. (%(default)s)",
help="Set the maximum number of request limit offences that a client can exceed before being banned. (%(default)s)",
)
parser.add_argument(
"--batch-limit",
@ -89,6 +96,24 @@ def main():
action="store_true",
help="Enable API keys database for per-user rate limits lookup",
)
parser.add_argument(
"--api-keys-db-path",
default=DEFARGS['API_KEYS_DB_PATH'],
type=str,
help="Use a specific path inside the container for the local database. Can be absolute or relative (%(default)s)",
)
parser.add_argument(
"--api-keys-remote",
default=DEFARGS['API_KEYS_REMOTE'],
type=str,
help="Use this remote endpoint to query for valid API keys instead of using the local database",
)
parser.add_argument(
"--get-api-key-link",
default=DEFARGS['GET_API_KEY_LINK'],
type=str,
help="Show a link in the UI where to direct users to get an API key",
)
parser.add_argument(
"--require-api-key-origin",
type=str,
@ -102,8 +127,31 @@ def main():
metavar="<comma-separated language codes>",
help="Set available languages (ar,de,en,es,fr,ga,hi,it,ja,ko,pt,ru,zh)",
)
parser.add_argument(
"--threads",
default=DEFARGS['THREADS'],
type=int,
metavar="<number of threads>",
help="Set number of threads (%(default)s)",
)
parser.add_argument(
"--suggestions", default=DEFARGS['SUGGESTIONS'], action="store_true", help="Allow user suggestions"
)
parser.add_argument(
"--disable-files-translation", default=DEFARGS['DISABLE_FILES_TRANSLATION'], action="store_true",
help="Disable files translation"
)
parser.add_argument(
"--disable-web-ui", default=DEFARGS['DISABLE_WEB_UI'], action="store_true", help="Disable web ui"
)
parser.add_argument(
"--update-models", default=DEFARGS['UPDATE_MODELS'], action="store_true", help="Update language models at startup"
)
return parser.parse_args()
args = parser.parse_args()
def main():
args = get_args()
app = create_app(args)
if sys.argv[0] == '--wsgi':
@ -114,11 +162,15 @@ def main():
else:
from waitress import serve
url_scheme = "https" if args.ssl else "http"
print("Running on %s://%s:%s" % (url_scheme, args.host, args.port))
serve(
app,
host=args.host,
port=args.port,
url_scheme="https" if args.ssl else "http",
url_scheme=url_scheme,
threads=args.threads
)

View file

@ -1,6 +1,8 @@
import argparse
import os
from app.api_keys import Database
from app.default_values import DEFAULT_ARGUMENTS as DEFARGS
def manage():
@ -10,6 +12,12 @@ def manage():
)
keys_parser = subparsers.add_parser("keys", help="Manage API keys database")
keys_parser.add_argument(
"--api-keys-db-path",
default=DEFARGS['API_KEYS_DB_PATH'],
type=str,
help="Use a specific path inside the container for the local database",
)
keys_subparser = keys_parser.add_subparsers(
help="", dest="sub_command", title="Command List"
)
@ -30,7 +38,10 @@ def manage():
args = parser.parse_args()
if args.command == "keys":
db = Database()
if not os.path.exists(args.api_keys_db_path):
print("No such database: %s" % args.api_keys_db_path)
exit(1)
db = Database(args.api_keys_db_path)
if args.sub_command is None:
# Print keys
keys = db.all()

View file

@ -0,0 +1,26 @@
import atexit
import os
import time
from datetime import datetime
from apscheduler.schedulers.background import BackgroundScheduler
def remove_translated_files(upload_dir: str):
now = time.mktime(datetime.now().timetuple())
for f in os.listdir(upload_dir):
f = os.path.join(upload_dir, f)
if os.path.isfile(f):
f_time = os.path.getmtime(f)
if (now - f_time) > 1800: # 30 minutes
os.remove(f)
def setup(upload_dir):
scheduler = BackgroundScheduler(daemon=True)
scheduler.add_job(remove_translated_files, "interval", minutes=30, kwargs={'upload_dir': upload_dir})
scheduler.start()
# Shut down the scheduler when exiting the app
atexit.register(lambda: scheduler.shutdown())

16
app/security.py Normal file
View file

@ -0,0 +1,16 @@
import os
class SuspiciousFileOperation(Exception):
pass
def path_traversal_check(unsafe_path, known_safe_path):
known_safe_path = os.path.abspath(known_safe_path)
unsafe_path = os.path.abspath(unsafe_path)
if (os.path.commonprefix([known_safe_path, unsafe_path]) != known_safe_path):
raise SuspiciousFileOperation("{} is not safe".format(unsafe_path))
# Passes the check
return unsafe_path

View file

@ -0,0 +1,122 @@
@media (prefers-color-scheme: dark) {
.white {
background-color: #111 !important;
color: #fff;
}
.blue.darken-3 {
background-color: #1E5DA6 !important;
}
/* like in btn-delete-text */
.btn-flat {
color: #666;
}
.btn-switch-type {
background-color: #333;
color: #5CA8FF;
}
.btn-switch-type:hover {
background-color: #444 !important;
color: #5CA8FF;
}
.btn-switch-type.active {
background-color: #3392FF !important;
color: #fff;
}
.btn-switch-type.active:hover {
background-color: #5CA8FF !important;
color: #fff;
}
.btn-switch-language {
color: #fff;
}
.language-select:after {
border: solid #fff;
border-width: 0 2px 2px 0;
}
/* like in textarea */
.card-content {
border: 1px solid #444 !important;
background-color: #222 !important;
color: #fff;
}
.file-dropzone {
background: #222;
border: 1px solid #444;
margin-top: 1rem;
}
select {
color: #fff;
background: #111;
}
option {
color: #fff;
background: #222;
}
textarea {
border: 1px solid #444 !important;
background-color: #222 !important;
color: #fff;
}
/* like in file dropzone */
.textarea-container {
margin-top: 1rem;
}
.code {
border: 1px solid #444;
background: #222;
color: #fff;
}
code[class*="language-"], pre[class*="language-"] {
color: #fff;
text-shadow: 0 1px #000;
}
.token.atrule,
.token.attr-value,
.token.keyword {
color: #40b5e8;
}
.language-css .token.string,
.style .token.string,
.token.entity,
.token.operator,
.token.url {
color: #eecfab;
background: hsla(0,0%,15%,.5);
}
.token.attr-name,
.token.builtin,
.token.char,
.token.inserted,
.token.selector,
.token.string {
color: #acd25f;
}
.token.boolean,
.token.constant,
.token.deleted,
.token.number,
.token.property,
.token.symbol,
.token.tag {
color: #ff8bcc;
}
.token.class-name, .token.function {
color: #ff7994;
}
}

View file

@ -7,6 +7,10 @@ select {
font-family: Arial, Helvetica, sans-serif !important;
}
a {
text-decoration: underline;
}
#app {
min-height: 80vh;
}
@ -23,6 +27,10 @@ h3.header {
margin-top: 0 !important;
}
.mb-1 {
margin-bottom: 1rem;
}
.position-relative {
position: relative;
}
@ -52,13 +60,6 @@ h3.header {
outline: none;
position: relative;
}
@-moz-document url-prefix() {
.language-select select {
-moz-appearance: none;
text-indent: -2px;
margin-right: -8px;
}
}
.language-select:after {
content: "";
@ -92,11 +93,11 @@ h3.header {
background: none;
padding: 0;
cursor: pointer;
color: #777;
color: #666;
}
.btn-delete-text:focus,
.btn-copy-translated:focus {
.btn-action:focus {
background: none !important;
}
@ -104,32 +105,96 @@ h3.header {
position: absolute;
right: 2rem;
bottom: 1rem;
color: #777;
color: #666;
pointer-events: none;
}
.btn-copy-translated {
.actions {
position: absolute;
right: 2.75rem;
right: 1.25rem;
bottom: 1rem;
display: flex;
}
.btn-switch-type {
background-color: #fff;
color: #1565C0;
display: flex;
align-items: center;
color: #777;
margin: .5rem;
}
.btn-switch-type:focus {
background-color: inherit;
}
.btn-switch-type:hover {
background-color: #eee !important;
color: #1565C0;
}
.btn-switch-type.active {
background-color: #1565C0 !important;
color: #fff;
}
.file-dropzone {
font-size: 1.1rem;
border: 1px solid #ccc;
background: #f3f3f3;
padding: 1rem 2rem 1rem 1.5rem;
min-height: 220px;
position: relative;
}
.dropzone-content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.btn-action {
display: flex;
align-items: center;
color: #666;
font-size: 0.85rem;
background: none;
border: none;
cursor: pointer;
margin-right: -1.5rem;
}
.btn-copy-translated span {
.btn-blue {
color: #1565C0;
}
.btn-action:disabled {
color: #666;
}
.btn-action span {
padding-right: 0.5rem;
}
.btn-copy-translated .material-icons {
.btn-action .material-icons {
font-size: 1.35rem;
}
#translation-type-btns {
display: flex;
flex-wrap: wrap;
justify-content: center;
margin: -.5rem;
}
.btn-text {
display: none;
margin-left: 1em;
}
#translation-form {
padding-top: 1em;
}
.progress {
background-color: #f3f3f3;
}
@ -187,6 +252,39 @@ h3.header {
height: 32px;
}
.brand-logo {
text-decoration: none;
}
.sidenav-trigger {
background-color: transparent;
border: none;
color: white;
}
@media (min-width: 993px) {
nav button.sidenav-trigger {
display: none;
}
}
#download-btn-wrapper {
display: flex;
justify-content: center;
margin: 2em 0;
}
#download-btn {
display: flex;
align-items: center;
}
@media (min-width: 280px) {
.btn-text {
display: inline;
}
}
@media (max-width: 760px) {
.language-select select {
text-align: center;

487
app/static/js/app.js Normal file
View file

@ -0,0 +1,487 @@
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-3.0
// API host/endpoint
var BaseUrl = window.location.protocol + "//" + window.location.host;
var htmlRegex = /<(.*)>.*?|<(.*)\/>/;
document.addEventListener('DOMContentLoaded', function(){
var sidenavElems = document.querySelectorAll('.sidenav');
var sidenavInstances = M.Sidenav.init(sidenavElems);
var app = new Vue({
el: '#app',
delimiters: ['[[',']]'],
data: {
BaseUrl: BaseUrl,
loading: true,
error: "",
langs: [],
settings: {},
sourceLang: "",
targetLang: "",
loadingTranslation: false,
inputText: "",
inputTextareaHeight: 250,
savedTanslatedText: "",
translatedText: "",
output: "",
charactersLimit: -1,
detectedLangText: "",
copyTextLabel: "Copy text",
suggestions: false,
isSuggesting: false,
supportedFilesFormat : [],
translationType: "text",
inputFile: false,
loadingFileTranslation: false,
translatedFileUrl: false,
filesTranslation: true,
frontendTimeout: 500
},
mounted: function() {
const self = this;
const settingsRequest = new XMLHttpRequest();
settingsRequest.open("GET", BaseUrl + "/frontend/settings", true);
const langsRequest = new XMLHttpRequest();
langsRequest.open("GET", BaseUrl + "/languages", true);
settingsRequest.onload = function() {
if (this.status >= 200 && this.status < 400) {
self.settings = JSON.parse(this.response);
self.sourceLang = self.settings.language.source.code;
self.targetLang = self.settings.language.target.code;
self.charactersLimit = self.settings.charLimit;
self.suggestions = self.settings.suggestions;
self.supportedFilesFormat = self.settings.supportedFilesFormat;
self.filesTranslation = self.settings.filesTranslation;
self.frontendTimeout = self.settings.frontendTimeout;
if (langsRequest.response) {
handleLangsResponse(self, langsRequest);
} else {
langsRequest.onload = function() {
handleLangsResponse(self, this);
}
}
} else {
self.error = "Cannot load /frontend/settings";
self.loading = false;
}
};
settingsRequest.onerror = function() {
self.error = "Error while calling /frontend/settings";
self.loading = false;
};
langsRequest.onerror = function() {
self.error = "Error while calling /languages";
self.loading = false;
};
settingsRequest.send();
langsRequest.send();
},
updated: function(){
if (this.isSuggesting) return;
M.FormSelect.init(this.$refs.sourceLangDropdown);
M.FormSelect.init(this.$refs.targetLangDropdown);
if (this.$refs.inputTextarea){
this.$refs.inputTextarea.focus()
if (this.inputText === ""){
this.$refs.inputTextarea.style.height = this.inputTextareaHeight + "px";
this.$refs.translatedTextarea.style.height = this.inputTextareaHeight + "px";
} else{
this.$refs.inputTextarea.style.height = this.$refs.translatedTextarea.style.height = "1px";
this.$refs.inputTextarea.style.height = Math.max(this.inputTextareaHeight, this.$refs.inputTextarea.scrollHeight + 32) + "px";
this.$refs.translatedTextarea.style.height = Math.max(this.inputTextareaHeight, this.$refs.translatedTextarea.scrollHeight + 32) + "px";
}
}
if (this.charactersLimit !== -1 && this.inputText.length >= this.charactersLimit){
this.inputText = this.inputText.substring(0, this.charactersLimit);
}
// Update "selected" attribute (to overcome a vue.js limitation)
// but properly display checkmarks on supported browsers.
// Also change the <select> width value depending on the <option> length
if (this.$refs.sourceLangDropdown) {
updateSelectedAttribute(this.$refs.sourceLangDropdown, this.sourceLang);
}
if (this.$refs.targetLangDropdown) {
updateSelectedAttribute(this.$refs.targetLangDropdown, this.targetLang);
}
},
computed: {
requestCode: function(){
return ['const res = await fetch("' + this.BaseUrl + '/translate", {',
' method: "POST",',
' body: JSON.stringify({',
' q: ' + this.$options.filters.escape(this.inputText) + ',',
' source: ' + this.$options.filters.escape(this.sourceLang) + ',',
' target: ' + this.$options.filters.escape(this.targetLang) + ',',
' format: "' + (this.isHtml ? "html" : "text") + '",',
' api_key: "' + (localStorage.getItem("api_key") || "") + '"',
' }),',
' headers: { "Content-Type": "application/json" }',
'});',
'',
'console.log(await res.json());'].join("\n");
},
supportedFilesFormatFormatted: function() {
return this.supportedFilesFormat.join(', ');
},
isHtml: function(){
return htmlRegex.test(this.inputText);
},
canSendSuggestion: function(){
return this.translatedText.trim() !== "" && this.translatedText !== this.savedTanslatedText;
},
targetLangs: function(){
if (!this.sourceLang) return this.langs;
else{
var lang = this.langs.find(l => l.code === this.sourceLang);
if (!lang) return this.langs;
return lang.targets.map(t => this.langs.find(l => l.code === t));
}
}
},
filters: {
escape: function(v){
return JSON.stringify(v);
},
highlight: function(v){
return Prism.highlight(v, Prism.languages.javascript, 'javascript');
}
},
methods: {
abortPreviousTransRequest: function(){
if (this.transRequest){
this.transRequest.abort();
this.transRequest = null;
}
},
swapLangs: function(e){
this.closeSuggestTranslation(e);
// Make sure that we can swap
// by checking that the current target language
// has source language as target
var tgtLang = this.langs.find(l => l.code === this.targetLang);
if (tgtLang.targets.indexOf(this.sourceLang) === -1) return; // Not supported
var t = this.sourceLang;
this.sourceLang = this.targetLang;
this.targetLang = t;
this.inputText = this.translatedText;
this.translatedText = "";
this.handleInput(e);
},
dismissError: function(){
this.error = '';
},
getQueryParam: function (key) {
const params = new URLSearchParams(window.location.search);
return params.get(key)
},
updateQueryParam: function (key, value) {
let searchParams = new URLSearchParams(window.location.search)
searchParams.set(key, value);
let newRelativePathQuery = window.location.pathname + '?' + searchParams.toString();
history.pushState(null, '', newRelativePathQuery);
},
handleInput: function(e){
this.closeSuggestTranslation(e)
this.updateQueryParam('source', this.sourceLang)
this.updateQueryParam('target', this.targetLang)
this.updateQueryParam('q', encodeURI(this.inputText))
if (this.timeout) clearTimeout(this.timeout);
this.timeout = null;
this.detectedLangText = "";
if (this.inputText === ""){
this.translatedText = "";
this.output = "";
this.abortPreviousTransRequest();
this.loadingTranslation = false;
return;
}
var self = this;
self.loadingTranslation = true;
this.timeout = setTimeout(function(){
self.abortPreviousTransRequest();
var request = new XMLHttpRequest();
self.transRequest = request;
var data = new FormData();
data.append("q", self.inputText);
data.append("source", self.sourceLang);
data.append("target", self.targetLang);
data.append("format", self.isHtml ? "html" : "text");
data.append("api_key", localStorage.getItem("api_key") || "");
request.open('POST', BaseUrl + '/translate', true);
request.onload = function() {
try{
var res = JSON.parse(this.response);
// Success!
if (res.translatedText !== undefined){
self.translatedText = res.translatedText;
self.loadingTranslation = false;
self.output = JSON.stringify(res, null, 4);
if(self.sourceLang == "auto" && res.detectedLanguage !== undefined){
let lang = self.langs.find(l => l.code === res.detectedLanguage.language)
self.detectedLangText = ": " + (lang !== undefined ? lang.name : res.detectedLanguage.language) + " (" + res.detectedLanguage.confidence + "%)";
}
} else{
throw new Error(res.error || "Unknown error");
}
} catch (e) {
self.error = e.message;
self.loadingTranslation = false;
}
};
request.onerror = function() {
self.error = "Error while calling /translate";
self.loadingTranslation = false;
};
request.send(data);
}, self.frontendTimeout);
},
copyText: function(e){
e.preventDefault();
this.$refs.translatedTextarea.select();
this.$refs.translatedTextarea.setSelectionRange(0, 9999999); /* For mobile devices */
document.execCommand("copy");
if (this.copyTextLabel === "Copy text"){
this.copyTextLabel = "Copied";
var self = this;
setTimeout(function(){
self.copyTextLabel = "Copy text";
}, 1500);
}
},
suggestTranslation: function(e) {
e.preventDefault();
this.savedTanslatedText = this.translatedText
this.isSuggesting = true;
this.$nextTick(() => {
this.$refs.translatedTextarea.focus();
});
},
closeSuggestTranslation: function(e) {
if(this.isSuggesting) {
e.preventDefault();
// this.translatedText = this.savedTanslatedText
}
this.isSuggesting = false;
},
sendSuggestion: function(e) {
e.preventDefault();
var self = this;
var request = new XMLHttpRequest();
self.transRequest = request;
var data = new FormData();
data.append("q", self.inputText);
data.append("s", self.translatedText);
data.append("source", self.sourceLang);
data.append("target", self.targetLang);
data.append("api_key", localStorage.getItem("api_key") || "");
request.open('POST', BaseUrl + '/suggest', true);
request.onload = function() {
try{
var res = JSON.parse(this.response);
if (res.success){
M.toast({html: 'Thanks for your correction. Note the suggestion will not take effect right away.'})
self.closeSuggestTranslation(e)
}else{
throw new Error(res.error || "Unknown error");
}
}catch(e){
self.error = e.message;
self.closeSuggestTranslation(e)
}
};
request.onerror = function() {
self.error = "Error while calling /suggest";
self.loadingTranslation = false;
};
request.send(data);
},
deleteText: function(e){
e.preventDefault();
this.inputText = this.translatedText = this.output = "";
this.$refs.inputTextarea.focus();
},
switchType: function(type) {
this.translationType = type;
},
handleInputFile: function(e) {
this.inputFile = e.target.files[0];
},
removeFile: function(e) {
e.preventDefault()
this.inputFile = false;
this.translatedFileUrl = false;
this.loadingFileTranslation = false;
},
translateFile: function(e) {
e.preventDefault();
let self = this;
let translateFileRequest = new XMLHttpRequest();
translateFileRequest.open("POST", BaseUrl + "/translate_file", true);
let data = new FormData();
data.append("file", this.inputFile);
data.append("source", this.sourceLang);
data.append("target", this.targetLang);
data.append("api_key", localStorage.getItem("api_key") || "");
this.loadingFileTranslation = true
translateFileRequest.onload = function() {
if (translateFileRequest.readyState === 4 && translateFileRequest.status === 200) {
try{
self.loadingFileTranslation = false;
let res = JSON.parse(this.response);
if (res.translatedFileUrl){
self.translatedFileUrl = res.translatedFileUrl;
let link = document.createElement("a");
link.target = "_blank";
link.href = self.translatedFileUrl;
link.click();
}else{
throw new Error(res.error || "Unknown error");
}
}catch(e){
self.error = e.message;
self.loadingFileTranslation = false;
self.inputFile = false;
}
}else{
let res = JSON.parse(this.response);
self.error = res.error || "Unknown error";
self.loadingFileTranslation = false;
self.inputFile = false;
}
}
translateFileRequest.onerror = function() {
self.error = "Error while calling /translate_file";
self.loadingFileTranslation = false;
self.inputFile = false;
};
translateFileRequest.send(data);
}
}
});
});
/**
* @param {object} self
* @param {XMLHttpRequest} response
*/
function handleLangsResponse(self, response) {
if (response.status >= 200 && response.status < 400) {
self.langs = JSON.parse(response.response);
if (self.langs.length === 0){
self.loading = false;
self.error = "No languages available. Did you install the models correctly?"
return;
}
self.langs.push({ name: "Auto Detect", code: "auto", targets: self.langs.map(l => l.code)})
const sourceLanguage = self.langs.find(l => l.code === self.getQueryParam("source"))
const targetLanguage = self.langs.find(l => l.code === self.getQueryParam("target"))
if (sourceLanguage) {
self.sourceLang = sourceLanguage.code
}
if (targetLanguage) {
self.targetLang = targetLanguage.code
}
const defaultText = self.getQueryParam("q")
if (defaultText) {
self.inputText = decodeURI(defaultText)
self.handleInput(new Event('none'))
}
} else {
self.error = "Cannot load /languages";
}
self.loading = false;
}
/**
* @param {object} langDropdown
* @param {string} lang
*/
function updateSelectedAttribute(langDropdown, lang) {
for (const child of langDropdown.children) {
if (child.value === lang){
child.setAttribute('selected', '');
langDropdown.style.width = getTextWidth(child.text) + 24 + 'px';
} else{
child.removeAttribute('selected');
}
}
}
function getTextWidth(text) {
var canvas = getTextWidth.canvas || (getTextWidth.canvas = document.createElement("canvas"));
var ctx = canvas.getContext("2d");
ctx.font = 'bold 16px sans-serif';
var textWidth = Math.ceil(ctx.measureText(text).width);
return textWidth;
}
function setApiKey(){
var prevKey = localStorage.getItem("api_key") || "";
var newKey = "";
var instructions = "contact the server operator.";
if (window.getApiKeyLink) instructions = "press the \"Get API Key\" link."
newKey = window.prompt("Type in your API Key. If you need an API key, " + instructions, prevKey);
if (newKey === null) newKey = "";
localStorage.setItem("api_key", newKey);
}
// @license-end

File diff suppressed because one or more lines are too long

39
app/suggestions.py Normal file
View file

@ -0,0 +1,39 @@
import sqlite3
import os
from expiringdict import ExpiringDict
DEFAULT_DB_PATH = "db/suggestions.db"
class Database:
def __init__(self, db_path=DEFAULT_DB_PATH, max_cache_len=1000, max_cache_age=30):
# Legacy check - this can be removed at some point in the near future
if os.path.isfile("suggestions.db") and not os.path.isfile("db/suggestions.db"):
print("Migrating %s to %s" % ("suggestions.db", "db/suggestions.db"))
try:
os.rename("suggestions.db", "db/suggestions.db")
except Exception as e:
print(str(e))
self.db_path = db_path
self.cache = ExpiringDict(max_len=max_cache_len, max_age_seconds=max_cache_age)
# Make sure to do data synchronization on writes!
self.c = sqlite3.connect(db_path, check_same_thread=False)
self.c.execute(
"""CREATE TABLE IF NOT EXISTS suggestions (
"q" TEXT NOT NULL,
"s" TEXT NOT NULL,
"source" TEXT NOT NULL,
"target" TEXT NOT NULL
);"""
)
def add(self, q, s, source, target):
self.c.execute(
"INSERT INTO suggestions (q, s, source, target) VALUES (?, ?, ?, ?)",
(q, s, source, target),
)
self.c.commit()
return True

View file

@ -5,43 +5,64 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LibreTranslate - Free and Open Source Machine Translation API</title>
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
<meta name="description" content="Free and Open Source Machine Translation API. 100% self-hosted, no limits, no ties to proprietary services. Run your own API server in just a few minutes.">
<meta name="keywords" content="translation,api">
<meta name="description" content="Free and Open Source Machine Translation API. 100% self-hosted, offline capable and easy to setup. Run your own API server in just a few minutes.">
<meta name="keywords" content="translation,api">
<meta property="og:title" content="LibreTranslate - Free and Open Source Machine Translation API" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://libretranslate.com" />
<meta property="og:image" content="https://user-images.githubusercontent.com/1951843/102724116-32a6df00-42db-11eb-8cc0-129ab39cdfb5.png" />
<meta property="og:description" name="description" class="swiftype" content="Free and Open Source Machine Translation API. 100% self-hosted, no limits, no ties to proprietary services. Run your own API server in just a few minutes."/>
<link rel="preload" href="{{ url_for('static', filename='icon.svg') }}" as="image" />
<link rel="preload" href="{{ url_for('static', filename='js/vue@2.js') }}" as="script">
<link rel="preload" href="{{ url_for('static', filename='js/materialize.min.js') }}" as="script">
<link rel="preload" href="{{ url_for('static', filename='js/prism.min.js') }}" as="script">
<link rel="preload" href="{{ url_for('static', filename='js/app.js') }}?v={{ version }}" as="script">
<link rel="preload" href="{{ url_for('static', filename='css/materialize.min.css') }}" as="style"/>
<link rel="preload" href="{{ url_for('static', filename='css/material-icons.css') }}" as="style"/>
<link rel="preload" href="{{ url_for('static', filename='css/prism.min.css') }}" as="style"/>
<link rel="preload" href="{{ url_for('static', filename='css/main.css') }}?v={{ version }}" as="style"/>
<link rel="preload" href="{{ url_for('static', filename='css/dark-theme.css') }}" as="style"/>
<meta property="og:title" content="LibreTranslate - Free and Open Source Machine Translation API" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://libretranslate.com" />
<meta property="og:image" content="https://user-images.githubusercontent.com/1951843/102724116-32a6df00-42db-11eb-8cc0-129ab39cdfb5.png" />
<meta property="og:description" name="description" class="swiftype" content="Free and Open Source Machine Translation API. 100% self-hosted, no limits, no ties to proprietary services. Run your own API server in just a few minutes."/>
<script src="{{ url_for('static', filename='js/vue@2.js') }}"></script>
<!-- Compiled and minified CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/materialize.min.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/material-icons.css') }}" />
<link rel="stylesheet" href="{{ url_for('static', filename='css/prism.min.css') }}" />
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}?t=2" />
{% if gaId %}
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id={{ gaId }}"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '{{ gaId }}');
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '{{ gaId }}');
</script>
{% endif %}
<!-- Compiled and minified CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/materialize.min.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/material-icons.css') }}" />
<link rel="stylesheet" href="{{ url_for('static', filename='css/prism.min.css') }}" />
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}?v={{ version }}" />
<link rel="stylesheet" href="{{ url_for('static', filename='css/dark-theme.css') }}" />
</head>
<body>
<body class="white">
<header>
<nav class="blue lighten-1" role="navigation">
<nav class="blue darken-3" role="navigation">
<div class="nav-wrapper container">
<button data-target="nav-mobile" class="sidenav-trigger"><i class="material-icons">menu</i></button>
<a id="logo-container" href="/" class="brand-logo">
<img src="{{ url_for('static', filename='icon.svg') }}" class="logo"> LibreTranslate
<img src="{{ url_for('static', filename='icon.svg') }}" alt="Logo for LibreTranslate" class="logo">
<span>LibreTranslate</span>
</a>
<ul class="right hide-on-med-and-down">
<li><a href="/docs">API Docs</a></li>
{% if get_api_key_link %}
<li><a href="{{ get_api_key_link }}">Get API Key</a></li>
<script>window.getApiKeyLink = "{{ get_api_key_link }}";</script>
{% endif %}
<li><a href="https://github.com/LibreTranslate/LibreTranslate" rel="noopener noreferrer">GitHub</a></li>
{% if api_keys %}
<li><a href="javascript:setApiKey()" title="Set API Key"><i class="material-icons">vpn_key</i></a></li>
@ -50,12 +71,14 @@
<ul id="nav-mobile" class="sidenav">
<li><a href="/docs">API Docs</a></li>
{% if get_api_key_link %}
<li><a href="{{ get_api_key_link }}">Get API Key</a></li>
{% endif %}
<li><a href="https://github.com/LibreTranslate/LibreTranslate" rel="noopener noreferrer">GitHub</a></li>
{% if api_keys %}
<li><a href="javascript:setApiKey()" title="Set API Key"><i class="material-icons">vpn_key</i></a></li>
{% endif %}
</ul>
<a href="#" data-target="nav-mobile" class="sidenav-trigger"><i class="material-icons">menu</i></a>
</div>
</nav>
</header>
@ -105,11 +128,21 @@
<div class="container">
<div class="row">
<h3 class="header center">Translation API</h3>
<form class="col s12">
<div id="translation-type-btns" class="s12 center" v-if="filesTranslation === true">
<button type="button" class="btn btn-switch-type" @click="switchType('text')" :class="{'active': translationType === 'text'}">
<i class="material-icons">title</i>
<span class="btn-text">Translate Text</span>
</button>
<button type="button" class="btn btn-switch-type" @click="switchType('files')" :class="{'active': translationType === 'files'}">
<i class="material-icons">description</i>
<span class="btn-text">Translate Files</span>
</button>
</div>
<form id="translation-form" class="col s12">
<div class="row mb-0">
<div class="col s6 language-select">
<span>Translate from</span>
<span v-if="detectedLangText !== ''">[[ detectedLangText ]]</span>
<select class="browser-default" v-model="sourceLang" ref="sourceLangDropdown" @change="handleInput">
<template v-for="option in langs">
<option :value="option.code">[[ option.name ]]</option>
@ -123,49 +156,95 @@
</a>
<span>Translate into</span>
<select class="browser-default" v-model="targetLang" ref="targetLangDropdown" @change="handleInput">
<template v-for="option in langs">
<template v-for="option in targetLangs">
<option v-if="option.code !== 'auto'" :value="option.code">[[ option.name ]]</option>
</template>
</select>
</select>
</div>
</div>
<div class="row">
<div class="row" v-if="translationType === 'text'">
<div class="input-field textarea-container col s6">
<label for="textarea1" class="sr-only">
Text to translate
</label>
</label>
<textarea id="textarea1" v-model="inputText" @input="handleInput" ref="inputTextarea" dir="auto"></textarea>
<button class="btn-delete-text" title="Delete text" @click="deleteText">
<i class="material-icons">close</i>
</button>
</button>
<div class="characters-limit-container" v-if="charactersLimit !== -1">
<label>[[ inputText.length ]] / [[ charactersLimit ]]</label>
</div>
</div>
<div class="input-field textarea-container col s6">
<label for="textarea2" class="sr-only">
Translated text
</label>
<textarea id="textarea2" v-model="translatedText" ref="translatedTextarea" dir="auto" readonly></textarea>
<button class="btn-copy-translated" @click="copyText">
<span>[[ copyTextLabel ]]</span> <i class="material-icons">content_copy</i>
</button>
<div class="position-relative">
</label>
<textarea id="textarea2" v-model="translatedText" ref="translatedTextarea" dir="auto" v-bind:readonly="suggestions && !isSuggesting"></textarea>
<div class="actions">
<button v-if="suggestions && !loadingTranslation && inputText.length && !isSuggesting" class="btn-action" @click="suggestTranslation">
<i class="material-icons">edit</i>
</button>
<button v-if="suggestions && !loadingTranslation && inputText.length && isSuggesting" class="btn-action btn-blue" @click="closeSuggestTranslation">
<span>Cancel</span>
</button>
<button v-if="suggestions && !loadingTranslation && inputText.length && isSuggesting" :disabled="!canSendSuggestion" class="btn-action btn-blue" @click="sendSuggestion">
<span>Send</span>
</button>
<button v-if="!isSuggesting" class="btn-action btn-copy-translated" @click="copyText">
<span>[[ copyTextLabel ]]</span> <i class="material-icons">content_copy</i>
</button>
</div>
<div class="position-relative">
<div class="progress translate" v-if="loadingTranslation">
<div class="indeterminate"></div>
</div>
</div>
</div>
</div>
</div>
<div class="row" v-if="translationType === 'files'">
<div class="file-dropzone">
<div v-if="inputFile === false" class="dropzone-content">
<span>Supported file formats: [[ supportedFilesFormatFormatted ]]</span>
<form action="#">
<div class="file-field input-field">
<div class="btn">
<span>File</span>
<input type="file" :accept="supportedFilesFormatFormatted" @change="handleInputFile" ref="fileInputRef">
</div>
<div class="file-path-wrapper hidden">
<input class="file-path validate" type="text">
</div>
</div>
</form>
</div>
<div v-if="inputFile !== false" class="dropzone-content">
<div class="card">
<div class="card-content">
<div class="row mb-0">
<div class="col s12">
[[ inputFile.name ]]
<button v-if="loadingFileTranslation !== true" @click="removeFile" class="btn-flat">
<i class="material-icons">close</i>
</button>
</div>
</div>
</div>
</div>
<button @click="translateFile" v-if="translatedFileUrl === false && loadingFileTranslation === false" class="btn">Translate</button>
<a v-if="translatedFileUrl !== false" :href="translatedFileUrl" class="btn">Download</a>
<div class="progress" v-if="loadingFileTranslation">
<div class="indeterminate"></div>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
<div class="section no-pad-bot">
<div class="section no-pad-bot" v-if="translationType !== 'files'">
<div class="container">
<div class="row center">
<div class="col s12 m12">
@ -192,10 +271,14 @@
<div class="container">
<div class="row center">
<div class="col s12 m12">
<h3 class="header">Open Source Machine Translation</h3>
<h4 class="header">100% Self-Hosted. No Limits. No Ties to Proprietary Services.</h4>
<br/><a class="waves-effect waves-light btn btn-large" href="https://github.com/LibreTranslate/LibreTranslate" rel="noopener noreferrer"><i class="material-icons left">cloud_download</i> Download</a>
<br/><br/><br/>
<h3 class="header">Open Source Machine Translation API</h3>
<h4 class="header">100% Self-Hosted. Offline Capable. Easy to Setup.</h4>
<div id="download-btn-wrapper">
<a id="download-btn" class="waves-effect waves-light btn btn-large teal darken-2" href="https://github.com/LibreTranslate/LibreTranslate" rel="noopener noreferrer">
<i class="material-icons">cloud_download</i>
<span class="btn-text">Download</span>
</a>
</div>
</div>
</div>
</div>
@ -210,19 +293,19 @@
<div class="col l12 s12">
<h5 class="white-text">LibreTranslate</h5>
<p class="grey-text text-lighten-4">Free and Open Source Machine Translation API</p>
<p><a class="grey-text text-lighten-4" href="https://www.gnu.org/licenses/agpl-3.0.en.html" rel="noopener noreferrer">License: AGPLv3</a></p>
<p>License: <a class="grey-text text-lighten-4" href="https://www.gnu.org/licenses/agpl-3.0.en.html" rel="noopener noreferrer">AGPLv3</a></p>
<p><a class="grey-text text-lighten-4" href="/javascript-licenses" rel="jslicense">JavaScript license information</a></p>
{% if web_version %}
<p>
This public API should be used for testing, personal or infrequent use. If you're going to run an application in production, please <a href="https://github.com/LibreTranslate/LibreTranslate" class="grey-text text-lighten-4" style="text-decoration: underline;" rel="noopener noreferrer">host your own server</a> or <a class="grey-text text-lighten-4" href="https://github.com/LibreTranslate/LibreTranslate#mirrors" style="text-decoration: underline;" rel="noopener noreferrer">get an API key</a>.
This public API should be used for testing, personal or infrequent use. If you're going to run an application in production, please <a href="https://github.com/LibreTranslate/LibreTranslate" class="grey-text text-lighten-4" rel="noopener noreferrer">host your own server</a> or <a class="grey-text text-lighten-4" href="{{ get_api_key_link if get_api_key_link else 'https://github.com/LibreTranslate/LibreTranslate#mirrors' }}" rel="noopener noreferrer">get an API key</a>.
</p>
{% endif %}
</div>
</div>
</div>
<div class="footer-copyright center">
<p>
Made with ❤ by <a class="grey-text text-lighten-3" href="https://github.com/LibreTranslate/LibreTranslate/graphs/contributors" rel="noopener noreferrer">LibreTranslate Contributors</a> and powered by <a class="grey-text text-lighten-3" href="https://github.com/argosopentech/argos-translate/" rel="noopener noreferrer">Argos Translate</a>
<p class="white-text">
Made with ❤ by <a class="white-text" href="https://github.com/LibreTranslate/LibreTranslate/graphs/contributors" rel="noopener noreferrer">LibreTranslate Contributors</a> and powered by <a class="white-text text-lighten-3" href="https://github.com/argosopentech/argos-translate/" rel="noopener noreferrer">Argos Translate</a>
</p>
</div>
</footer>
@ -234,267 +317,7 @@
window.Prism.manual = true;
// @license-end
</script>
<script src="{{ url_for('static', filename='js/prism.min.js') }}"></script>
<script>
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-3.0
// API host/endpoint
var BaseUrl = window.location.protocol + "//" + window.location.host;
document.addEventListener('DOMContentLoaded', function(){
var sidenavElems = document.querySelectorAll('.sidenav');
var sidenavInstances = M.Sidenav.init(sidenavElems);
var app = new Vue({
el: '#app',
delimiters: ['[[',']]'],
data: {
BaseUrl: BaseUrl,
loading: true,
error: "",
langs: [],
settings: {},
sourceLang: "",
targetLang: "",
loadingTranslation: false,
inputText: "",
inputTextareaHeight: 250,
translatedText: "",
output: "",
charactersLimit: -1,
copyTextLabel: "Copy text"
},
mounted: function(){
var self = this;
var requestSettings = new XMLHttpRequest();
requestSettings.open('GET', BaseUrl + '/frontend/settings', true);
requestSettings.onload = function() {
if (this.status >= 200 && this.status < 400) {
// Success!
self.settings = JSON.parse(this.response);
self.sourceLang = self.settings.language.source.code;
self.targetLang = self.settings.language.target.code;
self.charactersLimit = self.settings.charLimit;
}else {
self.error = "Cannot load /frontend/settings";
self.loading = false;
}
};
requestSettings.onerror = function() {
self.error = "Error while calling /frontend/settings";
self.loading = false;
};
requestSettings.send();
var requestLanguages = new XMLHttpRequest();
requestLanguages.open('GET', BaseUrl + '/languages', true);
requestLanguages.onload = function() {
if (this.status >= 200 && this.status < 400) {
// Success!
self.langs = JSON.parse(this.response);
self.langs.push({ name: 'Auto Detect (Experimental)', code: 'auto' })
if (self.langs.length === 0){
self.loading = false;
self.error = "No languages available. Did you install the models correctly?"
return;
}
self.loading = false;
} else {
self.error = "Cannot load /languages";
self.loading = false;
}
};
requestLanguages.onerror = function() {
self.error = "Error while calling /languages";
self.loading = false;
};
requestLanguages.send();
},
updated: function(){
M.FormSelect.init(this.$refs.sourceLangDropdown);
M.FormSelect.init(this.$refs.targetLangDropdown);
if (this.inputText === ""){
this.$refs.inputTextarea.style.height = this.inputTextareaHeight + "px";
this.$refs.translatedTextarea.style.height = this.inputTextareaHeight + "px";
}else{
this.$refs.inputTextarea.style.height = this.$refs.translatedTextarea.style.height = "1px";
this.$refs.inputTextarea.style.height = Math.max(this.inputTextareaHeight, this.$refs.inputTextarea.scrollHeight + 32) + "px";
this.$refs.translatedTextarea.style.height = Math.max(this.inputTextareaHeight, this.$refs.translatedTextarea.scrollHeight + 32) + "px";
}
if (this.charactersLimit !== -1 && this.inputText.length >= this.charactersLimit){
this.inputText = this.inputText.substring(0, this.charactersLimit);
}
// Update "selected" attribute (to overcome a vue.js limitation)
// but properly display checkmarks on supported browsers.
// Also change the <select> width value depending on the <option> length
for (var i = 0; i < this.$refs.sourceLangDropdown.children.length; i++){
var el = this.$refs.sourceLangDropdown.children[i];
if (el.value === this.sourceLang){
el.setAttribute('selected', '');
this.$refs.sourceLangDropdown.style.width = getTextWidth(el.text) + 24 + 'px';
}else{
el.removeAttribute('selected');
}
}
for (var i = 0; i < this.$refs.targetLangDropdown.children.length; i++){
var el = this.$refs.targetLangDropdown.children[i];
if (el.value === this.targetLang){
el.setAttribute('selected', '');
this.$refs.targetLangDropdown.style.width = getTextWidth(el.text) + 24 + 'px';
}else{
el.removeAttribute('selected');
}
}
},
computed: {
requestCode: function(){
return ['const res = await fetch("' + this.BaseUrl + '/translate", {',
' method: "POST",',
' body: JSON.stringify({',
' q: "' + this.$options.filters.escape(this.inputText) + '",',
' source: "' + this.$options.filters.escape(this.sourceLang) + '",',
' target: "' + this.$options.filters.escape(this.targetLang) + '"',
' }),',
' headers: { "Content-Type": "application/json" }',
'});',
'',
'console.log(await res.json());'].join("\n");
}
},
filters: {
escape: function(v){
return v.replace('"', '\\\"');
},
highlight: function(v){
return Prism.highlight(v, Prism.languages.javascript, 'javascript');
}
},
methods: {
abortPreviousTransRequest: function(){
if (this.transRequest){
this.transRequest.abort();
this.transRequest = null;
}
},
swapLangs: function(){
var t = this.sourceLang;
this.sourceLang = this.targetLang;
this.targetLang = t;
this.inputText = this.translatedText;
this.translatedText = "";
this.handleInput();
},
dismissError: function(){
this.error = '';
},
handleInput: function(e){
if (this.timeout) clearTimeout(this.timeout);
this.timeout = null;
if (this.inputText === ""){
this.translatedText = "";
this.output = "";
this.abortPreviousTransRequest();
this.loadingTranslation = false;
return;
}
var self = this;
self.loadingTranslation = true;
this.timeout = setTimeout(function(){
self.abortPreviousTransRequest();
var request = new XMLHttpRequest();
self.transRequest = request;
var data = new FormData();
data.append("q", self.inputText);
data.append("source", self.sourceLang);
data.append("target", self.targetLang);
data.append("api_key", localStorage.getItem("api_key") || "");
request.open('POST', BaseUrl + '/translate', true);
request.onload = function() {
try{
var res = JSON.parse(this.response);
// Success!
if (res.translatedText !== undefined){
self.translatedText = res.translatedText;
self.loadingTranslation = false;
self.output = JSON.stringify(res, null, 4);
}else{
throw new Error(res.error || "Unknown error");
}
}catch(e){
self.error = e.message;
self.loadingTranslation = false;
}
};
request.onerror = function() {
self.error = "Error while calling /translate";
self.loadingTranslation = false;
};
request.send(data);
}, '{{ frontendTimeout }}');
},
copyText: function(e){
e.preventDefault();
this.$refs.translatedTextarea.select();
this.$refs.translatedTextarea.setSelectionRange(0, 9999999); /* For mobile devices */
document.execCommand("copy");
if (this.copyTextLabel === "Copy text"){
this.copyTextLabel = "Copied";
var self = this;
setTimeout(function(){
self.copyTextLabel = "Copy text";
}, 1500);
}
},
deleteText: function(e){
e.preventDefault();
this.inputText = this.translatedText = this.output = "";
}
}
});
});
function getTextWidth(text) {
var canvas = getTextWidth.canvas || (getTextWidth.canvas = document.createElement("canvas"));
var ctx = canvas.getContext("2d");
ctx.font = 'bold 16px sans-serif';
var textWidth = Math.ceil(ctx.measureText(text).width);
return textWidth;
}
function setApiKey(){
var prevKey = localStorage.getItem("api_key") || "";
var newKey = "";
newKey = window.prompt("Type in your API Key. If you need an API key, contact the server operator.", prevKey);
if (newKey === null) newKey = "";
localStorage.setItem("api_key", newKey);
}
// @license-end
</script>
<script src="{{ url_for('static', filename='js/prism.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/app.js') }}?v={{ version }}"></script>
</body>
</html>

0
db/.gitignore vendored Normal file
View file

18
docker-compose.cuda.yml Normal file
View file

@ -0,0 +1,18 @@
version: "3"
services:
libretranslate-cuda:
container_name: libretranslate-cuda
build:
context: .
dockerfile: docker/Dockerfile.cuda
restart: unless-stopped
ports:
- 5000:5000
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]

View file

@ -8,4 +8,12 @@ services:
ports:
- 5000:5000
## Uncomment above command and define your args if necessary
# command: --ssl --ga-id MY-GA-ID --req-limit 100 --char-limit 500
# command: --ssl --ga-id MY-GA-ID --req-limit 100 --char-limit 500
## Uncomment this section and the `volumes` section if you want to backup your API keys
# environment:
# - LT_API_KEYS_DB_PATH=/app/db/api_keys.db # Same result as `db/api_keys.db` or `./db/api_keys.db`
# volumes:
# - libretranslate_api_keys:/app/db/api_keys.db
# volumes:
# libretranslate_api_keys:

45
docker/Dockerfile.cuda Normal file
View file

@ -0,0 +1,45 @@
FROM nvidia/cuda:11.2.2-devel-ubuntu20.04
ENV ARGOS_DEVICE_TYPE cuda
ARG with_models=true
ARG models=
WORKDIR /app
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update -qq \
&& apt-get -qqq install --no-install-recommends -y libicu-dev libaspell-dev libcairo2 libcairo2-dev pkg-config gcc g++ python3.8-dev python3-pip libpython3.8-dev\
&& apt-get clean \
&& rm -rf /var/lib/apt
RUN apt-get update && apt-get upgrade --assume-yes
RUN pip3 install --upgrade pip && apt-get remove python3-pip --assume-yes
COPY . .
RUN ln -s /usr/bin/python3 /usr/bin/python
RUN pip3 install torch==1.12.0+cu116 -f https://download.pytorch.org/whl/torch_stable.html
RUN if [ "$with_models" = "true" ]; then \
# install only the dependencies first
pip3 install -e .; \
# initialize the language models
if [ ! -z "$models" ]; then \
./install_models.py --load_only_lang_codes "$models"; \
else \
./install_models.py; \
fi \
fi
# Install package from source code
RUN pip3 install . \
&& pip3 cache purge
# Depending on your cuda install you may need to uncomment this line to allow the container to access the cuda libraries
# See: https://docs.nvidia.com/cuda/cuda-installation-guide-linux/index.html#post-installation-actions
# ENV LD_LIBRARY_PATH=/usr/local/cuda/lib:/usr/local/cuda/lib64
EXPOSE 5000
ENTRYPOINT [ "libretranslate", "--host", "0.0.0.0" ]

View file

@ -1,7 +1,12 @@
#!/usr/bin/env python
from app.init import check_and_install_models, check_and_install_transliteration
import argparse
from app.init import check_and_install_models
if __name__ == "__main__":
check_and_install_models(force=True)
check_and_install_transliteration(force=True)
parser = argparse.ArgumentParser()
parser.add_argument("--load_only_lang_codes", type=str, default="")
args = parser.parse_args()
lang_codes = args.load_only_lang_codes.split(",")
if len(lang_codes) == 0 or lang_codes[0] == '':
lang_codes = None
check_and_install_models(force=True, load_only_lang_codes=lang_codes)

View file

@ -1,14 +1,17 @@
argostranslate==1.5.1
Flask==1.1.2
argostranslate==1.7.0
Flask==2.2.2
flask-swagger==0.2.14
flask-swagger-ui==3.36.0
Flask-Limiter==1.4
waitress==1.4.4
expiringdict==1.2.1
pyicu==2.7
pycld2==0.41
flask-swagger-ui==4.11.1
Flask-Limiter==2.6.3
waitress==2.1.2
expiringdict==1.2.2
LTpycld2==0.42
morfessor==2.0.6
polyglot==16.7.4
appdirs==1.4.4
APScheduler==3.7.0
translatehtml==1.5.1
APScheduler==3.9.1
translatehtml==1.5.2
argos-translate-files==1.1.0
itsdangerous==2.1.2
Werkzeug==2.2.2
requests==2.28.1
redis==4.3.4

41
run.bat Normal file
View file

@ -0,0 +1,41 @@
@ECHO OFF
SETLOCAL
SET LT_PORT=5000
:loop
IF NOT "%1"=="" (
IF "%1"=="--port" (
SET LT_PORT=%2
SHIFT
)
IF "%1"=="--help" (
echo Usage: run.bat [--port N]
echo:
echo Run LibreTranslate using docker.
echo:
GOTO :done
)
IF "%1"=="--api-keys" (
SET DB_VOLUME=-v lt-db:/app/db
SHIFT
)
SHIFT
GOTO :loop
)
WHERE /Q docker
IF %ERRORLEVEL% NEQ 0 GOTO :install_docker
docker run -ti --rm -p %LT_PORT%:%LT_PORT% %DB_VOLUME% -v lt-local:/home/libretranslate/.local libretranslate/libretranslate %*
GOTO :done
:install_docker
ECHO Cannot find docker! Go to https://docs.docker.com/desktop/install/windows-install/ and install docker before running this script (pressing Enter will open the page)
pause
start "" https://docs.docker.com/desktop/install/windows-install/
GOTO :done
:done

89
run.sh Executable file
View file

@ -0,0 +1,89 @@
#!/bin/bash
set -eo pipefail
__dirname=$(cd "$(dirname "$0")"; pwd -P)
cd "${__dirname}"
platform="Linux" # Assumed
uname=$(uname)
case $uname in
"Darwin")
platform="MacOS / OSX"
;;
MINGW*)
platform="Windows"
;;
esac
usage(){
echo "Usage: $0 [--port N]"
echo
echo "Run LibreTranslate using docker."
echo
exit
}
export LT_PORT=5000
# Parse args for overrides
ARGS=()
while [[ $# -gt 0 ]]
do
key="$1"
case $key in
--port)
export LT_PORT="$2"
ARGS+=("$1")
ARGS+=("$2") # save it in an array for later
shift # past argument
shift # past value
;;
--debug)
export LT_DEBUG=YES
ARGS+=("$1")
shift # past argument
;;
--api-keys)
export DB_VOLUME="-v lt-db:/app/db"
ARGS+=("$1")
shift # past argument
;;
--help)
usage
;;
*) # unknown option
ARGS+=("$1")
shift # past argument
;;
esac
done
# $1 = command | $2 = help_text | $3 = install_command (optional)
check_command(){
hash "$1" 2>/dev/null || not_found=true
if [[ $not_found ]]; then
check_msg_prefix="Checking for $1... "
# Can we attempt to install it?
if [[ -n "$3" ]]; then
echo -e "$check_msg_prefix \033[93mnot found, we'll attempt to install\033[39m"
$3 || sudo $3
# Recurse, but don't pass the install command
check_command "$1" "$2"
else
check_msg_result="\033[91m can't find $1! Check that the program is installed and that you have added the proper path to the program to your PATH environment variable before launching WebODM. If you change your PATH environment variable, remember to close and reopen your terminal. $2\033[39m"
fi
fi
echo -e "$check_msg_prefix $check_msg_result"
if [[ $not_found ]]; then
return 1
fi
}
environment_check(){
check_command "docker" "https://www.docker.com/"
}
environment_check
docker run -ti --rm -p $LT_PORT:$LT_PORT $DB_VOLUME -v lt-local:/home/libretranslate/.local libretranslate/libretranslate ${ARGS[@]}

View file

@ -3,7 +3,7 @@
from setuptools import setup, find_packages
setup(
version='1.2.4',
version=open('VERSION').read().strip(),
name='libretranslate',
license='GNU Affero General Public License v3.0',
description='Free and Open Source Machine Translation API. Self-hosted, no limits, no ties to proprietary services.',
@ -22,17 +22,18 @@ setup(
],
},
python_requires='>=3.6.0',
python_requires='>=3.7.0',
long_description=open('README.md').read(),
long_description_content_type="text/markdown",
install_requires=open("requirements.txt", "r").readlines(),
tests_require=['pytest==5.2.0'],
tests_require=['pytest==7.1.2'],
setup_requires=['pytest-runner'],
classifiers=[
"License :: OSI Approved :: GNU Affero General Public License v3 ",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8"
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10"
]
)

46
suggestions-to-jsonl.py Executable file
View file

@ -0,0 +1,46 @@
#!/usr/bin/env python
import argparse
import time
import sqlite3
import json
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Program to generate JSONL files from a LibreTranslate's suggestions.db")
parser.add_argument(
"--db",
type=str,
nargs=1,
help="Path to suggestions.db file",
default='suggestions.db'
)
parser.add_argument(
"--clear",
action='store_true',
help="Clear suggestions.db after generation",
default=False
)
args = parser.parse_args()
output_file = str(int(time.time())) + ".jsonl"
con = sqlite3.connect(args.db, check_same_thread=False)
cur = con.cursor()
with open(output_file, 'w', encoding="utf-8") as f:
for row in cur.execute('SELECT q, s, source, target FROM suggestions WHERE source != "auto" ORDER BY source'):
q, s, source, target = row
obj = {
'q': q,
's': s,
'source': source,
'target': target
}
json.dump(obj, f, ensure_ascii=False)
f.write('\n')
print("Wrote %s" % output_file)
if args.clear:
cur.execute("DELETE FROM suggestions")
con.commit()
print("Cleared " + args.db)

View file

View file

@ -0,0 +1,20 @@
import sys
import pytest
from app.app import create_app
from app.default_values import DEFAULT_ARGUMENTS
from app.main import get_args
@pytest.fixture()
def app():
sys.argv = ['']
DEFAULT_ARGUMENTS['LOAD_ONLY'] = "en,es"
app = create_app(get_args())
yield app
@pytest.fixture()
def client(app):
return app.test_client()

View file

@ -0,0 +1,26 @@
import json
def test_api_detect_language(client):
response = client.post("/detect", data={
"q": "Hello"
})
response_json = json.loads(response.data)
assert "confidence" in response_json[0] and "language" in response_json[0]
assert len(response_json) >= 1
assert response.status_code == 200
def test_api_detect_language_must_fail_without_parameters(client):
response = client.post("/detect")
response_json = json.loads(response.data)
assert "error" in response_json
assert response.status_code == 400
def test_api_detect_language_must_fail_bad_request_type(client):
response = client.get("/detect")
assert response.status_code == 405

View file

@ -0,0 +1,4 @@
def test_api_get_frontend_settings(client):
response = client.get("/frontend/settings")
assert response.status_code == 200

View file

@ -0,0 +1,16 @@
import json
def test_api_get_languages(client):
response = client.get("/languages")
response_json = json.loads(response.data)
assert "code" in response_json[0] and "name" in response_json[0]
assert len(response_json) >= 1
assert response.status_code == 200
def test_api_get_languages_must_fail_bad_request_type(client):
response = client.post("/languages")
assert response.status_code == 405

View file

@ -0,0 +1,10 @@
def test_api_get_spec(client):
response = client.get("/spec")
assert response.status_code == 200
def test_api_get_spec_must_fail_bad_request_type(client):
response = client.post("/spec")
assert response.status_code == 405

View file

@ -0,0 +1,61 @@
import json
def test_api_translate(client):
response = client.post("/translate", data={
"q": "Hello",
"source": "en",
"target": "es",
"format": "text"
})
response_json = json.loads(response.data)
assert "translatedText" in response_json
assert response.status_code == 200
def test_api_translate_batch(client):
response = client.post("/translate", json={
"q": ["Hello", "World"],
"source": "en",
"target": "es",
"format": "text"
})
response_json = json.loads(response.data)
assert "translatedText" in response_json
assert isinstance(response_json["translatedText"], list)
assert len(response_json["translatedText"]) == 2
assert response.status_code == 200
def test_api_translate_unsupported_language(client):
response = client.post("/translate", data={
"q": "Hello",
"source": "en",
"target": "zz",
"format": "text"
})
response_json = json.loads(response.data)
assert "error" in response_json
assert "zz is not supported" == response_json["error"]
assert response.status_code == 400
def test_api_translate_missing_parameter(client):
response = client.post("/translate", data={
"source": "en",
"target": "es",
"format": "text"
})
response_json = json.loads(response.data)
assert "error" in response_json
assert "Invalid request: missing q parameter" == response_json["error"]
assert response.status_code == 400

View file

@ -4,6 +4,6 @@ from argostranslate import package
def test_boot_argos():
"""Test Argos translate models initialization"""
boot()
boot(["en", "es"])
assert len(package.get_installed_packages()) > 2
assert len(package.get_installed_packages()) >= 2