mirror of
https://github.com/LibreTranslate/LibreTranslate.git
synced 2025-05-14 05:52:55 +00:00
app->libretranslate; mv tests/ inside libretranslate/
This commit is contained in:
parent
40a1141eac
commit
a23a9fbd75
47 changed files with 24 additions and 25 deletions
3
libretranslate/__init__.py
Normal file
3
libretranslate/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
import os
|
||||
from .main import main
|
||||
from .manage import manage
|
100
libretranslate/api_keys.py
Normal file
100
libretranslate/api_keys.py
Normal file
|
@ -0,0 +1,100 @@
|
|||
import os
|
||||
import sqlite3
|
||||
import uuid
|
||||
import requests
|
||||
from expiringdict import ExpiringDict
|
||||
from libretranslate.default_values import DEFAULT_ARGUMENTS as DEFARGS
|
||||
|
||||
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)
|
||||
|
||||
# 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 api_keys (
|
||||
"api_key" TEXT NOT NULL,
|
||||
"req_limit" INTEGER NOT NULL,
|
||||
PRIMARY KEY("api_key")
|
||||
);"""
|
||||
)
|
||||
|
||||
def lookup(self, api_key):
|
||||
req_limit = self.cache.get(api_key)
|
||||
if req_limit is None:
|
||||
# DB Lookup
|
||||
stmt = self.c.execute(
|
||||
"SELECT req_limit FROM api_keys WHERE api_key = ?", (api_key,)
|
||||
)
|
||||
row = stmt.fetchone()
|
||||
if row is not None:
|
||||
self.cache[api_key] = row[0]
|
||||
req_limit = row[0]
|
||||
else:
|
||||
self.cache[api_key] = False
|
||||
req_limit = False
|
||||
|
||||
if isinstance(req_limit, bool):
|
||||
req_limit = None
|
||||
|
||||
return req_limit
|
||||
|
||||
def add(self, req_limit, api_key="auto"):
|
||||
if api_key == "auto":
|
||||
api_key = str(uuid.uuid4())
|
||||
|
||||
self.remove(api_key)
|
||||
self.c.execute(
|
||||
"INSERT INTO api_keys (api_key, req_limit) VALUES (?, ?)",
|
||||
(api_key, req_limit),
|
||||
)
|
||||
self.c.commit()
|
||||
return (api_key, req_limit)
|
||||
|
||||
def remove(self, api_key):
|
||||
self.c.execute("DELETE FROM api_keys WHERE api_key = ?", (api_key,))
|
||||
self.c.commit()
|
||||
return api_key
|
||||
|
||||
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
|
1007
libretranslate/app.py
Normal file
1007
libretranslate/app.py
Normal file
File diff suppressed because it is too large
Load diff
177
libretranslate/default_values.py
Normal file
177
libretranslate/default_values.py
Normal file
|
@ -0,0 +1,177 @@
|
|||
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']:
|
||||
return False
|
||||
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':
|
||||
return _get_value_str(env_name, default_value)
|
||||
if value_type == 'int':
|
||||
return _get_value_int(env_name, default_value)
|
||||
if value_type == 'bool':
|
||||
return _get_value_bool(env_name, default_value)
|
||||
return default_value
|
||||
|
||||
|
||||
_default_options_objects = [
|
||||
{
|
||||
'name': 'HOST',
|
||||
'default_value': '127.0.0.1',
|
||||
'value_type': 'str'
|
||||
},
|
||||
{
|
||||
'name': 'PORT',
|
||||
'default_value': 5000,
|
||||
'value_type': 'int'
|
||||
},
|
||||
{
|
||||
'name': 'CHAR_LIMIT',
|
||||
'default_value': -1,
|
||||
'value_type': 'int'
|
||||
},
|
||||
{
|
||||
'name': 'REQ_LIMIT',
|
||||
'default_value': -1,
|
||||
'value_type': 'int'
|
||||
},
|
||||
{
|
||||
'name': 'REQ_LIMIT_STORAGE',
|
||||
'default_value': 'memory://',
|
||||
'value_type': 'str'
|
||||
},
|
||||
{
|
||||
'name': 'DAILY_REQ_LIMIT',
|
||||
'default_value': -1,
|
||||
'value_type': 'int'
|
||||
},
|
||||
{
|
||||
'name': 'REQ_FLOOD_THRESHOLD',
|
||||
'default_value': -1,
|
||||
'value_type': 'int'
|
||||
},
|
||||
{
|
||||
'name': 'BATCH_LIMIT',
|
||||
'default_value': -1,
|
||||
'value_type': 'int'
|
||||
},
|
||||
{
|
||||
'name': 'GA_ID',
|
||||
'default_value': None,
|
||||
'value_type': 'str'
|
||||
},
|
||||
{
|
||||
'name': 'DEBUG',
|
||||
'default_value': False,
|
||||
'value_type': 'bool'
|
||||
},
|
||||
{
|
||||
'name': 'SSL',
|
||||
'default_value': None,
|
||||
'value_type': 'bool'
|
||||
},
|
||||
{
|
||||
'name': 'FRONTEND_LANGUAGE_SOURCE',
|
||||
'default_value': 'en',
|
||||
'value_type': 'str'
|
||||
},
|
||||
{
|
||||
'name': 'FRONTEND_LANGUAGE_TARGET',
|
||||
'default_value': 'es',
|
||||
'value_type': 'str'
|
||||
},
|
||||
{
|
||||
'name': 'FRONTEND_TIMEOUT',
|
||||
'default_value': 500,
|
||||
'value_type': 'int'
|
||||
},
|
||||
{
|
||||
'name': 'API_KEYS',
|
||||
'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': '',
|
||||
'value_type': 'str'
|
||||
},
|
||||
{
|
||||
'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'
|
||||
},
|
||||
{
|
||||
'name': 'METRICS',
|
||||
'default_value': False,
|
||||
'value_type': 'bool'
|
||||
},
|
||||
{
|
||||
'name': 'METRICS_AUTH_TOKEN',
|
||||
'default_value': '',
|
||||
'value_type': 'str'
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
DEFAULT_ARGUMENTS = {obj['name']: _get_value(**obj) for obj in _default_options_objects}
|
72
libretranslate/detect.py
Normal file
72
libretranslate/detect.py
Normal 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
|
58
libretranslate/flood.py
Normal file
58
libretranslate/flood.py
Normal file
|
@ -0,0 +1,58 @@
|
|||
import atexit
|
||||
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
|
||||
banned = {}
|
||||
active = False
|
||||
threshold = -1
|
||||
|
||||
|
||||
def forgive_banned():
|
||||
global 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):
|
||||
global active
|
||||
global threshold
|
||||
|
||||
active = True
|
||||
threshold = violations_threshold
|
||||
|
||||
scheduler = BackgroundScheduler()
|
||||
scheduler.add_job(func=forgive_banned, trigger="interval", minutes=30)
|
||||
scheduler.start()
|
||||
|
||||
# Shut down the scheduler when exiting the app
|
||||
atexit.register(lambda: scheduler.shutdown())
|
||||
|
||||
|
||||
def report(request_ip):
|
||||
if active:
|
||||
banned[request_ip] = banned.get(request_ip, 0)
|
||||
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
|
60
libretranslate/init.py
Normal file
60
libretranslate/init.py
Normal file
|
@ -0,0 +1,60 @@
|
|||
from pathlib import Path
|
||||
|
||||
from argostranslate import package, translate
|
||||
|
||||
import libretranslate.language
|
||||
|
||||
|
||||
def boot(load_only=None, update_models=False):
|
||||
try:
|
||||
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))
|
||||
|
||||
|
||||
def check_and_install_models(force=False, load_only_lang_codes=None):
|
||||
if len(package.get_installed_packages()) < 2 or force:
|
||||
# Update package definitions from remote
|
||||
print("Updating language models")
|
||||
package.update_package_index()
|
||||
|
||||
# Load available packages from local package index
|
||||
available_packages = package.get_available_packages()
|
||||
print("Found %s models" % len(available_packages))
|
||||
|
||||
if load_only_lang_codes is not None:
|
||||
# load_only_lang_codes: List[str] (codes)
|
||||
# Ensure the user does not use any unavailable language code.
|
||||
unavailable_lang_codes = set(load_only_lang_codes)
|
||||
for pack in available_packages:
|
||||
unavailable_lang_codes -= {pack.from_code, pack.to_code}
|
||||
if unavailable_lang_codes:
|
||||
raise ValueError(
|
||||
"Unavailable language codes: %s."
|
||||
% ",".join(sorted(unavailable_lang_codes))
|
||||
)
|
||||
# Keep only the packages that have both from_code and to_code in our list.
|
||||
available_packages = [
|
||||
pack
|
||||
for pack in available_packages
|
||||
if pack.from_code in load_only_lang_codes and pack.to_code in load_only_lang_codes
|
||||
]
|
||||
if not available_packages:
|
||||
raise ValueError("no available package")
|
||||
print("Keep %s models" % len(available_packages))
|
||||
|
||||
# Download and install all available packages
|
||||
for available_package in available_packages:
|
||||
print(
|
||||
"Downloading %s (%s) ..."
|
||||
% (available_package, available_package.package_version)
|
||||
)
|
||||
download_path = available_package.download()
|
||||
package.install_from_path(download_path)
|
||||
|
||||
# reload installed languages
|
||||
app.language.languages = translate.get_installed_languages()
|
||||
print(
|
||||
"Loaded support for %s languages (%s models total)!"
|
||||
% (len(translate.get_installed_languages()), len(available_packages))
|
||||
)
|
117
libretranslate/language.py
Normal file
117
libretranslate/language.py
Normal file
|
@ -0,0 +1,117 @@
|
|||
import string
|
||||
|
||||
from argostranslate import translate
|
||||
from libretranslate.detect import Detector, UnknownLanguage
|
||||
|
||||
__languages = None
|
||||
|
||||
def load_languages():
|
||||
global __languages
|
||||
|
||||
if __languages is None or len(__languages) == 0:
|
||||
__languages = translate.get_installed_languages()
|
||||
|
||||
return __languages
|
||||
|
||||
def detect_languages(text):
|
||||
# detect batch processing
|
||||
if isinstance(text, list):
|
||||
is_batch = True
|
||||
else:
|
||||
is_batch = False
|
||||
text = [text]
|
||||
|
||||
# get the candidates
|
||||
candidates = []
|
||||
for t in text:
|
||||
try:
|
||||
d = Detector(t).languages
|
||||
for i in range(len(d)):
|
||||
d[i].text_length = len(t)
|
||||
candidates.extend(d)
|
||||
except UnknownLanguage:
|
||||
pass
|
||||
|
||||
# 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)
|
||||
)
|
||||
|
||||
# this happens if no language could be detected
|
||||
if not candidate_langs:
|
||||
# use language "en" by default but with zero confidence
|
||||
return [{"confidence": 0.0, "language": "en"}]
|
||||
|
||||
# for multiple occurrences of the same language (can happen on batch detection)
|
||||
# calculate the average confidence for each language
|
||||
if is_batch:
|
||||
temp_average_list = []
|
||||
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:
|
||||
# if more than one is present, calculate the average confidence
|
||||
lang = lc[0]
|
||||
lang.confidence = sum(l.confidence for l in lc) / len(lc)
|
||||
lang.text_length = sum(l.text_length for l in lc)
|
||||
temp_average_list.append(lang)
|
||||
elif lc:
|
||||
# otherwise just add it to the temporary list
|
||||
temp_average_list.append(lc[0])
|
||||
|
||||
if temp_average_list:
|
||||
# replace the list
|
||||
candidate_langs = temp_average_list
|
||||
|
||||
# sort the candidates descending based on the detected confidence
|
||||
candidate_langs.sort(
|
||||
key=lambda l: (l.confidence * l.text_length) / text_length_total, reverse=True
|
||||
)
|
||||
|
||||
return [{"confidence": l.confidence, "language": l.code} for l in candidate_langs]
|
||||
|
||||
|
||||
def improve_translation_formatting(source, translation, improve_punctuation=True):
|
||||
source = source.strip()
|
||||
|
||||
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]
|
||||
|
||||
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]
|
||||
|
||||
translation += source_last_char
|
||||
elif translation_last_char in punctuation_chars:
|
||||
translation = translation[:-1]
|
||||
|
||||
if source.islower():
|
||||
return translation.lower()
|
||||
|
||||
if source.isupper():
|
||||
return translation.upper()
|
||||
|
||||
if source[0].islower():
|
||||
return translation[0].lower() + translation[1:]
|
||||
|
||||
if source[0].isupper():
|
||||
return translation[0].upper() + translation[1:]
|
||||
|
||||
return translation
|
||||
|
190
libretranslate/main.py
Normal file
190
libretranslate/main.py
Normal file
|
@ -0,0 +1,190 @@
|
|||
import argparse
|
||||
import operator
|
||||
import sys
|
||||
|
||||
from libretranslate.app import create_app
|
||||
from libretranslate.default_values import DEFAULT_ARGUMENTS as DEFARGS
|
||||
|
||||
|
||||
def get_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="LibreTranslate - Free and Open Source Translation API"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--host", type=str, help="Hostname (%(default)s)", default=DEFARGS['HOST']
|
||||
)
|
||||
parser.add_argument("--port", type=int, help="Port (%(default)s)", default=DEFARGS['PORT'])
|
||||
parser.add_argument(
|
||||
"--char-limit",
|
||||
default=DEFARGS['CHAR_LIMIT'],
|
||||
type=int,
|
||||
metavar="<number of characters>",
|
||||
help="Set character limit (%(default)s)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--req-limit",
|
||||
default=DEFARGS['REQ_LIMIT'],
|
||||
type=int,
|
||||
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'],
|
||||
type=int,
|
||||
metavar="<number>",
|
||||
help="Set the default maximum number of requests per day per client, in addition to req-limit. (%(default)s)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--req-flood-threshold",
|
||||
default=DEFARGS['REQ_FLOOD_THRESHOLD'],
|
||||
type=int,
|
||||
metavar="<number>",
|
||||
help="Set the maximum number of request limit offences that a client can exceed before being banned. (%(default)s)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--batch-limit",
|
||||
default=DEFARGS['BATCH_LIMIT'],
|
||||
type=int,
|
||||
metavar="<number of texts>",
|
||||
help="Set maximum number of texts to translate in a batch request (%(default)s)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ga-id",
|
||||
type=str,
|
||||
default=DEFARGS['GA_ID'],
|
||||
metavar="<GA ID>",
|
||||
help="Enable Google Analytics on the API client page by providing an ID (%(default)s)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--debug", default=DEFARGS['DEBUG'], action="store_true", help="Enable debug environment"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ssl", default=DEFARGS['SSL'], action="store_true", help="Whether to enable SSL"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--frontend-language-source",
|
||||
type=str,
|
||||
default=DEFARGS['FRONTEND_LANGUAGE_SOURCE'],
|
||||
metavar="<language code>",
|
||||
help="Set frontend default language - source (%(default)s)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--frontend-language-target",
|
||||
type=str,
|
||||
default=DEFARGS['FRONTEND_LANGUAGE_TARGET'],
|
||||
metavar="<language code>",
|
||||
help="Set frontend default language - target (%(default)s)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--frontend-timeout",
|
||||
type=int,
|
||||
default=DEFARGS['FRONTEND_TIMEOUT'],
|
||||
metavar="<milliseconds>",
|
||||
help="Set frontend translation timeout (%(default)s)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--api-keys",
|
||||
default=DEFARGS['API_KEYS'],
|
||||
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,
|
||||
default=DEFARGS['REQUIRE_API_KEY_ORIGIN'],
|
||||
help="Require use of an API key for programmatic access to the API, unless the request origin matches this domain",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--load-only",
|
||||
type=operator.methodcaller("split", ","),
|
||||
default=DEFARGS['LOAD_ONLY'],
|
||||
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"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--metrics",
|
||||
default=DEFARGS['METRICS'],
|
||||
action="store_true",
|
||||
help="Enable the /metrics endpoint for exporting Prometheus usage metrics",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--metrics-auth-token",
|
||||
default=DEFARGS['METRICS_AUTH_TOKEN'],
|
||||
type=str,
|
||||
help="Protect the /metrics endpoint by allowing only clients that have a valid Authorization Bearer token (%(default)s)",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main():
|
||||
args = get_args()
|
||||
app = create_app(args)
|
||||
|
||||
if sys.argv[0] == '--wsgi':
|
||||
return app
|
||||
else:
|
||||
if args.debug:
|
||||
app.run(host=args.host, port=args.port)
|
||||
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=url_scheme,
|
||||
threads=args.threads
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
60
libretranslate/manage.py
Normal file
60
libretranslate/manage.py
Normal file
|
@ -0,0 +1,60 @@
|
|||
import argparse
|
||||
import os
|
||||
|
||||
from libretranslate.api_keys import Database
|
||||
from libretranslate.default_values import DEFAULT_ARGUMENTS as DEFARGS
|
||||
|
||||
|
||||
def manage():
|
||||
parser = argparse.ArgumentParser(description="LibreTranslate Manage Tools")
|
||||
subparsers = parser.add_subparsers(
|
||||
help="", dest="command", required=True, title="Command List"
|
||||
)
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
keys_add_parser = keys_subparser.add_parser("add", help="Add API keys to database")
|
||||
keys_add_parser.add_argument(
|
||||
"req_limit", type=int, help="Request Limits (per second)"
|
||||
)
|
||||
keys_add_parser.add_argument(
|
||||
"--key", type=str, default="auto", required=False, help="API Key"
|
||||
)
|
||||
|
||||
keys_remove_parser = keys_subparser.add_parser(
|
||||
"remove", help="Remove API keys to database"
|
||||
)
|
||||
keys_remove_parser.add_argument("key", type=str, help="API Key")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == "keys":
|
||||
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()
|
||||
if not keys:
|
||||
print("There are no API keys")
|
||||
else:
|
||||
for item in keys:
|
||||
print("%s: %s" % item)
|
||||
|
||||
elif args.sub_command == "add":
|
||||
print(db.add(args.req_limit, args.key)[0])
|
||||
elif args.sub_command == "remove":
|
||||
print(db.remove(args.key))
|
||||
else:
|
||||
parser.print_help()
|
||||
exit(1)
|
10
libretranslate/no_limiter.py
Normal file
10
libretranslate/no_limiter.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
from functools import wraps
|
||||
|
||||
|
||||
class Limiter:
|
||||
def exempt(self, f):
|
||||
@wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return wrapper
|
26
libretranslate/remove_translated_files.py
Normal file
26
libretranslate/remove_translated_files.py
Normal 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
libretranslate/security.py
Normal file
16
libretranslate/security.py
Normal 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
|
122
libretranslate/static/css/dark-theme.css
Normal file
122
libretranslate/static/css/dark-theme.css
Normal 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;
|
||||
}
|
||||
}
|
300
libretranslate/static/css/main.css
Normal file
300
libretranslate/static/css/main.css
Normal file
|
@ -0,0 +1,300 @@
|
|||
/* Custom styles for LibreTranslate page */
|
||||
|
||||
html,
|
||||
body,
|
||||
select {
|
||||
font-size: 16px;
|
||||
font-family: Arial, Helvetica, sans-serif !important;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#app {
|
||||
min-height: 80vh;
|
||||
}
|
||||
|
||||
h3.header {
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.mb-0 {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.mt-0 {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.mb-1 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.position-relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.language-select {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.language-select select {
|
||||
border: none;
|
||||
width: auto;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
text-decoration: underline;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
text-indent: 0.01px;
|
||||
text-overflow: "";
|
||||
margin: 0;
|
||||
margin-left: 6px;
|
||||
height: 2rem;
|
||||
line-height: inherit;
|
||||
outline: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.language-select:after {
|
||||
content: "";
|
||||
width: 0.5em;
|
||||
height: 0.5em;
|
||||
margin: 0 0 0.25rem -0.75rem;
|
||||
border: solid black;
|
||||
border-width: 0 2px 2px 0;
|
||||
display: inline-block;
|
||||
padding: 3px;
|
||||
transform: rotate(45deg);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.btn-switch-language {
|
||||
color: black;
|
||||
margin-left: -1.5rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.textarea-container {
|
||||
margin-top: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.btn-delete-text {
|
||||
position: absolute;
|
||||
right: 1.5rem;
|
||||
top: 0.75rem;
|
||||
border: 0;
|
||||
background: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.btn-delete-text:focus,
|
||||
.btn-action:focus {
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
.characters-limit-container {
|
||||
position: absolute;
|
||||
right: 2rem;
|
||||
bottom: 1rem;
|
||||
color: #666;
|
||||
pointer-events: none;
|
||||
}
|
||||
.actions {
|
||||
position: absolute;
|
||||
right: 1.25rem;
|
||||
bottom: 1rem;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.btn-switch-type {
|
||||
background-color: #fff;
|
||||
color: #1565C0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
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;
|
||||
}
|
||||
|
||||
.btn-blue {
|
||||
color: #1565C0;
|
||||
}
|
||||
.btn-action:disabled {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.btn-action span {
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.progress.translate {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.progress .indeterminate {
|
||||
background-color: steelblue;
|
||||
}
|
||||
|
||||
.textarea-container textarea {
|
||||
font-size: 1.25rem;
|
||||
resize: none;
|
||||
border: 1px solid #ccc;
|
||||
background: #f3f3f3;
|
||||
padding: 1rem 2rem 1rem 1.5rem;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.code {
|
||||
font-size: 90%;
|
||||
padding: 1rem 1.5rem;
|
||||
border: 1px solid #ccc;
|
||||
background: #fbfbfb;
|
||||
overflow: auto;
|
||||
font-family: monospace;
|
||||
min-height: 280px;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.page-footer .footer-copyright {
|
||||
justify-content: center;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
#logo-container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.logo {
|
||||
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;
|
||||
margin: auto;
|
||||
padding: 0;
|
||||
}
|
||||
.language-select:after {
|
||||
content: none;
|
||||
}
|
||||
.language-select span {
|
||||
display: none;
|
||||
}
|
||||
}
|
36
libretranslate/static/css/material-icons.css
Normal file
36
libretranslate/static/css/material-icons.css
Normal file
|
@ -0,0 +1,36 @@
|
|||
@font-face {
|
||||
font-family: 'Material Icons';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('../fonts/MaterialIcons-Regular.eot'); /* For IE6-8 */
|
||||
src: local('Material Icons'),
|
||||
local('MaterialIcons-Regular'),
|
||||
url('../fonts/MaterialIcons-Regular.woff2') format('woff2'),
|
||||
url('../fonts/MaterialIcons-Regular.woff') format('woff'),
|
||||
url('../fonts/MaterialIcons-Regular.ttf') format('truetype');
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
font-family: 'Material Icons';
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-size: 24px; /* Preferred icon size */
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
word-wrap: normal;
|
||||
white-space: nowrap;
|
||||
direction: ltr;
|
||||
|
||||
/* Support for all WebKit browsers. */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
/* Support for Safari and Chrome. */
|
||||
text-rendering: optimizeLegibility;
|
||||
|
||||
/* Support for Firefox. */
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
/* Support for IE. */
|
||||
font-feature-settings: 'liga';
|
||||
}
|
13
libretranslate/static/css/materialize.min.css
vendored
Normal file
13
libretranslate/static/css/materialize.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
libretranslate/static/css/prism.min.css
vendored
Normal file
1
libretranslate/static/css/prism.min.css
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
code[class*=language-],pre[class*=language-]{color:#000;background:0 0;text-shadow:0 1px #fff;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{text-shadow:none;background:#b3d4fc}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{text-shadow:none;background:#b3d4fc}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#f5f2f0}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#708090}.token.punctuation{color:#999}.token.namespace{opacity:.7}.token.boolean,.token.constant,.token.deleted,.token.number,.token.property,.token.symbol,.token.tag{color:#905}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#690}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url{color:#9a6e3a;background:hsla(0,0%,100%,.5)}.token.atrule,.token.attr-value,.token.keyword{color:#07a}.token.class-name,.token.function{color:#dd4a68}.token.important,.token.regex,.token.variable{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}
|
BIN
libretranslate/static/favicon.ico
Normal file
BIN
libretranslate/static/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 262 KiB |
BIN
libretranslate/static/fonts/MaterialIcons-Regular.eot
Normal file
BIN
libretranslate/static/fonts/MaterialIcons-Regular.eot
Normal file
Binary file not shown.
BIN
libretranslate/static/fonts/MaterialIcons-Regular.ttf
Normal file
BIN
libretranslate/static/fonts/MaterialIcons-Regular.ttf
Normal file
Binary file not shown.
BIN
libretranslate/static/fonts/MaterialIcons-Regular.woff
Normal file
BIN
libretranslate/static/fonts/MaterialIcons-Regular.woff
Normal file
Binary file not shown.
BIN
libretranslate/static/fonts/MaterialIcons-Regular.woff2
Normal file
BIN
libretranslate/static/fonts/MaterialIcons-Regular.woff2
Normal file
Binary file not shown.
83
libretranslate/static/icon.svg
Normal file
83
libretranslate/static/icon.svg
Normal file
|
@ -0,0 +1,83 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="187.04305"
|
||||
height="188.81523"
|
||||
viewBox="0 0 49.488472 49.957363"
|
||||
version="1.1"
|
||||
id="svg8">
|
||||
<defs
|
||||
id="defs2">
|
||||
<rect
|
||||
x="25.162016"
|
||||
y="84.327377"
|
||||
width="71.115189"
|
||||
height="52.835255"
|
||||
id="rect835" />
|
||||
<rect
|
||||
x="25.162016"
|
||||
y="84.327377"
|
||||
width="71.115189"
|
||||
height="52.835255"
|
||||
id="rect835-7" />
|
||||
<rect
|
||||
x="25.162016"
|
||||
y="84.327377"
|
||||
width="71.115189"
|
||||
height="52.835255"
|
||||
id="rect874" />
|
||||
<rect
|
||||
x="25.162016"
|
||||
y="84.327377"
|
||||
width="71.115189"
|
||||
height="52.835255"
|
||||
id="rect923" />
|
||||
</defs>
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
id="layer1"
|
||||
transform="translate(-23.040803,-26.932047)">
|
||||
<g
|
||||
id="g861"
|
||||
transform="translate(-42.114518,-17.993737)"
|
||||
style="fill:#ffffff">
|
||||
<g
|
||||
aria-label="众"
|
||||
transform="matrix(4.3205134,0,0,4.3205134,-37.271798,-327.6536)"
|
||||
id="text833"
|
||||
style="font-style:normal;font-weight:normal;font-size:10.5833px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect835);fill:#ffffff;fill-opacity:1;stroke:none">
|
||||
<path
|
||||
d="m 34.996103,90.121035 -0.614947,0.759641 q -2.754346,-1.41593 -3.948067,-2.950715 -1.167884,1.617467 -3.7672,2.888703 L 26.0096,90.084861 q 3.457142,-1.601964 4.283963,-3.849882 l 0.878496,0.273884 q -0.175699,0.516763 -0.232543,0.604613 1.116207,1.596797 4.056587,3.007559 z m 0.165364,4.91958 -0.676959,0.645954 q -1.514115,-1.157549 -2.346102,-2.826692 -0.547769,1.550288 -2.268589,2.806021 l -0.676959,-0.625283 q 1.19889,-0.795814 1.798334,-1.875848 0.599445,-1.080034 0.682127,-3.079906 l 0.909502,0.07751 q 0,0.268716 -0.04134,0.671791 l -0.03617,0.361734 q 0,0.273884 0.30489,1.033525 0.310058,0.754474 0.899167,1.467606 0.594277,0.707965 1.452103,1.343583 z m -4.800725,-1.374588 -0.702797,0.63045 q -0.594277,-0.780312 -1.162716,-1.276404 -0.651121,1.421098 -2.020542,2.676831 l -0.687295,-0.614948 q 1.229895,-1.095537 1.767329,-2.201409 0.5426,-1.105872 0.697629,-2.795686 l 0.919838,0.09819 q -0.103353,0.940508 -0.366902,1.91719 1.00252,0.862993 1.555456,1.565791 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:'Adobe Garamond Pro';-inkscape-font-specification:'Adobe Garamond Pro Bold';fill:#ffffff"
|
||||
id="path961" />
|
||||
</g>
|
||||
<g
|
||||
aria-label="L"
|
||||
id="text841"
|
||||
style="font-style:normal;font-weight:normal;font-size:43.3964px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.08492">
|
||||
<path
|
||||
d="M 84.81389,94.883148 V 91.324643 H 69.191186 V 63.247172 h -4.035865 v 31.635976 z"
|
||||
style="fill:#ffffff;stroke-width:1.08492"
|
||||
id="path964" />
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
id="g921"
|
||||
transform="translate(29.198135,-14.725175)" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.5 KiB |
487
libretranslate/static/js/app.js
Normal file
487
libretranslate/static/js/app.js
Normal 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
|
6
libretranslate/static/js/materialize.min.js
vendored
Normal file
6
libretranslate/static/js/materialize.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
libretranslate/static/js/prism.min.js
vendored
Normal file
1
libretranslate/static/js/prism.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
6
libretranslate/static/js/vue@2.js
Normal file
6
libretranslate/static/js/vue@2.js
Normal file
File diff suppressed because one or more lines are too long
39
libretranslate/suggestions.py
Normal file
39
libretranslate/suggestions.py
Normal 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
|
323
libretranslate/templates/index.html
Normal file
323
libretranslate/templates/index.html
Normal file
|
@ -0,0 +1,323 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<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, offline capable and easy to setup. Run your own API server in just a few minutes.">
|
||||
<meta name="keywords" content="translation,api">
|
||||
|
||||
<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>
|
||||
|
||||
|
||||
{% 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 }}');
|
||||
</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 class="white">
|
||||
<header>
|
||||
<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') }}" 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>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main id="app">
|
||||
<div class="section no-pad-bot center" v-if="loading">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="preloader-wrapper active">
|
||||
<div class="spinner-layer spinner-blue-only">
|
||||
<div class="circle-clipper left">
|
||||
<div class="circle"></div>
|
||||
</div><div class="gap-patch">
|
||||
<div class="circle"></div>
|
||||
</div><div class="circle-clipper right">
|
||||
<div class="circle"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="error">
|
||||
<div class="section no-pad-bot">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col s12 m7">
|
||||
<div class="card horizontal">
|
||||
<div class="card-stacked">
|
||||
<div class="card-content">
|
||||
<i class="material-icons">warning</i><p> [[ error ]]</p>
|
||||
</div>
|
||||
<div class="card-action">
|
||||
<a href="#" @click="dismissError">Dismiss</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div class="section no-pad-bot">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<h3 class="header center">Translation API</h3>
|
||||
<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>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col s6 language-select">
|
||||
<a href="javascript:void(0)" @click="swapLangs" class="btn-switch-language">
|
||||
<i class="material-icons">swap_horiz</i>
|
||||
</a>
|
||||
<span>Translate into</span>
|
||||
<select class="browser-default" v-model="targetLang" ref="targetLangDropdown" @change="handleInput">
|
||||
<template v-for="option in targetLangs">
|
||||
<option v-if="option.code !== 'auto'" :value="option.code">[[ option.name ]]</option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
<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" 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 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" v-if="translationType !== 'files'">
|
||||
<div class="container">
|
||||
<div class="row center">
|
||||
<div class="col s12 m12">
|
||||
|
||||
<div class="row center">
|
||||
<div class="col s12 m12 l6 left-align">
|
||||
<p class="mb-0">Request</p>
|
||||
<pre class="code mt-0"><code class="language-javascript" v-html="$options.filters.highlight(requestCode)">
|
||||
</code></pre>
|
||||
</div>
|
||||
<div class="col s12 m12 l6 left-align">
|
||||
<p class="mb-0">Response</p>
|
||||
<pre class="code mt-0"><code class="language-javascript" v-html="$options.filters.highlight(output)">
|
||||
</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if web_version %}
|
||||
<div class="section no-pad-bot">
|
||||
<div class="container">
|
||||
<div class="row center">
|
||||
<div class="col s12 m12">
|
||||
<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>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="page-footer blue darken-3">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<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>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" 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 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>
|
||||
|
||||
<script src="{{ url_for('static', filename='js/materialize.min.js') }}"></script>
|
||||
<script>
|
||||
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-3.0
|
||||
window.Prism = window.Prism || {};
|
||||
window.Prism.manual = true;
|
||||
// @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>
|
22
libretranslate/templates/javascript-licenses.html
Normal file
22
libretranslate/templates/javascript-licenses.html
Normal file
|
@ -0,0 +1,22 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>jslicense-labels1 for LibreTranslate</title>
|
||||
</head>
|
||||
<body>
|
||||
<h3>Weblabels</h3>
|
||||
<table id="jslicense-labels1" border="1">
|
||||
<tr>
|
||||
<td><a href="{{ url_for('static', filename='js/vue@2.js') }}">Vue.js</a></td>
|
||||
<td><a href="http://www.jclark.com/xml/copying.txt">Expat</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="{{ url_for('static', filename='js/prism.min.js') }}">prism.min.js</a></td>
|
||||
<td><a href="http://www.jclark.com/xml/copying.txt">Expat</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="{{ url_for('static', filename='js/materialize.min.js') }}">materialize.min.js</a></td>
|
||||
<td><a href="http://www.jclark.com/xml/copying.txt">Expat</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
0
libretranslate/tests/__init__.py
Normal file
0
libretranslate/tests/__init__.py
Normal file
0
libretranslate/tests/test_api/__init__.py
Normal file
0
libretranslate/tests/test_api/__init__.py
Normal file
20
libretranslate/tests/test_api/conftest.py
Normal file
20
libretranslate/tests/test_api/conftest.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
import sys
|
||||
import pytest
|
||||
|
||||
from libretranslate.app import create_app
|
||||
from libretranslate.default_values import DEFAULT_ARGUMENTS
|
||||
from libretranslate.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()
|
26
libretranslate/tests/test_api/test_api_detect_language.py
Normal file
26
libretranslate/tests/test_api/test_api_detect_language.py
Normal 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
|
|
@ -0,0 +1,4 @@
|
|||
def test_api_get_frontend_settings(client):
|
||||
response = client.get("/frontend/settings")
|
||||
|
||||
assert response.status_code == 200
|
16
libretranslate/tests/test_api/test_api_get_languages.py
Normal file
16
libretranslate/tests/test_api/test_api_get_languages.py
Normal 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
|
10
libretranslate/tests/test_api/test_api_spec.py
Normal file
10
libretranslate/tests/test_api/test_api_spec.py
Normal 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
|
61
libretranslate/tests/test_api/test_api_translate.py
Normal file
61
libretranslate/tests/test_api/test_api_translate.py
Normal 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
|
9
libretranslate/tests/test_init.py
Normal file
9
libretranslate/tests/test_init.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
from libretranslate.init import boot
|
||||
from argostranslate import package
|
||||
|
||||
|
||||
def test_boot_argos():
|
||||
"""Test Argos translate models initialization"""
|
||||
boot(["en", "es"])
|
||||
|
||||
assert len(package.get_installed_packages()) >= 2
|
Loading…
Add table
Add a link
Reference in a new issue