diff --git a/app.py b/app.py index d33f63c..07ba5fe 100644 --- a/app.py +++ b/app.py @@ -246,7 +246,7 @@ def login(): session.permanent = request.form.get('remember_me', False) return redirect(url_for('index')) else: - flash("Wrong password", 'danger') + flash(_("Wrong password"), 'danger') return redirect(url_for('admin.login')) else: if logged_in: @@ -321,7 +321,7 @@ def blacklist(): blacklist = request.form.get('blacklist') with open(const.blacklistFile, 'w') as file: file.write(blacklist) - return {'message': 'Blacklist updated!'}, 200 + return {'message': _("Blacklist updated!")}, 200 else: blacklist = func.readPlainFile(const.blacklistFile) @@ -355,7 +355,7 @@ def postInstall(): # -- server routes -- @api_bp.errorhandler(404) -def notFound(): +def notFound(e): return jsonify({'error': 'Not found'}), 404 @api_bp.errorhandler(400) @@ -523,9 +523,9 @@ def addAnswer(): cw = request.form.get('cw', '') if not question_id: - abort(400, "Missing 'question_id' attribute or 'question_id' is empty") + abort(400, _("Missing 'question_id' attribute or 'question_id' is empty")) if not answer: - abort(400, "Missing 'answer' attribute or 'answer' is empty") + abort(400, _("Missing 'answer' attribute or 'answer' is empty")) return func.addAnswer(question_id, answer, cw) @@ -543,24 +543,24 @@ def uploadFavicon(): func.generateFavicon(filename) - return {'message': 'Successfully updated favicon!'}, 201 + return {'message': _("Successfully updated favicon!")}, 201 elif favicon and not func.allowedFile(favicon.filename): - return {'error': 'File type is not supported'}, 400 + return {'error': _("File type is not supported")}, 400 elif not favicon: - return {'error': "favicon is not specified"}, 400 + return {'error': _("favicon is not specified")}, 400 @api_bp.route('/upload_emoji/', methods=['POST']) @loginRequired def uploadEmoji(): if 'emoji' not in request.files: - return jsonify({'error': 'No file part in the request'}), 400 + return jsonify({'error': _("No file part in the request")}), 400 emoji = request.files['emoji'] if emoji.filename == '': - return jsonify({'error': 'No file selected for uploading'}), 400 + return jsonify({'error': _("No file selected for uploading")}), 400 if not func.allowedFile(emoji.filename): - return jsonify({'error': 'Invalid file type. Only png, jpg, jpeg, webp, bmp, jxl supported'}), 400 + return jsonify({'error': _("Invalid file type. Only png, jpg, jpeg, webp, bmp, jxl, gif supported")}), 400 # Secure the filename and determine the archive name filename = secure_filename(emoji.filename) @@ -569,20 +569,20 @@ def uploadEmoji(): emoji.save(emoji_path) - return jsonify({'message': f'Emoji {filename} successfully uploaded'}), 201 + return jsonify({'message': _("Emoji {} successfully uploaded").format(filename)}), 201 @api_bp.route('/upload_emoji_pack/', methods=['POST']) @loginRequired def uploadEmojiPack(): if 'emoji_archive' not in request.files: - return jsonify({'error': 'No file part in the request'}), 400 + return jsonify({'error': _("No file part in the request")}), 400 emoji_archive = request.files['emoji_archive'] if emoji_archive.filename == '': - return jsonify({'error': 'No file selected for uploading'}), 400 + return jsonify({'error': _("No file selected for uploading")}), 400 if not func.allowedArchive(emoji_archive.filename): - return jsonify({'error': 'Invalid file type. Only .zip, .tar, .tar.gz, .tar.bz2 allowed'}), 400 + return jsonify({'error': _("Invalid file type. Only .zip, .tar, .tar.gz, .tar.bz2 allowed")}), 400 filename = secure_filename(emoji_archive.filename) archive_name = func.stripArchExtension(filename) @@ -601,15 +601,15 @@ def uploadEmojiPack(): with tarfile.open(archive_path, 'r:*') as tar_ref: tar_ref.extractall(extract_path) else: - return jsonify({'error': 'Unsupported archive format'}), 400 + return jsonify({'error': _("Unsupported archive format")}), 400 # parse meta.json if it exists meta_json_path = extract_path / 'meta.json' if meta_json_path.exists(): processed_emojis = func.processEmojis(meta_json_path) - return jsonify({'message': f'Successfully uploaded and processed {len(processed_emojis)} emojis from archive "{filename}".'}), 201 + return jsonify({'message': _('Successfully uploaded and processed {} emojis from archive "{}".').format(len(processed_emojis), filename)}), 201 else: - return jsonify({'message': f'Archive {filename} successfully uploaded and extracted.'}), 201 + return jsonify({'message': _("Archive {} successfully uploaded and extracted.").format(filename)}), 201 except Exception as e: return jsonify({'error': str(e)}), 500 @@ -654,11 +654,11 @@ def deleteEmoji(): emoji_ext = os.path.splitext(f"{emoji_base_path}/{emoji_file_path}") if not emoji_file_path.exists() or not emoji_file_path.is_file(): - return jsonify({'error': 'Emoji not found'}), 404 + return jsonify({'error': _("Emoji not found")}), 404 try: emoji_file_path.unlink() - return jsonify({'message': f'Emoji "{emoji_name}" deleted successfully'}), 200 + return jsonify({'message': _('Emoji "{}" deleted successfully').format(emoji_name)}), 200 except Exception as e: return jsonify({'error': str(e)}), 500 @@ -670,14 +670,14 @@ def deleteEmojiPack(): emoji_base_path = Path.cwd() / 'static' / 'emojis' / pack_name.lower() if not emoji_base_path.exists() or not emoji_base_path.is_dir(): - return jsonify({'error': f'Emoji pack "{pack_name.capitalize()}" not found'}), 404 + return jsonify({'error': _('Emoji pack "{}" not found').format(pack_name)}), 404 try: for json_file in emojis_path.glob(f'{pack_name.lower()}.json'): json_file.unlink() shutil.rmtree(emoji_base_path) - return jsonify({'message': f'Emoji pack "{pack_name.capitalize()}" deleted successfully.'}), 200 + return jsonify({'message': _('Emoji pack "{}" deleted successfully.').format(pack_name)}), 200 except Exception as e: return jsonify({'error': str(e)}), 500 @@ -760,10 +760,10 @@ def deleteExport(): def viewQuestion(): question_id = request.args.get('question_id', '') if not question_id: - abort(400, "Missing 'question_id' attribute or 'question_id' is empty") + abort(400, _("Missing 'question_id' attribute or 'question_id' is empty")) conn = func.connectToDb() - cursor = conn.cursor(dictionary=True) + cursor = conn.cursor() cursor.execute("SELECT id, from_who, creation_date, content, answered, answer_id FROM questions WHERE id=%s", (question_id,)) question = cursor.fetchone() cursor.close() @@ -775,10 +775,10 @@ def viewQuestion(): def viewAnswer(): answer_id = request.args.get('answer_id', '') if not answer_id: - abort(400, "Missing 'answer_id' attribute or 'answer_id' is empty") + abort(400, _("Missing 'answer_id' attribute or 'answer_id' is empty")) conn = func.connectToDb() - cursor = conn.cursor(dictionary=True) + cursor = conn.cursor() cursor.execute("SELECT id, question_id, creation_date, content FROM answers WHERE id=%s", (answer_id,)) answer = cursor.fetchone() cursor.close() @@ -809,7 +809,9 @@ def updateConfig(): func.saveJSON(cfg, const.configFile) app.config.update(cfg) - return {'message': 'Settings saved!'} + # loading the config file again to read new values + func.loadJSON(const.configFile) + return {'message': _("Settings saved!")} app.register_blueprint(api_bp, url_prefix='/api/v1') app.register_blueprint(admin_bp, url_prefix='/admin') diff --git a/functions.py b/functions.py index 4af067c..d3bc37c 100644 --- a/functions.py +++ b/functions.py @@ -24,6 +24,25 @@ import constants as const app = Flask(const.appName) +app.config['BABEL_DEFAULT_LOCALE'] = 'en' +app.config['BABEL_TRANSLATION_DIRECTORIES'] = 'locales' +# refreshing locale +refresh() + +# update this once more languages are supported +app.config['available_languages'] = { + "en_US": _("English (US)"), + "ru_RU": _("Russian") +} + +def getLocale(): + if not session.get('language'): + app.config.update(cfg) + session['language'] = cfg['languages']['default'] + return session.get('language') + +babel = Babel(app, locale_selector=getLocale) + # load json file def loadJSON(file_path): # open the file @@ -197,7 +216,7 @@ def addQuestion(from_who, question, cw, noAntispam=False): antispam_valid = antispam in antispam_wordlist if not antispam_valid: # return a generic error message so bad actors wouldn't figure out the antispam list - return {'error': 'An error has occurred'}, 500 + return {'error': _('An error has occurred')}, 500 # it's probably bad to hardcode the siteverify urls, but meh, that will do for now elif cfg['antispam']['type'] == 'recaptcha': r = requests.post( diff --git a/templates/admin/base.html b/templates/admin/base.html index 397531d..1f4f1c8 100644 --- a/templates/admin/base.html +++ b/templates/admin/base.html @@ -1,9 +1,10 @@ {% extends 'base.html' %} -{% block title %}{% block _title %}{% endblock %} - Admin{% endblock %} +{% block title %}{% block _title %}{% endblock %} - {{ _('Admin') }}{% endblock %} {% set adminLink = 'active' %} {% block additionalHeadItems %} +{% block _additionalHeadItems %}{% endblock %} +{% endblock %} {% block _content %} -
Customize {{ const.appName }} to your liking
-{{ _('Customize {} to your liking').format(const.appName) }}
+Import or export your {{ const.appName }} instance data
-{{ _('Import or export your {} instance data').format(const.appName) }}
+{{ _('Note') }}
++ {{ _('Please note that import may take a while, depending on your database size') }} +
+Note: Retrospring exports are not supported yet
+{{ _('Note: Retrospring exports are not supported yet') }}
Date | -Actions | +{{ _('Date') }} | +{{ _('Actions') }} |
---|---|---|---|
{{ export.timestamp }} | - Download - + {{ _('Download') }} + |
No exports created yet
+{{ _('No exports created yet') }}
{% endif %} {% endblock %} diff --git a/templates/admin/categories/instance.html b/templates/admin/categories/instance.html index 46bad80..5ddb4f2 100644 --- a/templates/admin/categories/instance.html +++ b/templates/admin/categories/instance.html @@ -1,47 +1,41 @@ {% extends 'admin/base.html' %} -{% block _title %}Information{% endblock %} +{% block _title %}{{ _('Information') }}{% endblock %} {% set info_link = 'active' %} {% block _content %}Essential information about your {{ const.appName }} instance
+{{ _('Essential information about your {} instance').format(const.appName) }}
Title of this CatAsk instance
+{{ _('Title of this {} instance').format(const.appName) }}
Description of this CatAsk instance
+{{ _('Description of this {} instance').format(const.appName) }}
Rules of this CatAsk instance
+{{ _('Rules of this {} instance').format(const.appName) }}
Image that's going to be used in a link preview
+ + +{{ _("Image that's going to be used in a link preview") }}
Full URL to homepage of this CatAsk instance without a trailing slash
-{{ _('Full URL to homepage of this {} instance without a trailing slash').format(const.appName) }}
Configure notifications for new questions using ntfy
+{{ _('Configure notifications for new questions using') }} ntfy
Server & Topic
+{{ _('Server & Topic') }}
Set credentials if the topic is protected
+{{ _('Set credentials if the topic is protected') }}
- Topic user + {{ _('Topic user') }}
- Topic password + {{ _('Topic password') }}