diff --git a/.gitignore b/.gitignore index 256d239..4c46265 100644 --- a/.gitignore +++ b/.gitignore @@ -131,3 +131,4 @@ installed_models/ # Misc api_keys.db +suggestions.db \ No newline at end of file diff --git a/app/app.py b/app/app.py index ea54a2d..659409f 100644 --- a/app/app.py +++ b/app/app.py @@ -9,6 +9,7 @@ from app import flood from app.language import detect_languages, transliterate from .api_keys import Database +from .suggestions import Database as SuggestionsDatabase from translatehtml import translate_html @@ -565,6 +566,9 @@ def create_app(args): frontendTimeout: type: integer description: Frontend translation timeout + suggestions: + type: boolean + description: Whether submitting suggestions is enabled. language: type: object properties: @@ -591,6 +595,7 @@ def create_app(args): { "charLimit": args.char_limit, "frontendTimeout": args.frontend_timeout, + "suggestions": args.suggestions, "language": { "source": { "code": frontend_argos_language_source.code, @@ -604,8 +609,76 @@ def create_app(args): } ) + @app.route("/suggest", methods=["POST"]) + @limiter.exempt + def suggest(): + """ + Submit a suggestion to improve a translation + --- + tags: + - feedback + parameters: + - in: formData + name: q + schema: + type: string + example: Hello world! + required: true + description: Original text + - in: formData + name: s + schema: + type: string + example: ¡Hola mundo! + required: true + description: Suggested translation + - in: formData + name: source + schema: + type: string + example: en + required: true + description: Language of original text + - in: formData + name: target + schema: + type: string + example: es + required: true + description: Language of suggested translation + responses: + 200: + description: Success + schema: + id: suggest-response + type: object + properties: + success: + type: boolean + description: Whether submission was successful + 403: + description: Not authorized + schema: + id: error-response + type: object + properties: + error: + type: string + description: Error message + """ + if not args.suggestions: + abort(403, description="Suggestions are disabled on this server.") + + q = request.values.get("q") + s = request.values.get("s") + source_lang = request.values.get("source") + target_lang = request.values.get("target") + + SuggestionsDatabase().add(q, s, source_lang, target_lang) + return jsonify({"success": True}) + swag = swagger(app) - swag["info"]["version"] = "1.2" + swag["info"]["version"] = "1.2.1" swag["info"]["title"] = "LibreTranslate" @app.route("/spec") diff --git a/app/default_values.py b/app/default_values.py index 2538a0e..d41843a 100644 --- a/app/default_values.py +++ b/app/default_values.py @@ -110,6 +110,11 @@ _default_options_objects = [ 'name': 'LOAD_ONLY', 'default_value': None, 'value_type': 'str' + }, + { + 'name': 'SUGGESTIONS', + 'default_value': False, + 'value_type': 'bool' } ] diff --git a/app/main.py b/app/main.py index d9b5b44..ffaef87 100644 --- a/app/main.py +++ b/app/main.py @@ -102,6 +102,9 @@ def main(): metavar="", help="Set available languages (ar,de,en,es,fr,ga,hi,it,ja,ko,pt,ru,zh)", ) + parser.add_argument( + "--suggestions", default=DEFARGS['SUGGESTIONS'], action="store_true", help="Allow user suggestions" + ) args = parser.parse_args() app = create_app(args) diff --git a/app/static/css/main.css b/app/static/css/main.css index 71dd74e..728093b 100644 --- a/app/static/css/main.css +++ b/app/static/css/main.css @@ -96,7 +96,7 @@ h3.header { } .btn-delete-text:focus, -.btn-copy-translated:focus { +.btn-action:focus { background: none !important; } @@ -107,26 +107,35 @@ h3.header { color: #777; pointer-events: none; } - -.btn-copy-translated { +.actions { position: absolute; - right: 2.75rem; + right: 1.25rem; bottom: 1rem; display: flex; +} + +.btn-action { + display: flex; align-items: center; color: #777; font-size: 0.85rem; background: none; border: none; cursor: pointer; - margin-right: -1.5rem; } -.btn-copy-translated span { +.btn-blue { + color: #42A5F5; +} +.btn-action:disabled { + color: #777; +} + +.btn-action span { padding-right: 0.5rem; } -.btn-copy-translated .material-icons { +.btn-action .material-icons { font-size: 1.35rem; } diff --git a/app/suggestions.py b/app/suggestions.py new file mode 100644 index 0000000..28b943a --- /dev/null +++ b/app/suggestions.py @@ -0,0 +1,31 @@ +import sqlite3 +import uuid + +from expiringdict import ExpiringDict + +DEFAULT_DB_PATH = "suggestions.db" + + +class Database: + def __init__(self, db_path=DEFAULT_DB_PATH, max_cache_len=1000, max_cache_age=30): + 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 diff --git a/app/templates/index.html b/app/templates/index.html index e79a549..4ca063f 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -126,7 +126,7 @@ - + @@ -134,11 +134,11 @@
+ +
@@ -147,15 +147,26 @@
- - -
+ + +
+ + + + +
+
-
+
@@ -261,11 +272,15 @@ loadingTranslation: false, inputText: "", inputTextareaHeight: 250, - translatedText: "", + savedTanslatedText: "", + translatedText: "", output: "", charactersLimit: -1, - copyTextLabel: "Copy text" + copyTextLabel: "Copy text", + + suggestions: false, + isSuggesting: false, }, mounted: function(){ var self = this; @@ -279,7 +294,8 @@ self.sourceLang = self.settings.language.source.code; self.targetLang = self.settings.language.target.code; self.charactersLimit = self.settings.charLimit; - }else { + self.suggestions = self.settings.suggestions; + }else { self.error = "Cannot load /frontend/settings"; self.loading = false; } @@ -335,7 +351,7 @@ 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