app->libretranslate; mv tests/ inside libretranslate/

This commit is contained in:
reynoldsnlp 2022-12-29 16:44:53 -07:00
parent 40a1141eac
commit a23a9fbd75
47 changed files with 24 additions and 25 deletions

View file

@ -0,0 +1,3 @@
import os
from .main import main
from .manage import manage

100
libretranslate/api_keys.py Normal file
View 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

File diff suppressed because it is too large Load diff

View 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
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

58
libretranslate/flood.py Normal file
View 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
View 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
View 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
View 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
View 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)

View 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

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())

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

@ -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;
}
}

View 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';
}

File diff suppressed because one or more lines are too long

View 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}

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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

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

1
libretranslate/static/js/prism.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

@ -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>

View 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>

View file

View 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()

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

@ -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