From 06ea29bfc7364092cf92a5229d2329fabb21e04e Mon Sep 17 00:00:00 2001 From: dgtlmoon Date: Thu, 15 Jan 2026 12:01:12 +0100 Subject: [PATCH] Translations - Fixing `zh_TW` to `zh_Hant_TW` , adding tests #3737 (#3744) --- changedetectionio/flask_app.py | 24 +++++++--- changedetectionio/languages.py | 4 +- changedetectionio/templates/base.html | 2 +- changedetectionio/tests/test_i18n.py | 45 ++++++++++++++++++ .../LC_MESSAGES/messages.mo | Bin .../LC_MESSAGES/messages.po | 0 6 files changed, 66 insertions(+), 9 deletions(-) rename changedetectionio/translations/{zh_TW => zh_Hant_TW}/LC_MESSAGES/messages.mo (100%) rename changedetectionio/translations/{zh_TW => zh_Hant_TW}/LC_MESSAGES/messages.po (100%) diff --git a/changedetectionio/flask_app.py b/changedetectionio/flask_app.py index a1545db26..d1e438ea5 100644 --- a/changedetectionio/flask_app.py +++ b/changedetectionio/flask_app.py @@ -9,6 +9,7 @@ import threading import time import timeago from blinker import signal +from pathlib import Path from changedetectionio.strtobool import strtobool from threading import Event @@ -84,6 +85,10 @@ app.config['NEW_VERSION_AVAILABLE'] = False if os.getenv('FLASK_SERVER_NAME'): app.config['SERVER_NAME'] = os.getenv('FLASK_SERVER_NAME') +# Babel/i18n configuration +app.config['BABEL_TRANSLATION_DIRECTORIES'] = str(Path(__file__).parent / 'translations') +app.config['BABEL_DEFAULT_LOCALE'] = 'en_GB' + #app.config["EXPLAIN_TEMPLATE_LOADING"] = True @@ -395,13 +400,9 @@ def changedetection_app(config=None, datastore_o=None): def get_locale(): # 1. Try to get locale from session (user explicitly selected) if 'locale' in session: - locale = session['locale'] - logger.trace(f"DEBUG: get_locale() returning from session: {locale}") - return locale + return session['locale'] # 2. Fall back to Accept-Language header - locale = request.accept_languages.best_match(language_codes) - logger.trace(f"DEBUG: get_locale() returning from Accept-Language: {locale}") - return locale + return request.accept_languages.best_match(language_codes) # Initialize Babel with locale selector babel = Babel(app, locale_selector=get_locale) @@ -518,9 +519,20 @@ def changedetection_app(config=None, datastore_o=None): @app.route('/set-language/') def set_language(locale): """Set the user's preferred language in the session""" + if not request.cookies: + logger.error("Cannot set language without session cookie") + flash("Cannot set language without session cookie", 'error') + return redirect(url_for('watchlist.index')) + # Validate the locale against available languages if locale in language_codes: session['locale'] = locale + + # CRITICAL: Flask-Babel caches the locale in the request context (ctx.babel_locale) + # We must refresh to clear this cache so the new locale takes effect immediately + # This is especially important for tests where multiple requests happen rapidly + from flask_babel import refresh + refresh() else: logger.error(f"Invalid locale {locale}, available: {language_codes}") diff --git a/changedetectionio/languages.py b/changedetectionio/languages.py index e6552d716..c169de9cf 100644 --- a/changedetectionio/languages.py +++ b/changedetectionio/languages.py @@ -29,7 +29,7 @@ def get_timeago_locale(flask_locale): """ locale_map = { 'zh': 'zh_CN', # Chinese Simplified - 'zh_Hant_TW': 'zh_TW', # Flask-Babel normalizes zh_TW to zh_Hant_TW + 'zh_TW': 'zh_Hant_TW', # Flask-Babel normalizes zh_TW to zh_Hant_TW 'pt': 'pt_PT', # Portuguese (Portugal) 'sv': 'sv_SE', # Swedish 'no': 'nb_NO', # Norwegian Bokmål @@ -54,7 +54,7 @@ LANGUAGE_DATA = { 'it': {'flag': 'fi fi-it fis', 'name': 'Italiano'}, 'ja': {'flag': 'fi fi-jp fis', 'name': '日本語'}, 'zh': {'flag': 'fi fi-cn fis', 'name': '中文 (简体)'}, - 'zh_TW': {'flag': 'fi fi-tw fis', 'name': '繁體中文'}, + 'zh_Hant_TW': {'flag': 'fi fi-tw fis', 'name': '繁體中文'}, 'ru': {'flag': 'fi fi-ru fis', 'name': 'Русский'}, 'pl': {'flag': 'fi fi-pl fis', 'name': 'Polski'}, 'nl': {'flag': 'fi fi-nl fis', 'name': 'Nederlands'}, diff --git a/changedetectionio/templates/base.html b/changedetectionio/templates/base.html index 35df0b70a..cc29d399d 100644 --- a/changedetectionio/templates/base.html +++ b/changedetectionio/templates/base.html @@ -1,5 +1,5 @@ - + diff --git a/changedetectionio/tests/test_i18n.py b/changedetectionio/tests/test_i18n.py index 76fa8f3e9..8f1b738ff 100644 --- a/changedetectionio/tests/test_i18n.py +++ b/changedetectionio/tests/test_i18n.py @@ -3,6 +3,39 @@ from flask import url_for from .util import live_server_setup +def test_zh_TW(client, live_server, measure_memory_usage, datastore_path): + + # Be sure we got a session cookie + res = client.get(url_for("watchlist.index"), follow_redirects=True) + + res = client.get( + url_for("set_language", locale="zh_Hant_TW"), # Traditional + follow_redirects=True + ) + # HTML follows BCP 47 language tag rules, not underscore-based locale formats. + assert b'zh + res = client.get( + url_for("set_language", locale="zh"), # Simplified chinese + follow_redirects=True + ) + res = client.get(url_for("watchlist.index"), follow_redirects=True) + assert "选择语言".encode() in res.data, "Simplified chinese worked and it means the flask-babel cache worked" + + + def test_language_switching(client, live_server, measure_memory_usage, datastore_path): """ @@ -13,6 +46,9 @@ def test_language_switching(client, live_server, measure_memory_usage, datastore 3. Switch back to English and verify English text appears """ + # Establish session cookie + client.get(url_for("watchlist.index"), follow_redirects=True) + # Step 1: Set the language to Italian using the /set-language endpoint res = client.get( url_for("set_language", locale="it"), @@ -61,6 +97,9 @@ def test_invalid_locale(client, live_server, measure_memory_usage, datastore_pat The app should ignore invalid locales and continue working. """ + # Establish session cookie + client.get(url_for("watchlist.index"), follow_redirects=True) + # First set to English res = client.get( url_for("set_language", locale="en"), @@ -93,6 +132,9 @@ def test_language_persistence_in_session(client, live_server, measure_memory_usa within the same session. """ + # Establish session cookie + client.get(url_for("watchlist.index"), follow_redirects=True) + # Set language to Italian res = client.get( url_for("set_language", locale="it"), @@ -119,6 +161,9 @@ def test_set_language_with_redirect(client, live_server, measure_memory_usage, d """ from flask import url_for + # Establish session cookie + client.get(url_for("watchlist.index"), follow_redirects=True) + # Set language with a redirect parameter (simulating language change from /settings) res = client.get( url_for("set_language", locale="de", redirect="/settings"), diff --git a/changedetectionio/translations/zh_TW/LC_MESSAGES/messages.mo b/changedetectionio/translations/zh_Hant_TW/LC_MESSAGES/messages.mo similarity index 100% rename from changedetectionio/translations/zh_TW/LC_MESSAGES/messages.mo rename to changedetectionio/translations/zh_Hant_TW/LC_MESSAGES/messages.mo diff --git a/changedetectionio/translations/zh_TW/LC_MESSAGES/messages.po b/changedetectionio/translations/zh_Hant_TW/LC_MESSAGES/messages.po similarity index 100% rename from changedetectionio/translations/zh_TW/LC_MESSAGES/messages.po rename to changedetectionio/translations/zh_Hant_TW/LC_MESSAGES/messages.po