From 2b9618bbb550d2cef1809018a75c2755e4b1bf99 Mon Sep 17 00:00:00 2001 From: dgtlmoon Date: Thu, 22 Jan 2026 05:57:37 +0100 Subject: [PATCH] Make language selection sticky and provide a way to return back to default auto-detect #3792 --- changedetectionio/blueprint/ui/__init__.py | 21 ++++++++++++++++ changedetectionio/flask_app.py | 11 +++++++++ changedetectionio/templates/base.html | 5 ++++ changedetectionio/tests/test_i18n.py | 28 +++++++++++++++++++++- 4 files changed, 64 insertions(+), 1 deletion(-) diff --git a/changedetectionio/blueprint/ui/__init__.py b/changedetectionio/blueprint/ui/__init__.py index d7fd587a3..c0bab9748 100644 --- a/changedetectionio/blueprint/ui/__init__.py +++ b/changedetectionio/blueprint/ui/__init__.py @@ -404,4 +404,25 @@ def construct_blueprint(datastore: ChangeDetectionStore, update_q, worker_handle return redirect(url_for('watchlist.index')) + @ui_blueprint.route("/language/auto-detect", methods=['GET']) + def delete_locale_language_session_var_if_it_exists(): + """Clear the session locale preference to auto-detect from browser Accept-Language header""" + if 'locale' in session: + session.pop('locale', None) + # Refresh Flask-Babel to clear cached locale + from flask_babel import refresh + refresh() + flash(gettext("Language set to auto-detect from browser")) + + # Check if there's a redirect parameter to return to the same page + redirect_url = request.args.get('redirect') + + # If redirect is provided and safe, use it + from changedetectionio.is_safe_url import is_safe_url + if redirect_url and is_safe_url(redirect_url): + return redirect(redirect_url) + + # Otherwise redirect to watchlist + return redirect(url_for('watchlist.index')) + return ui_blueprint \ No newline at end of file diff --git a/changedetectionio/flask_app.py b/changedetectionio/flask_app.py index 40b1452e6..4333cd2cb 100644 --- a/changedetectionio/flask_app.py +++ b/changedetectionio/flask_app.py @@ -94,6 +94,14 @@ if os.getenv('FLASK_SERVER_NAME'): app.config['BABEL_TRANSLATION_DIRECTORIES'] = str(Path(__file__).parent / 'translations') app.config['BABEL_DEFAULT_LOCALE'] = 'en_GB' +# Session configuration +# NOTE: Flask session (for locale, etc.) is separate from Flask-Login's remember-me cookie +# - Flask session stores data like session['locale'] in a signed cookie +# - Flask-Login's remember=True creates a separate authentication cookie +# - Setting PERMANENT_SESSION_LIFETIME controls how long the Flask session cookie lasts +from datetime import timedelta +app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=3650) # ~10 years (effectively unlimited) + #app.config["EXPLAIN_TEMPLATE_LOADING"] = True @@ -550,6 +558,9 @@ def changedetection_app(config=None, datastore_o=None): # Validate the locale against available languages if locale in language_codes: + # Make session permanent so language preference persists across browser sessions + # NOTE: This is the Flask session cookie (separate from Flask-Login's remember-me auth cookie) + session.permanent = True session['locale'] = locale # CRITICAL: Flask-Babel caches the locale in the request context (ctx.babel_locale) diff --git a/changedetectionio/templates/base.html b/changedetectionio/templates/base.html index cc29d399d..56cba6c18 100644 --- a/changedetectionio/templates/base.html +++ b/changedetectionio/templates/base.html @@ -265,6 +265,11 @@ {% endfor %} +
+ + 🌐 {{ _('Auto-detect from browser') }} + +
{{ _('Language support is in beta, please help us improve by opening a PR on GitHub with any updates.') }}
diff --git a/changedetectionio/tests/test_i18n.py b/changedetectionio/tests/test_i18n.py index c99b559f6..89da88b0b 100644 --- a/changedetectionio/tests/test_i18n.py +++ b/changedetectionio/tests/test_i18n.py @@ -160,7 +160,7 @@ def test_invalid_locale(client, live_server, measure_memory_usage, datastore_pat def test_language_persistence_in_session(client, live_server, measure_memory_usage, datastore_path): """ Test that the language preference persists across multiple requests - within the same session. + within the same session, and that auto-detect properly clears the preference. """ # Establish session cookie @@ -184,6 +184,32 @@ def test_language_persistence_in_session(client, live_server, measure_memory_usa assert res.status_code == 200 assert b"Annulla" in res.data, "Italian text should persist across requests" + # Verify locale is in session + with client.session_transaction() as sess: + assert sess.get('locale') == 'it', "Locale should be set in session" + + # Call auto-detect to clear the locale + res = client.get( + url_for("ui.delete_locale_language_session_var_if_it_exists"), + follow_redirects=True + ) + + assert res.status_code == 200 + + # Verify locale was removed from session + with client.session_transaction() as sess: + assert 'locale' not in sess, "Locale should be removed from session after auto-detect" + + # Now requests should use browser default (English in test environment) + res = client.get( + url_for("watchlist.index"), + follow_redirects=True + ) + + assert res.status_code == 200 + assert b"Cancel" in res.data, "Should show English after auto-detect clears Italian" + assert b"Annulla" not in res.data, "Should not show Italian after auto-detect" + def test_set_language_with_redirect(client, live_server, measure_memory_usage, datastore_path): """