babel stuff

This commit is contained in:
mst 2025-02-28 07:14:24 +03:00
parent 38b012c276
commit cb22ef5c23
No known key found for this signature in database
14 changed files with 261 additions and 236 deletions

56
app.py
View file

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

View file

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

View file

@ -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 %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/toastify.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/coloris.min.css') }}">
{% block _additionalHeadItems %}{% endblock %}
<style>
[data-bs-theme=dark] .form-switch .form-check-input:focus:not(:checked) {
--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23{{ cfg.style.accentDark[1:] }}'/%3e%3c/svg%3e");
@ -39,39 +40,43 @@
<div id="sidebar" class="sticky-lg-top pt-2">
<a class="ps-3 btn btn-outline-secondary my-1 fs-6 scale-parent {{ info_link }} d-flex align-items-center" href="{{ url_for('admin.information') }}">
<i class="bi bi-card-text fs-5 scale-child"></i>
<span class="sidebar-btn-text ms-2 ps-1">Information</span>
<span class="sidebar-btn-text ms-2 ps-1">{{ _('Information') }}</span>
</a>
<a class="ps-3 btn btn-outline-secondary my-1 fs-6 scale-parent {{ access_link }} d-flex align-items-center" href="{{ url_for('admin.accessibility') }}">
<i class="bi bi-universal-access fs-5 scale-child"></i>
<span class="sidebar-btn-text ms-2 ps-1">Accessibility</span>
<span class="sidebar-btn-text ms-2 ps-1">{{ _('Accessibility') }}</span>
</a>
<a class="ps-3 btn btn-outline-secondary my-1 fs-6 scale-parent {{ langs_link }} d-flex align-items-center" href="{{ url_for('admin.languages') }}">
<i class="bi bi-translate fs-5 scale-child"></i>
<span class="sidebar-btn-text ms-2 ps-1">{{ _('Languages') }}</span>
</a>
<a class="ps-3 btn btn-outline-secondary my-1 fs-6 scale-parent {{ notif_link }} d-flex align-items-center" href="{{ url_for('admin.notifications') }}">
<i class="bi bi-bell fs-5 scale-child"></i>
<span class="sidebar-btn-text ms-2 ps-1">Notifications</span>
<span class="sidebar-btn-text ms-2 ps-1">{{ _('Notifications') }}</span>
</a>
<a class="ps-3 btn btn-outline-secondary my-1 fs-6 scale-parent {{ custom_link }} d-flex align-items-center" href="{{ url_for('admin.customize') }}">
<i class="bi bi-columns-gap fs-5 scale-child"></i>
<span class="sidebar-btn-text ms-2 ps-1">Customize</span>
<span class="sidebar-btn-text ms-2 ps-1">{{ _('Customize') }}</span>
</a>
<a class="ps-3 btn btn-outline-secondary my-1 fs-6 scale-parent {{ antispam_link }} d-flex align-items-center" href="{{ url_for('admin.antispam') }}">
<i class="bi bi-shield fs-5 scale-child"></i>
<span class="sidebar-btn-text ms-2 ps-1">Anti-spam</span>
<span class="sidebar-btn-text ms-2 ps-1">{{ _('Anti-spam') }}</span>
</a>
<a class="ps-3 btn btn-outline-secondary my-1 fs-6 scale-parent {{ general_link }} d-flex align-items-center" href="{{ url_for('admin.general') }}">
<i class="bi bi-gear fs-5 scale-child"></i>
<span class="sidebar-btn-text ms-2 ps-1">General</span>
<span class="sidebar-btn-text ms-2 ps-1">{{ _('General') }}</span>
</a>
<a class="ps-3 btn btn-outline-secondary my-1 fs-6 scale-parent {{ emojis_link }} d-flex align-items-center" href="{{ url_for('admin.emojis') }}">
<i class="bi bi-emoji-smile fs-5 scale-child"></i>
<span class="sidebar-btn-text ms-2 ps-1">Emojis</span>
<span class="sidebar-btn-text ms-2 ps-1">{{ _('Emojis') }}</span>
</a>
<a class="ps-3 btn btn-outline-secondary my-1 fs-6 scale-parent {{ import_link }} d-flex align-items-center" href="{{ url_for('admin.importExport') }}">
<i class="bi bi-box-seam fs-5 scale-child"></i>
<span class="sidebar-btn-text ms-2 ps-1">Import/Export</span>
<span class="sidebar-btn-text ms-2 ps-1">{{ _('Import/Export') }}</span>
</a>
<a class="ps-3 btn btn-outline-secondary my-1 fs-6 scale-parent {{ blacklist_link }} d-flex align-items-center" href="{{ url_for('admin.blacklist') }}">
<i class="bi bi-ban fs-5 scale-child"></i>
<span class="sidebar-btn-text ms-2 ps-1">Word blacklist</span>
<span class="sidebar-btn-text ms-2 ps-1">{{ _('Word blacklist') }}</span>
</a>
<hr class="d-block d-md-none mt-4">
</div>

View file

@ -1,19 +1,20 @@
{% extends 'admin/base.html' %}
{% block _title %}Accessibility{% endblock %}
{% block _title %}{{ _('Accessibility') }}{% endblock %}
{% set access_link = 'active' %}
{% block _content %}
<form hx-post="{{ url_for('api.updateConfig') }}" hx-target="#response-container" hx-swap="none">
<h2 id="general" class="mb-2 fw-normal">Accessibility</h2>
<p class="fs-5 h3 text-body-secondary mb-3">Make {{ const.appName }} accessible to everyone</p>
<h2 id="general" class="mb-2 fw-normal">{{ _('Accessibility') }}</h2>
<p class="fs-5 h3 text-body-secondary mb-3">{{ _('Make {} accessible to everyone').format(const.appName) }}</p>
<div class="form-group mb-4">
<label class="form-label" for="accessibility.font">Font</label>
<label class="form-label" for="accessibility.font">{{ _('Font') }}</label>
<select id="accessibility.font" name="accessibility.font" class="form-select">
<option value="default"{% if cfg.accessibility.font == 'default' %} selected{% endif %}>Default</option>
<option value="system"{% if cfg.accessibility.font == 'system' %} selected{% endif %}>System</option>
<option value="atkinson"{% if cfg.accessibility.font == 'atkinson' %} selected{% endif %}>Atkinson Hyperlegible</option>
<option value="default"{% if cfg.accessibility.font == 'default' %} selected{% endif %}>{{ _('Default') }}</option>
<option value="system"{% if cfg.accessibility.font == 'system' %} selected{% endif %}>{{ _('System') }}</option>
<option value="atkinson"{% if cfg.accessibility.font == 'atkinson' %} selected{% endif %}>{{ _('Atkinson Hyperlegible') }}</option>
</select>
</div>
<h3 id="antispam" class="mb-2 fw-normal d-flex align-items-center gap-2">UserWay <a href="https://userway.org/" target="_blank" class="fs-5" title="what's this?"><i class="bi bi-question-circle"></i></a></h3>
{#- i dont think userway should be translated since its a company name and those usually arent translated #}
<h3 id="antispam" class="mb-2 fw-normal d-flex align-items-center gap-2">UserWay <a href="https://userway.org/" target="_blank" class="fs-5" title="{{ _("what's this?") }}"><i class="bi bi-question-circle"></i></a></h3>
<div class="form-check form-switch mb-3">
<input
class="form-check-input"
@ -24,22 +25,16 @@
role="switch"
{% if cfg.accessibility.userway.enabled %}checked{% endif %}>
<input type="hidden" id="accessibility.userway.enabled" name="accessibility.userway.enabled" value="{{ cfg.accessibility.userway.enabled }}">
<label for="_accessibility.userway.enabled" class="form-check-label">Enabled</label>
<label for="_accessibility.userway.enabled" class="form-check-label">{{ _('Enabled') }}</label>
</div>
<div class="form-group mb-3">
<label class="form-label" for="accessibility.userway.account">Account key</label>
<label class="form-label" for="accessibility.userway.account">{{ _('Account key') }}</label>
<input type="text" id="accessibility.userway.account" name="accessibility.userway.account" value="{{ cfg.accessibility.userway.account }}" class="form-control">
<p class="form-text">
UserWay account key, find one at <a href="https://manage.userway.org/embed-code" target="_blank">manage.userway.org/embed-code</a>
{{ _('UserWay account key, find one at') }} <a href="https://manage.userway.org/embed-code" target="_blank">manage.userway.org/embed-code</a>
</p>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary mt-3" id="saveConfig">
<span class="spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span>
<span class="visually-hidden" role="status">Loading...</span>
Save
</button>
</div>
{% include 'snippets/admin/saveBtn.html' %}
</form>
{% endblock %}
{% block _scripts %}

View file

@ -1,10 +1,10 @@
{% extends 'admin/base.html' %}
{% block _title %}Anti-spam{% endblock %}
{% block _title %}{{ _('Anti-spam') }}{% endblock %}
{% set antispam_link = 'active' %}
{% block _content %}
<form hx-post="{{ url_for('api.updateConfig') }}" hx-target="#response-container" hx-swap="none">
<h2 id="antispam" class="mb-2 fw-normal">Anti-spam</h2>
<p class="fs-5 h3 text-body-secondary mb-3">Protect your {{ const.appName }} instance from spammers</p>
<h2 id="antispam" class="mb-2 fw-normal">{{ _('Anti-spam') }}</h2>
<p class="fs-5 h3 text-body-secondary mb-3">{{ _('Protect your {} instance from spammers').format(const.appName) }}</p>
<div class="form-check form-switch mb-3">
<input
class="form-check-input"
@ -15,73 +15,68 @@
role="switch"
{% if cfg.antispam.enabled %}checked{% endif %}>
<input type="hidden" id="antispam.enabled" name="antispam.enabled" value="{{ cfg.antispam.enabled }}">
<label for="_antispam.enabled" class="form-check-label">Enabled</label>
<label for="_antispam.enabled" class="form-check-label">{{ _('Enabled') }}</label>
</div>
{% if not cfg.antispam.enabled %}
<div class="alert alert-warning border-0 small" role="alert">
<p class="m-0 d-flex align-items-center gap-2 fw-medium"><i class="bi bi-exclamation-circle fs-5"></i> Warning</p>
<p class="m-0 d-flex align-items-center gap-2 fw-medium"><i class="bi bi-exclamation-circle fs-5"></i> {{ _('Warning') }}</p>
<p class="m-0">
It's highly encouraged to keep anti-spam enabled, otherwise you could become a target of a spam attack.<br><br>
If you don't want your visitors to worry about completing a CAPTCHA, some providers have "non-interactive" and "invisible" CAPTCHA variants.
{{ _("It's highly encouraged to keep anti-spam enabled, otherwise you could become a target of a spam attack.") }}<br><br>
{#- i hate quotes and their issues #}
{{ _("If you don't want your visitors to worry about completing a CAPTCHA, some providers have 'non-interactive' and 'invisible' CAPTCHA variants.") }}
</p>
</div>
{% endif %}
<div class="form-group mb-3{% if not cfg.antispam.enabled %}d-none{% endif %}">
<label class="form-label" for="antispam.type">Anti-spam type</label>
<label class="form-label" for="antispam.type">{{ _('Anti-spam type') }}</label>
<select id="antispam.type" name="antispam.type" class="form-select" onchange="checkForRecaptcha(this)">
<option value="basic"{% if cfg.antispam.type == 'basic' %} selected{% endif %}>Basic</option>
<option value="basic"{% if cfg.antispam.type == 'basic' %} selected{% endif %}>{{ _('Basic') }}</option>
<option value="turnstile"{% if cfg.antispam.type == 'turnstile' %} selected{% endif %}>Cloudflare Turnstile</option>
<option value="frc"{% if cfg.antispam.type == 'frc' %} selected{% endif %}>Friendly Captcha</option>
<option value="recaptcha"{% if cfg.antispam.type == 'recaptcha' %} selected{% endif %}>reCAPTCHA v2</option>
</select>
<p class="form-text">Anti-spam type to use</p>
<p class="form-text">{{ _('Anti-spam type to use') }}</p>
</div>
<div id="recaptcha-options"{% if cfg.antispam.type != 'recaptcha' %} class="d-none"{% endif %}>
<h3 id="recaptcha" class="mb-3 fw-light">reCAPTCHA options</h3>
<h3 id="recaptcha" class="mb-3 fw-light">{{ _('reCAPTCHA options') }}</h3>
<div class="form-group mb-3">
<label class="form-label" for="antispam.recaptcha.sitekey">Site key</label>
<label class="form-label" for="antispam.recaptcha.sitekey">{{ _('Site key') }}</label>
<input type="text" id="antispam.recaptcha.sitekey" name="antispam.recaptcha.sitekey" value="{{ cfg.antispam.recaptcha.sitekey }}" class="form-control">
<p class="form-text">reCAPTCHA site key</p>
<p class="form-text">{{ _('reCAPTCHA site key') }}</p>
</div>
<div class="form-group mb-3">
<label class="form-label" for="antispam.recaptcha.secretkey">Secret key</label>
<label class="form-label" for="antispam.recaptcha.secretkey">{{ _('Secret key') }}</label>
<input type="password" id="antispam.recaptcha.secretkey" name="antispam.recaptcha.secretkey" value="{{ cfg.antispam.recaptcha.secretkey }}" class="form-control">
<p class="form-text">reCAPTCHA secret key</p>
<p class="form-text">{{ _('reCAPTCHA secret key') }}</p>
</div>
</div>
<div id="turnstile-options"{% if cfg.antispam.type != 'turnstile' %} class="d-none"{% endif %}>
<h3 id="turnstile" class="mb-3 fw-light">Turnstile options</h3>
<h3 id="turnstile" class="mb-3 fw-light">{{ _('Turnstile options') }}</h3>
<div class="form-group mb-3">
<label class="form-label" for="antispam.turnstile.sitekey">Site key</label>
<label class="form-label" for="antispam.turnstile.sitekey">{{ _('Site key') }}</label>
<input type="text" id="antispam.turnstile.sitekey" name="antispam.turnstile.sitekey" value="{{ cfg.antispam.turnstile.sitekey }}" class="form-control">
<p class="form-text">Turnstile site key</p>
<p class="form-text">{{ _('Turnstile site key') }}</p>
</div>
<div class="form-group mb-3">
<label class="form-label" for="antispam.turnstile.secretkey">Secret key</label>
<label class="form-label" for="antispam.turnstile.secretkey">{{ _('Secret key') }}</label>
<input type="password" id="antispam.turnstile.secretkey" name="antispam.turnstile.secretkey" value="{{ cfg.antispam.turnstile.secretkey }}" class="form-control">
<p class="form-text">Turnstile secret key</p>
<p class="form-text">{{ _('Turnstile secret key') }}</p>
</div>
</div>
<div id="frc-options"{% if cfg.antispam.type != 'frc' %} class="d-none"{% endif %}>
<h3 id="frc" class="mb-3 fw-light">Friendly Captcha options</h3>
<h3 id="frc" class="mb-3 fw-light">{{ _('Friendly Captcha options') }}</h3>
<div class="form-group mb-3">
<label class="form-label" for="antispam.frc.sitekey">Site key</label>
<label class="form-label" for="antispam.frc.sitekey">{{ _('Site key') }}</label>
<input type="text" id="antispam.frc.sitekey" name="antispam.frc.sitekey" value="{{ cfg.antispam.frc.sitekey }}" class="form-control">
<p class="form-text">Friendly Captcha site key</p>
<p class="form-text">{{ _('Friendly Captcha site key') }}</p>
</div>
<div class="form-group mb-3">
<label class="form-label" for="antispam.frc.apikey">API key</label>
<label class="form-label" for="antispam.frc.apikey">{{ _('API key') }}</label>
<input type="password" id="antispam.frc.apikey" name="antispam.frc.apikey" value="{{ cfg.antispam.frc.apikey }}" class="form-control">
<p class="form-text">Friendly Captcha API key</p>
<p class="form-text">{{ _('Friendly Captcha API key') }}</p>
</div>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary mt-3" id="saveConfig">
<span class="spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span>
<span class="visually-hidden" role="status">Loading...</span>
Save
</button>
</div>
{% include 'snippets/admin/saveBtn.html' %}
</form>
{% endblock %}
{% block _scripts %}

View file

@ -1,20 +1,16 @@
{% extends 'admin/base.html' %}
{% block _title %}Blacklist{% endblock %}
{% block _title %}{{ _('Word blacklist') }}{% endblock %}
{% set blacklist_link = 'active' %}
{% block _content %}
<h2 id="blacklist" class="fw-normal mb-2">Word blacklist</h2>
<p class="fs-5 h3 text-body-secondary mb-3">Blacklist words that you don't want to see</p>
<h2 id="blacklist" class="fw-normal mb-2">{{ _('Word blacklist') }}</h2>
<p class="fs-5 h3 text-body-secondary mb-3">{{ _("Blacklist words that you don't want to see") }}</p>
<form hx-put="{{ url_for('api.updateBlacklist') }}" hx-target="#response-container" hx-swap="none">
<input type="hidden" name="action" value="update_word_blacklist">
<div class="form-group mb-3">
<label class="form-label" for="blacklist_cat">Blacklisted words for questions, one word per line</label>
<label class="form-label" for="blacklist_cat">{{ _('Blacklisted words for questions, one word per line') }}</label>
<!-- <p class="text-body-secondary">Blacklisted words for questions; one word per line</p> -->
<textarea id="blacklist_cat" name="blacklist" style="height: 300px; resize: vertical;" class="form-control">{{ blacklist }}</textarea>
<button type="submit" class="btn btn-primary mt-3" id="save-blacklist">
<span class="me-1 spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span>
<span class="visually-hidden" role="status">Loading...</span>
Save
</button>
{% include 'snippets/admin/saveBtn.html' %}
</div>
</form>
{% endblock %}

View file

@ -1,36 +1,61 @@
{% extends 'admin/base.html' %}
{% block _title %}Customize{% endblock %}
{% block _title %}{{ _('Customize') }}{% endblock %}
{% set custom_link = 'active' %}
{% block _additionalHeadItems %}
<style>
.cm-content, .cm-gutter {
min-height: 200px !important;
}
.cm-editor, .cm-scroller {
border-radius: var(--bs-border-radius);
}
.cm-editor {
font-size: 14px;
}
.ͼ1.cm-focused {
box-shadow: 0 0 0 .25rem color-mix(in srgb, var(--bs-primary) 25%, transparent);
outline: 0 !important;
}
#settings-dropdown {
min-width: 25rem;
}
@media screen and (max-width: 600px) {
#settings-dropdown {
min-width: 100vw;
}
}
</style>
{% endblock %}
{% block _content %}
<h2 id="customize" class="mb-2 fw-normal">Customize</h2>
<p class="fs-5 h3 text-body-secondary mb-3">Customize {{ const.appName }} to your liking</p>
<h3 class="fw-light">Favicon</h3>
<h2 id="customize" class="mb-2 fw-normal">{{ _('Customize') }}</h2>
<p class="fs-5 h3 text-body-secondary mb-3">{{ _('Customize {} to your liking').format(const.appName) }}</p>
<h3 class="fw-light">{{ _('Favicon') }}</h3>
<form hx-post="{{ url_for('api.uploadFavicon') }}" hx-target="#response-container" hx-swap="none" hx-encoding="multipart/form-data">
<p class="m-0">Current favicon: <img src="{{ url_for('static', filename='icons/favicon/apple-touch-icon.png') }}" width="32" height="32" alt="{{ cfg.instance.title }}'s icon" class="rounded"></p>
<p class="m-0">{{ _('Current favicon:') }} <img src="{{ url_for('static', filename='icons/favicon/apple-touch-icon.png') }}" width="32" height="32" alt="{{ cfg.instance.title }}'s icon" class="rounded"></p>
<div class="mt-2">
<label for="favicon" class="form-label">Upload favicon</label>
<label for="favicon" class="form-label">{{ _('Upload favicon') }}</label>
<input class="form-control" type="file" id="favicon" name="favicon">
</div>
<div class="mb-4">
<button type="submit" class="btn btn-primary mt-3" id="saveFavicon">
<span class="spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span>
<span class="visually-hidden" role="status">Loading...</span>
<i class="bi bi-file-earmark-arrow-up me-1"></i> Upload
<span class="visually-hidden" role="status">{{ _('Loading...') }}</span>
<i class="bi bi-file-earmark-arrow-up me-1"></i> {{ _('Upload') }}
</button>
</div>
</form>
<form hx-post="{{ url_for('api.updateConfig') }}" hx-target="#response-container" hx-swap="none">
<h3 class="fw-light">Accent color</h3>
<form hx-post="{{ url_for('api.updateConfig') }}" hx-target="#response-container" hx-swap="none" hx-on::before-request="setCmTextareaValue()">
<h3 class="fw-light">{{ _('Accent color') }}</h3>
{# <h4 class="fw-light">Color</h4> #}
<div class="form-group d-flex flex-column">
<label class="form-label" for="style.accentLight">Light theme</label>
<input type="text" name="style.accentLight" id="style.accentLight" value="{{ cfg.style.accentLight }}" class="form-control" data-coloris title="Click to open the color picker">
<p class="form-text">Default: <b>#6345d9</b></p>
<label class="form-label" for="style.accentLight">{{ _('Light theme') }}</label>
<input type="text" name="style.accentLight" id="style.accentLight" value="{{ cfg.style.accentLight }}" class="form-control" data-coloris title="{{ _('Click to open the color picker') }}">
<p class="form-text">{{ _('Default') }}: <b>#6345d9</b></p>
</div>
<div class="form-group d-flex flex-column">
<label class="form-label" for="style.accentDark">Dark theme</label>
<input type="text" name="style.accentDark" id="style.accentDark" value="{{ cfg.style.accentDark }}" class="form-control" data-coloris title="Click to open the color picker">
<p class="form-text">Default: <b>#7a63e3</b></p>
<label class="form-label" for="style.accentDark">{{ _('Dark theme') }}</label>
<input type="text" name="style.accentDark" id="style.accentDark" value="{{ cfg.style.accentDark }}" class="form-control" data-coloris title="{{ _('Click to open the color picker') }}">
<p class="form-text">{{ _('Default') }}: <b>#7a63e3</b></p>
</div>
{# brain doesn't feel like implementing this rn (9/27/24)
<h4 class="fw-light mt-2">Background</h4>
@ -55,7 +80,7 @@
role="switch"
{% if cfg.style.tintColors == true %}checked{% endif %}>
<input type="hidden" id="style.tintColors" name="style.tintColors" value="{{ cfg.style.tintColors }}">
<label for="_style.tintColors" class="form-check-label">Tint all colors with accent color</label>
<label for="_style.tintColors" class="form-check-label">{{ _('Tint all colors with accent color') }}</label>
</div>
<div class="form-check form-switch mb-1">
<input
@ -67,9 +92,9 @@
role="switch"
{% if cfg.style.navIcons == true %}checked{% endif %}>
<input type="hidden" id="style.navIcons" name="style.navIcons" value="{{ cfg.style.navIcons }}">
<label for="_style.navIcons" class="form-check-label">Include icons in nav links</label>
<label for="_style.navIcons" class="form-check-label">{{ _('Include icons in nav links') }}</label>
</div>
<p class="form-text mb-2"><b>Note:</b> on mobile screens text will always be hidden if this option is enabled</p>
<p class="form-text mb-2"><b>{{ _('Note') }}:</b> {{ _('on mobile screens text will always be hidden if this option is enabled') }}</p>
<div class="form-check form-switch mb-4 ms-3">
<input
class="form-check-input nav-icons-only-checkbox"
@ -80,7 +105,7 @@
role="switch"
{% if cfg.style.navIconsOnly == true %}checked{% endif %}>
<input type="hidden" id="style.navIconsOnly" name="style.navIconsOnly" value="{{ cfg.style.navIconsOnly }}">
<label for="_style.navIconsOnly" class="form-check-label">Include <b>only</b> icons in nav links</label>
<label for="_style.navIconsOnly" class="form-check-label">{{ _('Include only icons in nav links') }}</label>
</div>
{#
<div class="form-check form-switch mb-3">

View file

@ -1,49 +1,49 @@
{% extends 'admin/base.html' %}
{% block _title %}Emojis{% endblock %}
{% block _title %}{{ _('Emojis') }}{% endblock %}
{% set emojis_link = 'active' %}
{% block _content %}
<h2 id="customEmojis" class="mb-2 fw-normal">Custom emojis</h2>
<p class="fs-5 h3 text-body-secondary mb-3">Add custom emojis to your {{ const.appName }} instance</p>
<h3 class="fw-light mb-3">Upload</h3>
<h2 id="customEmojis" class="mb-2 fw-normal">{{ _('Custom emojis') }}</h2>
<p class="fs-5 h3 text-body-secondary mb-3">{{ _('Add custom emojis to your {} instance').format(const.appName) }}</p>
<h3 class="fw-light mb-3">{{ _('Upload') }}</h3>
<form hx-post="{{ url_for('api.uploadEmoji') }}" hx-target="#response-container" hx-swap="none" hx-encoding="multipart/form-data">
<div>
<label for="emoji" class="form-label">Upload an emoji</label>
<label for="emoji" class="form-label">{{ _('Upload an emoji') }}</label>
<input class="form-control" type="file" id="emoji" name="emoji">
</div>
<div class="mb-4">
<button type="submit" class="btn btn-primary mt-3" id="uploadEmoji">
<span class="spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span>
<span class="visually-hidden" role="status">Loading...</span>
<i class="bi bi-file-earmark-plus me-1"></i> Upload emoji
<span class="visually-hidden" role="status">{{ _('Loading...') }}</span>
<i class="bi bi-file-earmark-plus me-1"></i> {{ _('Upload emoji') }}
</button>
</div>
</form>
<hr>
<form id="pack-form" hx-post="{{ url_for('api.uploadEmojiPack') }}" hx-target="#response-container" hx-swap="none" hx-encoding="multipart/form-data">
<div>
<label for="emoji_archive" class="form-label">Upload emoji pack</label>
<label for="emoji_archive" class="form-label">{{ _('Upload emoji pack') }}</label>
<input class="form-control" type="file" id="emoji_archive" name="emoji_archive">
<p class="form-text mb-0">Supported archive formats: <strong>.zip, .tar, .tar.gz, .tar.bz2, .tar.xz</strong></p>
<p class="form-text mb-0">{{ _('Supported archive formats:') }} <strong>.zip, .tar, .tar.gz, .tar.bz2, .tar.xz</strong></p>
</div>
<div class="mb-5">
<button type="submit" class="btn btn-primary mt-3" id="uploadEmojiPack">
<span class="spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span>
<span class="visually-hidden" role="status">Loading...</span>
<i class="bi bi-file-earmark-zip me-1"></i> Upload pack
<span class="visually-hidden" role="status">{{ _('Loading...') }}</span>
<i class="bi bi-file-earmark-zip me-1"></i> {{ _('Upload pack') }}
</button>
</div>
</form>
<h3 class="fw-light mb-3">Manage</h3>
<h4 class="fw-light">Emojis</h4>
<h3 class="fw-light mb-3">{{ _('Manage') }}</h3>
<h4 class="fw-light">{{ _('Emojis') }}</h4>
{% if emojis %}
<div class="table-responsive">
<table id="emojiTable" class="table align-middle">
<thead>
<tr>
<th>Image</th>
<th>Name</th>
<th>Filename</th>
<th>Actions</th>
<th>{{ _('Image') }}</th>
<th>{{ _('Name') }}</th>
<th>{{ _('Filename') }}</th>
<th>{{ _('Actions') }}</th>
</tr>
</thead>
<tbody id="emojis">
@ -54,23 +54,23 @@
</table>
</div>
{% else %}
<p class="text-center">No emojis uploaded</p>
<p class="text-center">{{ _('No emojis uploaded') }}</p>
{% endif %}
<h4 class="fw-light mt-3">Emoji packs</h4>
<p class="text-body-secondary small">Please note that if meta.json is not found in the archive, preview images are selected by first emoji name to appear alphabetically, so they may not be accurate sometimes</p>
<h4 class="fw-light mt-3">{{ _('Emoji packs') }}</h4>
<p class="text-body-secondary small">{{ _('Please note that if meta.json is not found in the archive, preview images are selected by first emoji name to appear alphabetically, so they may not be accurate sometimes') }}</p>
{% if packs %}
<div class="table-responsive">
<table id="emojiPackTable" class="table align-middle">
<thead>
<tr>
<th>Image</th>
<th>Name</th>
<th>{{ _('Image') }}</th>
<th>{{ _('Name') }}</th>
{% if json_pack %}
<th>Author</th>
<th>Released</th>
<th>{{ _('Author') }}</th>
<th>{{ _('Released') }}</th>
{% endif %}
<th>Folder</th>
<th>Actions</th>
<th>{{ _('Folder') }}</th>
<th>{{ _('Actions') }}</th>
</tr>
</thead>
<tbody id="packs">
@ -83,6 +83,6 @@
</table>
</div>
{% else %}
<p class="text-center">No emoji packs uploaded</p>
<p class="text-center">{{ _('No emoji packs uploaded') }}</p>
{% endif %}
{% endblock %}

View file

@ -25,7 +25,7 @@
role="switch"
{% if cfg.lockInbox %}checked{% endif %}>
<input type="hidden" id="lockInbox" name="lockInbox" value="{{ cfg.lockInbox }}">
<label for="_lockInbox" class="form-check-label">Lock inbox and don't allow new questions</label>
<label for="_lockInbox" class="form-check-label">{{ _("Lock inbox and don't allow new questions") }}</label>
</div>
<div class="form-check form-switch mb-2">
<input
@ -37,7 +37,7 @@
role="switch"
{% if cfg.allowAnonQuestions %}checked{% endif %}>
<input type="hidden" id="allowAnonQuestions" name="allowAnonQuestions" value="{{ cfg.allowAnonQuestions }}">
<label for="_allowAnonQuestions" class="form-check-label">Allow anonymous questions</label>
<label for="_allowAnonQuestions" class="form-check-label">{{ _('Allow anonymous questions') }}</label>
</div>
<div class="form-check form-switch mb-2">
<input
@ -49,7 +49,7 @@
role="switch"
{% if cfg.noDeleteConfirm %}checked{% endif %}>
<input type="hidden" id="noDeleteConfirm" name="noDeleteConfirm" value="{{ cfg.noDeleteConfirm }}">
<label for="_noDeleteConfirm" class="form-check-label">Disable confirmation when deleting questions</label>
<label for="_noDeleteConfirm" class="form-check-label">{{ _('Disable confirmation when deleting questions') }}</label>
</div>
<div class="form-check form-switch mb-3">
<input
@ -61,15 +61,9 @@
role="switch"
{% if cfg.showQuestionCount %}checked{% endif %}>
<input type="hidden" id="showQuestionCount" name="showQuestionCount" value="{{ cfg.showQuestionCount }}">
<label for="_showQuestionCount" class="form-check-label">Show question count in homepage</label>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary mt-3" id="saveConfig">
<span class="spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span>
<span class="visually-hidden" role="status">Loading...</span>
Save
</button>
<label for="_showQuestionCount" class="form-check-label">{{ _('Show question count in homepage') }}</label>
</div>
{% include 'snippets/admin/saveBtn.html' %}
</form>
{% endblock %}
{% block _scripts %}

View file

@ -1,20 +1,26 @@
{% extends 'admin/base.html' %}
{% block _title %}Import/Export{% endblock %}
{% block _title %}{{ _('Import/Export') }}{% endblock %}
{% set import_link = 'active' %}
{% block _content %}
<h2 id="general" class="mb-2 fw-normal">Import/Export</h2>
<p class="fs-5 h3 text-body-secondary mb-3">Import or export your {{ const.appName }} instance data</p>
<h3 id="import" class="mb-3 fw-light">Import</h3>
<h2 id="general" class="mb-2 fw-normal">{{ _('Import/Export') }}</h2>
<p class="fs-5 h3 text-body-secondary mb-3">{{ _('Import or export your {} instance data').format(const.appName) }}</p>
<h3 id="import" class="mb-3 fw-light">{{ _('Import') }}</h3>
<div class="alert alert-info border-0 small" role="alert">
<p class="m-0 d-flex align-items-center gap-2 fw-medium"><i class="bi bi-info-circle fs-5"></i> {{ _('Note') }}</p>
<p class="m-0">
{{ _('Please note that import may take a while, depending on your database size') }}
</p>
</div>
<form class="mb-2" hx-disabled-elt="find button[type=submit]" hx-put="{{ url_for('api.importData') }}" hx-encoding="multipart/form-data" hx-target="#response-container" hx-swap="none">
<div class="form-group mb-3">
<label class="form-label" for="import_archive">Import data</label>
<label class="form-label" for="import_archive">{{ _('Import data') }}</label>
<input class="form-control" type="file" id="import_archive" name="import_archive" required>
<p class="form-text">Note: Retrospring exports are not supported yet</p>
<p class="form-text">{{ _('Note: Retrospring exports are not supported yet') }}</p>
</div>
<button type="submit" class="btn btn-primary mt-2" id="import-data">
<span class="me-1 spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span>
<span class="visually-hidden" role="status">Loading...</span>
<i class="bi bi-database-up me-1"></i> Import
<span class="visually-hidden" role="status">{{ _('Loading...') }}</span>
<i class="bi bi-database-up me-1"></i> {{ _('Import') }}
</button>
</form>
{#
@ -32,13 +38,13 @@
</button>
</form>
#}
<h3 id="export" class="mb-3 mt-5 fw-light">Export</h3>
<h3 id="export" class="mb-3 mt-5 fw-light">{{ _('Export') }}</h3>
<div class="form-group mb-3">
<form hx-post="{{ url_for('api.createExport') }}" hx-swap="none" hx-disabled-elt="#export-btn">
<button type="submit" class="btn btn-primary" id="export-btn">
<span class="spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span>
<span class="visually-hidden" role="status">Loading...</span>
<i class="bi bi-plus-lg me-1"></i> New Export
<span class="visually-hidden" role="status">{{ _('Loading...') }}</span>
<i class="bi bi-plus-lg me-1"></i> {{ _('New Export') }}
</button>
</form>
{% if exports %}
@ -46,8 +52,8 @@
<table class="table align-middle">
<thead>
<tr>
<th>Date</th>
<th>Actions</th>
<th>{{ _('Date') }}</th>
<th>{{ _('Actions') }}</th>
</tr>
</thead>
<tbody>
@ -55,8 +61,8 @@
<tr id="export-{{ export.timestamp_esc }}">
<td>{{ export.timestamp }}</td>
<td>
<a href="/{{ export.downloadPath }}" class="btn btn-secondary btn-sm px-3 px-md-2"><i class="bi bi-download"></i> <span class="d-none d-lg-inline-block ms-1">Download</span></a>
<button class="btn btn-outline-danger btn-sm px-3 px-md-2" hx-target="#export-{{ export.timestamp_esc }}" hx-swap="delete" hx-delete="{{ url_for('api.deleteExport', timestamp=export.timestamp_esc) }}"><i class="bi bi-trash"></i> <span class="d-none d-lg-inline-block ms-1">Delete</span></button>
<a href="/{{ export.downloadPath }}" class="btn btn-secondary btn-sm px-3 px-md-2"><i class="bi bi-download"></i> <span class="d-none d-lg-inline-block ms-1">{{ _('Download') }}</span></a>
<button class="btn btn-outline-danger btn-sm px-3 px-md-2" hx-target="#export-{{ export.timestamp_esc }}" hx-swap="delete" hx-delete="{{ url_for('api.deleteExport', timestamp=export.timestamp_esc) }}"><i class="bi bi-trash"></i> <span class="d-none d-lg-inline-block ms-1">{{ _('Delete') }}</span></button>
</td>
</tr>
{% endfor %}
@ -64,7 +70,7 @@
</table>
</div>
{% else %}
<p class="text-center my-3"><i>No exports created yet</i></p>
<p class="text-center my-3">{{ _('No exports created yet') }}</p>
{% endif %}
</div>
{% endblock %}

View file

@ -1,47 +1,41 @@
{% extends 'admin/base.html' %}
{% block _title %}Information{% endblock %}
{% block _title %}{{ _('Information') }}{% endblock %}
{% set info_link = 'active' %}
{% block _content %}
<form hx-post="{{ url_for('api.updateConfig') }}" hx-target="#response-container" hx-swap="none">
<h2 id="instance" class="mb-2 fw-normal">Information</h2>
<p class="fs-5 h3 text-body-secondary mb-3">Essential information about your {{ const.appName }} instance</p>
<h2 id="instance" class="mb-2 fw-normal">{{ _('Information') }}</h2>
<p class="fs-5 h3 text-body-secondary mb-3">{{ _('Essential information about your {} instance').format(const.appName) }}</p>
<div class="form-group mb-3">
<label class="form-label" for="instance.title">Title <small class="text-body-secondary">(e.g. My question box)</small></label>
<label class="form-label" for="instance.title">{{ _('Title') }} <small class="text-body-secondary">{{ _('(e.g. My question box)') }}</small></label>
<input type="text" id="instance.title" name="instance.title" value="{{ cfg.instance.title }}" class="form-control">
<p class="form-text">Title of this CatAsk instance</p>
<p class="form-text">{{ _('Title of this {} instance').format(const.appName) }}</p>
</div>
<div class="form-group mb-3">
<label class="form-label d-flex flex-column flex-lg-row align-items-lg-center justify-content-between" for="instance.description">
<span>Description <small class="text-body-secondary">(e.g. Ask me a question!)</small></span>
<small class="text-body-secondary"><i class="bi bi-markdown me-1"></i> Markdown supported</small>
<span>{{ _('Description') }} <small class="text-body-secondary">{{ _('(e.g. Ask me a question!)') }}</small></span>
<small class="text-body-secondary"><i class="bi bi-markdown me-1"></i> {{ _('Markdown supported') }}</small>
</label>
<textarea spellcheck="false" id="instance.description" name="instance.description" class="form-control" style="height: 200px; resize: vertical;">{{ cfg.instance.description }}</textarea>
<p class="form-text">Description of this CatAsk instance</p>
<p class="form-text">{{ _('Description of this {} instance').format(const.appName) }}</p>
</div>
<div class="form-group mb-3">
<label class="form-label d-flex flex-column flex-lg-row align-items-lg-center justify-content-between" for="instance.rules">
<span>Rules</span>
<small class="text-body-secondary"><i class="bi bi-markdown me-1"></i> Markdown supported</small>
<span>{{ _('Rules') }}</span>
<small class="text-body-secondary"><i class="bi bi-markdown me-1"></i> {{ _('Markdown supported') }}</small>
</label>
<textarea spellcheck="false" id="instance.rules" name="instance.rules" class="form-control" style="height: 200px; resize: vertical;">{{ cfg.instance.rules }}</textarea>
<p class="form-text">Rules of this CatAsk instance</p>
<p class="form-text">{{ _('Rules of this {} instance').format(const.appName) }}</p>
</div>
<div class="form-group mb-3">
<label class="form-label" for="instance.image">Relative image path <small class="text-body-secondary">(default: /static/icons/favicon/android-chrome-512x512.png)</small></label>
<input type="text" id="instance.image" name="instance.image" value="{{ cfg.instance.image }}" class="form-control">
<p class="form-text">Image that's going to be used in a link preview</p>
<label class="form-label" for="instance.image">{{ _('Relative image path') }}</label>
<input type="text" id="instance.image" name="instance.image" value="{{ cfg.instance.image }}" placeholder="/static/icons/favicon/android-chrome-512x512.png" class="form-control">
<p class="form-text">{{ _("Image that's going to be used in a link preview") }}</p>
</div>
<div class="form-group mb-2">
<label class="form-label" for="instance.fullBaseUrl">Base URL <small class="text-body-secondary">(e.g. https://ask.example.com)</small></label>
<input type="text" id="instance.fullBaseUrl" name="instance.fullBaseUrl" value="{{ cfg.instance.fullBaseUrl }}" class="form-control">
<p class="form-text">Full URL to homepage of this CatAsk instance without a trailing slash</p>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary" id="saveConfig">
<span class="spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span>
<span class="visually-hidden" role="status">Loading...</span>
Save
</button>
<label class="form-label" for="instance.fullBaseUrl">{{ _('Base URL') }}</label>
<input type="text" id="instance.fullBaseUrl" name="instance.fullBaseUrl" value="{{ cfg.instance.fullBaseUrl }}" placeholder="https://ask.example.com" class="form-control">
<p class="form-text">{{ _('Full URL to homepage of this {} instance without a trailing slash').format(const.appName) }}</p>
</div>
{% include 'snippets/admin/saveBtn.html' %}
</form>
{% endblock %}

View file

@ -1,10 +1,10 @@
{% extends 'admin/base.html' %}
{% block _title %}Notifications{% endblock %}
{% block _title %}{{ _('Notifications') }}{% endblock %}
{% set notif_link = 'active' %}
{% block _content %}
<form hx-post="{{ url_for('api.updateConfig') }}" hx-target="#response-container" hx-swap="none" hx-disabled-elt="#saveConfig">
<h2 id="general" class="mb-22 fw-normal">Notifications</h2>
<p class="fs-5 h3 text-body-secondary mb-3">Configure notifications for new questions using <a href="https://ntfy.sh/" target="_blank">ntfy</a></p>
<h2 id="general" class="mb-22 fw-normal">{{ _('Notifications') }}</h2>
<p class="fs-5 h3 text-body-secondary mb-3">{{ _('Configure notifications for new questions using') }} <a href="https://ntfy.sh/" target="_blank">ntfy</a></p>
<div class="form-check form-switch mb-3">
<input
class="form-check-input"
@ -15,37 +15,31 @@
role="switch"
{% if cfg.ntfy.enabled %}checked{% endif %}>
<input type="hidden" id="ntfy.enabled" name="ntfy.enabled" value="{{ cfg.ntfy.enabled }}">
<label for="_ntfy.enabled" class="form-check-label">Enabled</label>
<label for="_ntfy.enabled" class="form-check-label">{{ _('Enabled') }}</label>
</div>
<p class="form-label">Server &amp; Topic</p>
<p class="form-label">{{ _('Server &amp; Topic') }}</p>
<div class="input-group mb-4">
<input type="text" id="ntfy.host" name="ntfy.host" value="{{ cfg.ntfy.host }}" class="form-control" aria-label="Server">
<input type="text" id="ntfy.host" name="ntfy.host" value="{{ cfg.ntfy.host }}" class="form-control" aria-label="{{ _('Server') }}">
<span class="input-group-text">/</span>
<input type="text" id="ntfy.topic" name="ntfy.topic" value="{{ cfg.ntfy.topic }}" class="form-control" aria-label="Topic">
<input type="text" id="ntfy.topic" name="ntfy.topic" value="{{ cfg.ntfy.topic }}" class="form-control" aria-label="{{ _('Topic') }}">
</div>
<h3 class="fw-light mb-2">Credentials (optional)</h3>
<p class="text-body-secondary mb-3">Set credentials if the topic is protected</p>
<h3 class="fw-light mb-2">{{ _('Credentials (optional)') }}</h3>
<p class="text-body-secondary mb-3">{{ _('Set credentials if the topic is protected') }}</p>
<div class="form-group mb-3 mt-2">
<label class="form-label" for="ntfy.user">Username</label>
<label class="form-label" for="ntfy.user">{{ _('Username') }}</label>
<input type="text" id="ntfy.user" name="ntfy.user" value="{{ cfg.ntfy.user }}" class="form-control">
<p class="form-text">
Topic user
{{ _('Topic user') }}
</p>
</div>
<div class="form-group mb-3">
<label class="form-label" for="ntfy.pass">Password</label>
<label class="form-label" for="ntfy.pass">{{ _('Password') }}</label>
<input type="password" id="ntfy.pass" name="ntfy.pass" value="{{ cfg.ntfy.pass }}" class="form-control">
<p class="form-text">
Topic password
{{ _('Topic password') }}
</p>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary mt-3" id="saveConfig">
<span class="spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span>
<span class="visually-hidden" role="status">Loading...</span>
Save
</button>
</div>
{% include 'snippets/admin/saveBtn.html' %}
</form>
{% endblock %}
{% block _scripts %}

View file

@ -1,22 +1,22 @@
{% extends 'base.html' %}
{% block title %}Admin Login{% endblock %}
{% block title %}{{ _('Admin Login') }}{% endblock %}
{% set loginLink = 'active' %}
{% block content %}
<div class="row">
<div class="col-lg-4 m-auto mt-5">
<div class="col-lg-3 m-auto mt-5">
<div class="card">
<div class="card-body">
<h2 class="text-center mb-4 mt-2">Login to {{ cfg.instance.title }}</h2>
<h2 class="text-center mb-4 mt-2">{{ _('Login to {}').format(cfg.instance.title) }}</h2>
<form action="{{ url_for('admin.login') }}" method="POST">
<div class="form-floating mb-2">
<input type="password" required class="form-control" id="admin_password" name="admin_password" placeholder="Password">
<label for="admin_password">Password</label>
<input type="password" required class="form-control" id="admin_password" name="admin_password" placeholder="{{ _('Password') }}">
<label for="admin_password">{{ _('Password') }}</label>
</div>
<div class="form-check mb-3">
<input type="checkbox" class="form-check-input" id="remember_me" name="remember_me" checked>
<label class="form-check-label" for="remember_me">Stay logged in</label>
<label class="form-check-label" for="remember_me">{{ _('Stay logged in') }}</label>
</div>
<button type="submit" class="btn btn-primary w-100">Login</button>
<button type="submit" class="btn btn-primary w-100">{{ _('Login') }}</button>
</form>
</div>
</div>

View file

@ -1,26 +1,26 @@
{% extends 'base.html' %}
{% block title %}Inbox {% if len(questions) > 0 %}({{ len(questions) }}){% endif %}{% endblock %}
{% block title %}{{ _('Inbox') }} {% if len(questions) > 0 %}({{ len(questions) }}){% endif %}{% endblock %}
{% set inboxLink = 'active' %}
{% block additionalHeadItems %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/toastify.css') }}">
{% endblock %}
{% block content %}
{% if questions != [] %}
<h3 class="fs-4"><span id="question-count-inbox">{{ len(questions) }}</span> <span class="fw-light">question(s)</span></h3>
<h3 class="fs-4"><span id="question-count-inbox">{{ len(questions) }}</span> <span class="fw-light">{{ _('question(s)') }}</span></h3>
<div class="row">
{% for question in questions %}
<div class="col-lg-8 m-auto">
<div class="card mb-3 mt-3 alert-placeholder question" id="question-{{ question.id }}">
<div class="card-header">
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title mt-1 mb-1 markdown-content w-50">
<h5 class="card-title mt-1 mb-0 markdown-content w-50">
{% if question.from_who %}
{{ render_markdown(question.from_who, fromWho=True) }}
{% else %}
<i class="bi bi-incognito" data-bs-toggle="tooltip" data-bs-title="This question was asked anonymously" data-bs-placement="top"></i> {{ cfg.anonName }}
<i class="bi bi-incognito" data-bs-toggle="tooltip" data-bs-title="{{ _('This question was asked anonymously') }}" data-bs-placement="top"></i> {{ cfg.anonName }}
{% endif %}
</h5>
<h6 class="card-subtitle fw-light text-body-secondary">
<h6 class="card-subtitle mt-1 fw-light text-body-secondary">
{#
reserved for version 1.6.0 or later
@ -38,8 +38,8 @@
{{ question.content | render_markdown }}
</div>
<button class="z-0 cw-btn btn btn-sm btn-secondary shadow text-center w-100 sticky-bottom" type="button" data-bs-toggle="collapse" data-bs-target="#question-cw-{{ question.id }}" aria-expanded="false" aria-controls="question-cw-{{ question.id }}">
<span class="fw-medium cw-btn-text">Show content</span>
<span class="text-body-secondary cw-btn-chars">({{ len(question.content) }} characters)</span>
<span class="fw-medium cw-btn-text">{{ _('Show content') }}</span>
<span class="text-body-secondary cw-btn-chars">({{ _("{} characters").format(len(question.content)) }})</span>
</button>
{% else %}
{{ question.content | render_markdown }}
@ -85,15 +85,15 @@
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header border-0">
<h1 class="modal-title fs-5 fw-normal" id="q-{{ question.id }}-modal-label">Confirmation</h1>
<h1 class="modal-title fs-5 fw-normal" id="q-{{ question.id }}-modal-label">{{ _('Confirmation') }}</h1>
<button type="button" class="btn-close d-flex align-items-center fs-5" data-bs-dismiss="modal" aria-label="Close"><i class="bi bi-x-lg"></i></button>
</div>
<div class="modal-body pt-0 pb-0">
<p>Are you sure you want to delete this question?</p>
<p>{{ _('Are you sure you want to delete this question?') }}</p>
</div>
<div class="modal-footer pt-1 border-0">
<button type="button" class="btn btn-outline-secondary flex-fill flex-sm-grow-0" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger flex-fill flex-sm-grow-0" data-bs-dismiss="modal" hx-delete="{{ url_for('api.deleteQuestion', question_id=question.id) }}" hx-target="#question-{{ question.id }}" hx-swap="none">Confirm</button>
<div class="modal-footer pt-1 border-0 flex-column flex-sm-row align-items-stretch align-items-sm-center">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">{{ _('Cancel') }}</button>
<button type="button" class="btn btn-danger" data-bs-dismiss="modal" hx-delete="{{ url_for('api.deleteQuestion', question_id=question.id) }}" hx-target="#question-{{ question.id }}" hx-swap="none">{{ _('Confirm') }}</button>
</div>
</div>
</div>
@ -103,7 +103,7 @@
{% endfor %}
</div>
{% else %}
<h2 class="text-center mt-5">Inbox is currently empty.</h2>
<h2 class="text-center mt-5">{{ _('Inbox is currently empty.') }}</h2>
{% endif %}
{% endblock %}
{% block scripts %}