From 5ac50ed6672160bd4eb1be1e8335a10249c933a6 Mon Sep 17 00:00:00 2001 From: Domeniko Gentner Date: Fri, 25 Dec 2020 14:26:48 +0100 Subject: [PATCH] i18n * Added class for internationalization to the project * Added English and German language files * Moved all strings to the language files * Translated to German * Added context processor to inject language file into every template * Translated modals * Added translation endpoint for Javascript * Translated Javascript messages * Improved dashboard javascript by removing duplicates --- i18n/de-DE.json | 36 +++- i18n/en-US.json | 35 ++- .../blueprints/bp_jsconnector/__init__.py | 1 + .../blueprints/bp_jsconnector/language.py | 21 ++ static/js/dashboard.js | 203 +++++++++--------- templates/login.html | 2 +- templates/modals/comments-export-all.html | 17 +- templates/modals/project-delete.html | 18 +- templates/modals/project_edit.html | 55 ++--- templates/modals/project_not_found.html | 21 +- 10 files changed, 246 insertions(+), 163 deletions(-) create mode 100644 labertasche/blueprints/bp_jsconnector/language.py diff --git a/i18n/de-DE.json b/i18n/de-DE.json index 619566d..b2b717c 100644 --- a/i18n/de-DE.json +++ b/i18n/de-DE.json @@ -2,12 +2,15 @@ "html_language": "de", "browser_language": "de-DE", "ok": "ok", + "link": "link", "cancel": "abbrechen", "dashboard": "dashboard", "username": "benutzername", "password": "passwort", "login": "login", "logout": "abmelden", + "error": "fehler", + "warning": "warnung", "comments": "kommentare", "unpublished": "unveröffentlicht", "published": "veröffentlicht", @@ -18,7 +21,8 @@ "delete": "löschen", "new": "neu", "project": "projekt", - "new_project": "neues projekt", + "new_project": "Neues Projekt", + "project_name": "Projekt Name", "statistics": "statistiken", "address": "adresse", "status": "status", @@ -44,7 +48,35 @@ "tooltip_export_all_comments": "Alle Kommentare nach Hugo exportieren.
Wird normalerweise nicht benötigt.", "tooltip_manage_this_project": "Dieses Projekt verwalten", "placeholder_search_mail": "Mail Adressen durchsuchen", + "export_all_comments": "Alle Kommentare exportieren", + "export_warning_text": "Dies wird alle Kommentare neu exportieren. Normalerweise wird das nicht benötigt, kann aber in gewissen Situatonen hilfreich sein.", + "delete_project_warning": "Du bist dabei ein Projekt zu löschen. Dies wird alle dazugehörigen Daten auch löschen. Bitte führe einen manuellen SQL Dump durch, falls du die Daten behalten willst.", + "wish_to_proceed": "Möchtest du fortfahren?", "tooltip_email_blocked": "Email ist momentan gesperrt. Zum entsperren klicken.", "tooltip_email_allowed": "Email darf momentan ohne Bestätigung posten. Zum Sperren klicken.", - "tooltip_delete_email": "Eintrag löschen. Email unterliegt wieder den normalen Regeln." + "tooltip_delete_email": "Eintrag löschen. Email unterliegt wieder den normalen Regeln.", + "javascript_required_field_empty": "Ein Pflichtfeld wurde leer gelassen!", + "javascript_invalid_project_name": "Der Projektname ist nicht gültig, bitte nur Buchstaben und Zahlen verwenden!", + "javascript_project_duplicate": "Ein Projekt mit diesem name existiert bereits!", + "javascript_blogurl_invalid": "Die blog-url ist ungültig!", + "javascript_output_nonexistent": "Der Ausgabe Pfad existiert nicht!", + "javascript_gravatar_cache_nonexistent": "Der caching Pfad existiert nicht!", + "javascript_exception": "Es gab eine unerwartet Ausnahme. Bitte eröffne ein issue auf Github!", + "javascript_edit_project_modal_title": "Projekt %name% editieren", + "description_hugo_url": "Die URL der Hugo Seite für dieses Projekt.", + "tooltip_hugo_url": "Die URL sollte so aussehen: https://beispiel.de", + "tooltip_project_name": "Bitte wähle einen einzigartigen Namen für das Projekt.", + "description_output_path": "Hugo Datenverzeichnis", + "tooltip_output_path": "Der Pfad zum Datenverzeichnis der Hugo Installation. Kann relativ sein.", + "description_gravatar_cache": "Gravatar Bilder lokal speichern?", + "tooltip_gravatar_cache": "Wenn dies aktiviert ist, wird Labertasche Gravatare herunterladen und hier speichern.", + "tooltip_gravatar_dir": "Der Pfad zum Zwischenspeicher für Gravatare. Kann relativ sein.", + "description_gravatar_dir": "Lokales Verzeichnis für Gravatar", + "tooltip_gravatar_size": "Die Größe der Gravatare. Sollte ein vielfaches von 2 sein, bswp. 64, 128 oder 256.", + "description_gravatar_size": "Gravatar Bildgröße", + "description_send_otp": "OTP zur Veröffentlichung senden?", + "tooltip_send_otp": "Wenn aktiviert, bekommt der User ein Einmalpasswort zugesendet, mit dem der Kommentar veröffentlich wird (empfohlen). Wenn deaktiviert, wird der Kommentar immer veröffentlicht (ausser Spam).", + "description_enable_smileys": "Smiley Addon aktivieren?", + "tooltip_enable_smileys": "Wenn aktiviert, werden simple Text Smileys mit Emojis ersetzt. In /etc/labertasche/smileys.yaml findest du die aktivierten Smileys.", + "message_project_404": "Das angegebene Projekt wurde nicht gefunden. Wurde es vielleicht gelöscht? Wenn du denkst, dass dies ein Bug ist, melde es bitte auf Github!" } diff --git a/i18n/en-US.json b/i18n/en-US.json index bceb0fe..9167e01 100644 --- a/i18n/en-US.json +++ b/i18n/en-US.json @@ -3,11 +3,14 @@ "browser_language": "en-US", "ok": "ok", "cancel": "cancel", + "link": "link", "dashboard": "dashboard", "username": "username", "password": "password", "login": "login", "logout": "logout", + "error": "error", + "warning": "warning", "comments": "comments", "unpublished": "unpublished", "published": "published", @@ -22,6 +25,7 @@ "statistics": "statistics", "address": "address", "status": "status", + "project_name": "Projekt Name", "manage_mail": "manage mail addresses", "stats_label_regular_comments": "regular comments", "stats_label_unpublished_comments": "unpublished comments", @@ -45,5 +49,34 @@ "placeholder_search_mail": "Search mail", "tooltip_email_blocked": "Email is currently blocked. Click to unblock.", "tooltip_email_allowed": "Email is currently excempt from spam detection. Click to block.", - "tooltip_delete_email": "Delete entry, Email has to follow the regular rules." + "tooltip_delete_email": "Delete entry, Email has to follow the regular rules.", + "export_all_comments": "Export all comments", + "export_warning_text": "This will export all comments of this project to all locations. Usually this is not needed, but can be helpful, if you have imported backups or similar.", + "wish_to_proceed": "Do you wish to proceed?", + "delete_project_warning": "You are about to delete a project. All associated data will be unrecoverably lost! Please perform a manual sql dump if you would like to retain that data.", + "javascript_required_field_empty": "A required field has been left empty!", + "javascript_invalid_project_name": "The project name is not valid. Please only use alphanumeric characters!", + "javascript_project_duplicate": "A project with this name already exists!", + "javascript_blogurl_invalid": "The blog-url is invalid!", + "javascript_output_nonexistent": "This output path does not exist!", + "javascript_gravatar_cache_nonexistent": "The cache path does not exist!", + "javascript_exception": "There was an unexpected exception. Please open an issue on Github!", + "javascript_edit_project_modal_title": "Edit project %name%", + "javascript_new_project_modal_title": "New Project", + "description_hugo_url": "URL of your Hugo site for this project", + "tooltip_hugo_url": "An URL is formed like this: https://example.com", + "tooltip_project_name": "Please select an unique name for your project.", + "description_output_path": "Hugo Data dir", + "tooltip_output_path": "The path to the data directory of your Hugo installation. Path can be relative.", + "description_gravatar_cache": "Cache Gravatar images locally?", + "tooltip_gravatar_cache": "If enabled, Labertasche will download gravatars to this location", + "tooltip_gravatar_dir": "The directory where to save the Gravatar images. Path can be relative.", + "description_gravatar_dir": "Gravatar caching directory.", + "tooltip_gravatar_size": "The numeric size of the images to download. Must be a power of 2, e.g 64, 128, 256", + "description_gravatar_size": "Gravatar image size", + "description_send_otp": "Send OTP to publish?", + "tooltip_send_otp": "If enabled, the user will be mailed a one time password to publish the comment (recommended). If disabled, it will be published by default (except spam).", + "description_enable_smileys": "Enable Smiley Addon?", + "tooltip_enable_smileys": "If enabled, simple text Smileys will be replaced with Emojis. Please see /etc/labertasche/smileys.yaml for more.", + "message_project_404": "The specified project was not found! Did you delete it? If you believe this to be a bug, please report it." } diff --git a/labertasche/blueprints/bp_jsconnector/__init__.py b/labertasche/blueprints/bp_jsconnector/__init__.py index ec3ba38..f982610 100644 --- a/labertasche/blueprints/bp_jsconnector/__init__.py +++ b/labertasche/blueprints/bp_jsconnector/__init__.py @@ -13,5 +13,6 @@ bp_jsconnector = Blueprint("bp_jsconnector", __name__, url_prefix='/api/') from .projects import api_create_project, api_delete_project, api_edit_project_name from .mail import api_toggle_email_reputation, api_reset_mail_reputation +from .language import api_translation from .comments import api_comment_allow_user, api_comment_allow_comment, \ api_comment_block_mail, api_comments_delete_comment diff --git a/labertasche/blueprints/bp_jsconnector/language.py b/labertasche/blueprints/bp_jsconnector/language.py new file mode 100644 index 0000000..9b143f2 --- /dev/null +++ b/labertasche/blueprints/bp_jsconnector/language.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# /********************************************************************************** +# * _author : Domeniko Gentner +# * _mail : code@tuxstash.de +# * _repo : https://git.tuxstash.de/gothseidank/labertasche +# * _license : This project is under MIT License +# *********************************************************************************/ +from . import bp_jsconnector +from flask import make_response, jsonify, request +from flask_cors import cross_origin +from flask_login import login_required +from labertasche.language import Language + + +@cross_origin +@bp_jsconnector.route('/language/') +@login_required +def api_translation(): + lang = Language(request=request) + return make_response(jsonify(lang.i18n), 200) diff --git a/static/js/dashboard.js b/static/js/dashboard.js index 7cc14a2..396e950 100644 --- a/static/js/dashboard.js +++ b/static/js/dashboard.js @@ -5,20 +5,27 @@ // # * _license : This project is under MIT License // # *********************************************************************************/ -async function get(partial, callback) { - await fetch(window.location.protocol + "//" + window.location.host + partial, +async function get(partial, callback = null, accept_lang = null) { + + let headers = { + 'Access-Control-Allow-Origin': window.location.host, + 'Accept': 'application/json', + 'Content-Type': 'application/json' + } + if (accept_lang){ + headers = Object.assign(headers, {'Accept-Language': accept_lang}) + } + + return await fetch(window.location.protocol + "//" + window.location.host + partial, { mode: "cors", - headers: { - 'Access-Control-Allow-Origin': window.location.host, - 'Accept': 'application/json', - 'Content-Type': 'application/json' - }, + headers, method: "GET" }) .then(async function (response) { - let result = await response.json(); - callback(result); + const result = await response.json(); + if (callback){ callback(result); } + return result; }) .catch(function (exc) { console.log(exc); @@ -26,8 +33,8 @@ async function get(partial, callback) { }) } -async function post(partial, stringified_json, callback) { - await fetch(window.location.protocol + "//" + window.location.host + partial, +async function post(partial, stringified_json, callback = null) { + return await fetch(window.location.protocol + "//" + window.location.host + partial, { mode: "cors", headers: { @@ -39,11 +46,13 @@ async function post(partial, stringified_json, callback) { body: stringified_json }) .then(async function (response) { - let result = await response.json(); - callback(result); + const result = await response.json(); + if (callback){ callback(result); } + return result; }) .catch(function (exc) { console.log(exc); + return null; }) } @@ -59,7 +68,10 @@ function dashboard_mailsearch(search_txt) children[i].style.display = "none"; let iTxt = children[i].innerText.replace(/(\r\n|\n|\r)/gm, "").trim(); - if ( search_txt.value === iTxt.slice(0, search_txt.value.length)){ + if (iTxt.includes(search_txt.value)){ + children[i].style.display = "table-row"; + } + if(search_txt.value === ''){ children[i].style.display = "table-row"; } } @@ -129,6 +141,9 @@ async function show_modal_with_project(id_name, proj_name) // Get Dialog let modal = document.getElementById(id_name); + // Load i18n + let i18n = await get('/api/language', null, document.body.dataset.language); + if (proj_name){ // Get Data await get('/api/project/get/' + proj_name, @@ -147,7 +162,7 @@ async function show_modal_with_project(id_name, proj_name) proj_el.value = proj_name // Set project name - title.innerText = "Edit project '" + proj_name + "'"; + title.innerText =i18n['javascript_edit_project_modal_title'].replace('%name%', proj_name); // Make active modal.classList.add("is-active"); @@ -158,7 +173,7 @@ async function show_modal_with_project(id_name, proj_name) } if (proj_name == null){ // Set project name - title.innerText = "New Project"; + title.innerText = i18n['new_project']; // Reset fields, needed when user pressed cancel on edit modal document.getElementById('edit-project-name').value = ""; @@ -199,96 +214,32 @@ async function save_project_settings(id_name) "addon_smileys": document.getElementById('edit-project-addons-smileys').checked } + // Get field for errors and reset it + let error = document.getElementById('modal-edit-error-messages') + error.innerText = '' + let has_error = false; + if (modal.dataset.mode === "edit"){ let old_name = modal.dataset.name; - await post('/api/project/edit/' + old_name, JSON.stringify(json_data), function(result){ - let error = document.getElementById('modal-edit-error-messages') - error.innerText = '' - - if (result['status'] === 'too-short'){ - error.innerText = "A required field has been left empty!" - return; - } - if (result['status'] === 'invalid-project-name') { - error.innerText = "The project name is not valid. Please only use alphanumeric characters!" - return; - } - if (result['status'] === 'project-exists') { - error.innerText = "A project with this name already exists!" - return; - } - if (result['status'] === 'invalid-blog-url') { - error.innerText = "The blog-url is invalid!" - return; - } - if (result['status'] === 'invalid-path-output') { - error.innerText = "This output path does not exist!" - return; + if (result['status'] !== 'ok') { + has_error = resolve_error_status(error, result); } - if (result['status'] === 'invalid-path-cache') { - error.innerText = "The cache path does not exist!" - return; - } - if (result['status'] === 'exception') { - error.innerText = "There was an unexpected exception. Please report this to contact@tuxstash.de:" - error.innerText += result['msg'] - return; - } - - // Reset button - btn.classList.remove('is-loading'); - window.location.reload(true); }) } if (modal.dataset.mode === 'new'){ await post('/api/project/new', JSON.stringify(json_data), function(result){ - let error = document.getElementById('modal-edit-error-messages') - error.innerText = ''; - - console.log(result['status']); - if (result['status'] === 'too-short'){ - error.innerText = "A required field has been left empty!"; - btn.classList.remove('is-loading'); - return; - } - if (result['status'] === 'invalid-project-name') { - error.innerText = "The project name is not valid. Please only use alphanumeric characters!"; - btn.classList.remove('is-loading'); - return; - } - if (result['status'] === 'project-exists') { - error.innerText = "A project with this name already exists!"; - btn.classList.remove('is-loading'); - return; - } - if (result['status'] === 'invalid-blog-url') { - error.innerText = "The blog-url is invalid!"; - btn.classList.remove('is-loading'); - return; + if (result['status'] !== 'ok') { + has_error = resolve_error_status(error, result); } - if (result['status'] === 'invalid-path-output') { - error.innerText = "This output path does not exist!"; - btn.classList.remove('is-loading'); - return; - } - if (result['status'] === 'invalid-path-cache') { - error.innerText = "The cache path does not exist!"; - btn.classList.remove('is-loading'); - return; - } - if (result['status'] === 'exception') { - error.innerText = "There was an unexpected exception. Please report this to contact@tuxstash.de:"; - error.innerText += result['msg']; - btn.classList.remove('is-loading'); - return; - } - - // Reset button - btn.classList.remove('is-loading'); - window.location.reload(true); }) } + + // Reset button + btn.classList.remove('is-loading'); + if (has_error === false){ + window.location.reload(true); + } } // ------------------------------------------------------ @@ -320,15 +271,57 @@ async function export_all_comments(btn) { btn.classList.add('is-loading'); let proj_name = document.getElementById('modal-comments-export').dataset.name; + let result = await get('/api/comment-export-all/' + proj_name); - await get('/api/comment-export-all/' + proj_name, function(result){ - if (result['status'] === 'ok'){ - hide_modal('modal-comments-export'); - } - if (result['status'] === 'not-found'){ - // Redirect to error - hide_modal('modal-comments-export', '?error=404'); - } - btn.classList.remove('is-loading'); - }) + if (result['status'] === 'ok'){ + hide_modal('modal-comments-export'); + } + if (result['status'] === 'not-found'){ + // Redirect to error + hide_modal('modal-comments-export', '?error=404'); + } + + // Reset button + btn.classList.remove('is-loading'); + console.log(result); +} + + +async function resolve_error_status(field, result) +{ + // Load i18n + let i18n = await get('/api/language', null, document.body.dataset.language); + + let has_error = false; + + if (result['status'] === 'too-short'){ + field.innerText = i18n['javascript_required_field_empty']; + has_error = true; + } + if (result['status'] === 'invalid-project-name') { + field.innerText = i18n['javascript_invalid_project_name']; + has_error = true; + } + if (result['status'] === 'project-exists') { + field.innerText = i18n['javascript_project_duplicate']; + has_error = true; + } + if (result['status'] === 'invalid-blog-url') { + field.innerText = i18n['javascript_blogurl_invalid']; + has_error = true; + } + if (result['status'] === 'invalid-path-output') { + field.innerText = i18n['javascript_output_nonexistent']; + has_error = true; + } + if (result['status'] === 'invalid-path-cache') { + field.innerText = "The cache path does not exist!"; + has_error = true; + } + if (result['status'] === 'exception') { + field.innerText = i18n['javascript_exception']; + field.innerText += "\n" + result['msg']; + has_error = true; + } + return has_error; } diff --git a/templates/login.html b/templates/login.html index 0f63287..2187e12 100644 --- a/templates/login.html +++ b/templates/login.html @@ -9,7 +9,7 @@ Labertasche {{ i18n['dashboard'] | capitalize }} - +
diff --git a/templates/modals/project-delete.html b/templates/modals/project-delete.html index 9d51f01..d132212 100644 --- a/templates/modals/project-delete.html +++ b/templates/modals/project-delete.html @@ -2,19 +2,19 @@ diff --git a/templates/modals/project_edit.html b/templates/modals/project_edit.html index c78bb38..29fa718 100644 --- a/templates/modals/project_edit.html +++ b/templates/modals/project_edit.html @@ -2,94 +2,97 @@ diff --git a/templates/modals/project_not_found.html b/templates/modals/project_not_found.html index b3ed5d7..b5d02af 100644 --- a/templates/modals/project_not_found.html +++ b/templates/modals/project_not_found.html @@ -2,22 +2,23 @@