#!/usr/bin/env python3 from flask import url_for from .util import live_server_setup, wait_for_all_checks def test_zh_TW(client, live_server, measure_memory_usage, datastore_path): import time test_url = url_for('test_endpoint', _external=True) # 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" # timeago library just hasn't been updated to use the more modern locale naming convention, before BCP 47 / RFC 5646. # The Python timeago library (https://github.com/hustcc/timeago) supports 48 locales but uses different naming conventions than Flask-Babel. def test_zh_Hant_TW_timeago_integration(): """Test that zh_Hant_TW mapping works and timeago renders Traditional Chinese correctly""" import timeago from datetime import datetime, timedelta from changedetectionio.languages import get_timeago_locale # 1. Test the mapping mapped_locale = get_timeago_locale('zh_Hant_TW') assert mapped_locale == 'zh_TW', "zh_Hant_TW should map to timeago's zh_TW" assert get_timeago_locale('zh_TW') == 'zh_TW', "zh_TW should also map to zh_TW" # 2. Test timeago library renders Traditional Chinese with the mapped locale now = datetime.now() # Test various time periods with Traditional Chinese strings result_15s = timeago.format(now - timedelta(seconds=15), now, mapped_locale) assert '秒前' in result_15s, f"Expected '秒前' in '{result_15s}'" result_5m = timeago.format(now - timedelta(minutes=5), now, mapped_locale) assert '分鐘前' in result_5m, f"Expected '分鐘前' in '{result_5m}'" result_2h = timeago.format(now - timedelta(hours=2), now, mapped_locale) assert '小時前' in result_2h, f"Expected '小時前' in '{result_2h}'" result_3d = timeago.format(now - timedelta(days=3), now, mapped_locale) assert '天前' in result_3d, f"Expected '天前' in '{result_3d}'" def test_language_switching(client, live_server, measure_memory_usage, datastore_path): """ Test that the language switching functionality works correctly. 1. Switch to Italian using the /set-language endpoint 2. Verify that Italian text appears on the page 3. Switch back to English and verify English text appears """ # Establish session cookie client.get(url_for("add_watch_ui.add_watch_ui_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"), follow_redirects=True ) assert res.status_code == 200 # Step 2: Request the index page - should be in Italian # The session cookie should be maintained by the test client res = client.get( url_for("add_watch_ui.add_watch_ui_index"), follow_redirects=True ) assert res.status_code == 200 # Check rendering switched to Italian. is set directly # from get_locale() so it can't be a translation accident — use it as the # canonical "what locale rendered?" signal instead of free-text sentinels # like 'Cancel', which can silently match a substring of a translated word # in another language (e.g. Italian 'Cancella' contains 'Cancel'). assert b' after switching to Italian" # One strong content check too — confirms the catalogue was actually loaded # and the substitution happened, not just the lang attribute. assert b'Modifiche testo/HTML, JSON e PDF' in res.data, "Expected italian from processors.available_processors()" # Step 3: Switch back to English # NB: use 'en_GB' not 'en' — only the variants are in language_codes; the # plain 'en' code is silently rejected by set_language and the locale would # remain at 'it', defeating the round-trip assertion below. res = client.get( url_for("set_language", locale="en_GB"), follow_redirects=True ) assert res.status_code == 200 # Request the index page - should now be in English res = client.get( url_for("add_watch_ui.add_watch_ui_index"), follow_redirects=True ) assert res.status_code == 200 # Round-tripped back to English — assert via the lang attribute (the only # signal that can't be tricked by substring overlap from another locale). assert b' after switching back to English" def test_invalid_locale(client, live_server, measure_memory_usage, datastore_path): """ Test that setting an invalid locale doesn't break the application. 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. Use the BCP 47 region-tagged form 'en_GB' — the # bare 'en' is NOT in language_codes and is silently rejected by # set_language, so passing it here would leave the session locale unset # and let the (unrelated) Accept-Language fallback decide what renders. res = client.get( url_for("set_language", locale="en_GB"), follow_redirects=True ) assert res.status_code == 200 # Try to set an invalid locale res = client.get( url_for("set_language", locale="invalid_locale_xyz"), follow_redirects=True ) assert res.status_code == 200 # Should still be able to access the page (should stay in English since invalid locale was ignored) res = client.get( url_for("watchlist.index"), follow_redirects=True ) assert res.status_code == 200 # Check via the lang attribute — the only signal that can't be tricked # by substring overlap with another locale's translations. assert b'' in res.data, \ "Expected Taiwan flag 'fi fi-tw' for Traditional Chinese" assert b'' not in res.data, \ "Should not show China flag 'fi fi-cn' for Traditional Chinese" # Verify we're getting Traditional Chinese text throughout the page res = client.get( url_for("settings.settings_page"), headers={'Accept-Language': 'zh-TW,zh;q=0.9,en;q=0.8'}, follow_redirects=True ) assert res.status_code == 200 # Check Traditional Chinese translations (not English) assert "小時".encode() in res.data, "Expected Traditional Chinese '小時' for Hours" assert "分鐘".encode() in res.data, "Expected Traditional Chinese '分鐘' for Minutes" assert b"Hours" not in res.data or "小時".encode() in res.data, "Should have Traditional Chinese, not English" def test_accept_language_header_en_variants(client, live_server, measure_memory_usage, datastore_path): """ Test that browsers sending en-GB and en-US in Accept-Language header get the correct English variant. This ensures the locale selector works properly for English variants. """ from flask import url_for # Test 1: British English (en-GB) with client.session_transaction() as sess: sess.clear() res = client.get( url_for("watchlist.index"), headers={'Accept-Language': 'en-GB,en;q=0.9'}, follow_redirects=True ) assert res.status_code == 200 # Should get English content assert b"Select Language" in res.data, "Expected English text 'Select Language'" # Check HTML lang attribute uses BCP 47 format with hyphen assert b'' in res.data, \ "Expected UK flag 'fi fi-gb' for British English" # Test 2: American English (en-US) with client.session_transaction() as sess: sess.clear() res = client.get( url_for("watchlist.index"), headers={'Accept-Language': 'en-US,en;q=0.9'}, follow_redirects=True ) assert res.status_code == 200 # Should get English content assert b"Select Language" in res.data, "Expected English text 'Select Language'" # Check HTML lang attribute uses BCP 47 format with hyphen assert b'' in res.data, \ "Expected US flag 'fi fi-us' for American English" # Test 3: Generic 'en' should fall back to one of the English variants with client.session_transaction() as sess: sess.clear() res = client.get( url_for("watchlist.index"), headers={'Accept-Language': 'en'}, follow_redirects=True ) assert res.status_code == 200 # Should get English content (either variant is fine) assert b"Select Language" in res.data, "Expected English text 'Select Language'" def test_accept_language_header_zh_simplified(client, live_server, measure_memory_usage, datastore_path): """ Test that browsers sending zh or zh-CN in Accept-Language header get Simplified Chinese. This ensures Simplified Chinese still works correctly and doesn't get confused with Traditional. """ from flask import url_for # Test 1: Generic 'zh' should get Simplified Chinese with client.session_transaction() as sess: sess.clear() res = client.get( url_for("watchlist.index"), headers={'Accept-Language': 'zh,en;q=0.9'}, follow_redirects=True ) assert res.status_code == 200 # Should get Simplified Chinese content, not Traditional Chinese # Simplified: 选择语言, Traditional: 選擇語言 assert '选择语言'.encode() in res.data, "Expected Simplified Chinese '选择语言' (Select Language)" assert '選擇語言'.encode() not in res.data, "Should not get Traditional Chinese '選擇語言'" # Check HTML lang attribute assert b'' in res.data, \ "Expected China flag 'fi fi-cn' for Simplified Chinese" assert b'' not in res.data, \ "Should not show Taiwan flag 'fi fi-tw' for Simplified Chinese" # Test 2: 'zh-CN' should also get Simplified Chinese with client.session_transaction() as sess: sess.clear() res = client.get( url_for("watchlist.index"), headers={'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'}, follow_redirects=True ) assert res.status_code == 200 # Should get Simplified Chinese content assert '选择语言'.encode() in res.data, "Expected Simplified Chinese '选择语言' with zh-CN header" assert '選擇語言'.encode() not in res.data, "Should not get Traditional Chinese with zh-CN header" # Check that the correct flag icon is shown (China flag for zh-CN) assert b'' in res.data, \ "Expected China flag 'fi fi-cn' for zh-CN header" # Verify Simplified Chinese in settings page res = client.get( url_for("settings.settings_page"), headers={'Accept-Language': 'zh,en;q=0.9'}, follow_redirects=True ) assert res.status_code == 200 # Check Simplified Chinese translations (not Traditional or English) # Simplified: 小时, Traditional: 小時 assert "小时".encode() in res.data, "Expected Simplified Chinese '小时' for Hours" assert "分钟".encode() in res.data, "Expected Simplified Chinese '分钟' for Minutes" assert "秒".encode() in res.data, "Expected Simplified Chinese '秒' for Seconds" # Make sure it's not Traditional Chinese assert "小時".encode() not in res.data, "Should not have Traditional Chinese '小時'" assert "分鐘".encode() not in res.data, "Should not have Traditional Chinese '分鐘'" def test_session_locale_overrides_accept_language(client, live_server, measure_memory_usage, datastore_path): """ Test that session locale preference overrides browser Accept-Language header. Scenario: 1. Browser auto-detects zh-TW (Traditional Chinese) from Accept-Language header 2. User explicitly selects Korean language 3. On subsequent page loads, Korean should be shown (not Traditional Chinese) even though the Accept-Language header still says zh-TW This tests the session override behavior for issue #3779. """ from flask import url_for # Step 1: Clear session and make first request with zh-TW header (auto-detect) with client.session_transaction() as sess: sess.clear() res = client.get( url_for("watchlist.index"), headers={'Accept-Language': 'zh-TW,zh;q=0.9,en;q=0.8'}, follow_redirects=True ) assert res.status_code == 200 # Should initially get Traditional Chinese from auto-detect assert '選擇語言'.encode() in res.data, "Expected Traditional Chinese '選擇語言' from auto-detect" assert b'' in res.data, \ "Expected Taiwan flag 'fi fi-tw' from auto-detect" # Step 2: User explicitly selects Korean language res = client.get( url_for("set_language", locale="ko"), headers={'Accept-Language': 'zh-TW,zh;q=0.9,en;q=0.8'}, # Browser still sends zh-TW follow_redirects=True ) assert res.status_code == 200 # Step 3: Make another request with same zh-TW header # Session should override the Accept-Language header res = client.get( url_for("watchlist.index"), headers={'Accept-Language': 'zh-TW,zh;q=0.9,en;q=0.8'}, # Still sending zh-TW! follow_redirects=True ) assert res.status_code == 200 # Should now get Korean (session overrides auto-detect) # Korean: 언어 선택, Traditional Chinese: 選擇語言 assert '언어 선택'.encode() in res.data, "Expected Korean '언어 선택' (Select Language) from session" assert '選擇語言'.encode() not in res.data, "Should not get Traditional Chinese when Korean is set in session" # Check HTML lang attribute is Korean assert b'' in res.data, \ "Expected Korean flag 'fi fi-kr' from session preference" assert b'' not in res.data, \ "Should not show Taiwan flag when Korean is set in session" # Verify Korean text on settings page as well res = client.get( url_for("settings.settings_page"), headers={'Accept-Language': 'zh-TW,zh;q=0.9,en;q=0.8'}, # Still zh-TW! follow_redirects=True ) assert res.status_code == 200 # Check Korean translations (not Traditional Chinese or English) # Korean: 시간 (Hours), 분 (Minutes), 초 (Seconds) # Traditional Chinese: 小時, 分鐘, 秒 assert "시간".encode() in res.data, "Expected Korean '시간' for Hours" assert "분".encode() in res.data, "Expected Korean '분' for Minutes" assert "小時".encode() not in res.data, "Should not have Traditional Chinese '小時' when Korean is set" assert "分鐘".encode() not in res.data, "Should not have Traditional Chinese '分鐘' when Korean is set" def test_clear_history_translated_confirmation(client, live_server, measure_memory_usage, datastore_path): """ Test that clearing snapshot history works with translated confirmation text. Issue #3865: When the app language is set to German, the clear history confirmation dialog shows the translated word (e.g. 'loschen') but the backend only accepted the English word 'clear', making it impossible to clear snapshots in non-English languages. """ from flask import url_for test_url = url_for('test_endpoint', _external=True) # Add a watch so there is history to clear res = client.post( url_for("imports.import_page"), data={"urls": test_url}, follow_redirects=True ) assert b"1 Imported" in res.data wait_for_all_checks(client) # Set language to German res = client.get( url_for("set_language", locale="de"), follow_redirects=True ) assert res.status_code == 200 # Verify the clear history page shows the German confirmation word res = client.get( url_for("ui.clear_all_history"), follow_redirects=True ) assert res.status_code == 200 assert "löschen".encode() in res.data, "Expected German word 'loschen' on clear history page" # Submit the form with the German translated word res = client.post( url_for("ui.clear_all_history"), data={"confirmtext": "löschen"}, follow_redirects=True ) assert res.status_code == 200 # Should NOT show error message assert b"Incorrect confirmation text" not in res.data, \ "German confirmation word 'loschen' should be accepted (issue #3865)" # Switch back to English and verify English word still works res = client.get( url_for("set_language", locale="en_US"), follow_redirects=True ) res = client.post( url_for("ui.clear_all_history"), data={"confirmtext": "clear"}, follow_redirects=True ) assert res.status_code == 200 assert b"Incorrect confirmation text" not in res.data, \ "English confirmation word 'clear' should still be accepted" # Verify that missing/empty confirmtext does not crash the server res = client.post( url_for("ui.clear_all_history"), data={}, follow_redirects=True ) assert res.status_code == 200, \ "Missing confirmtext should not crash the server"