From 1cefe387ab5299bb8ba26099570cb7d4a851ad35 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 11 May 2026 18:48:53 +0000 Subject: [PATCH] rebase localization onto latest main Agent-Logs-Url: https://github.com/wasi-master/13ft/sessions/85227ed8-0556-4ae4-a47b-92bfed6cfe67 Co-authored-by: wasi-master <63045920+wasi-master@users.noreply.github.com> --- README.md | 6 +-- app/__pycache__/portable.cpython-312.pyc | Bin 32335 -> 33763 bytes app/locales/de.json | 4 +- app/locales/en.json | 4 +- app/locales/fr.json | 4 +- app/portable.py | 62 +++++++++++++++++------ 6 files changed, 55 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 281d177..571b699 100644 --- a/README.md +++ b/README.md @@ -168,7 +168,7 @@ WantedBy=multi-user.target ## Localisation -The UI strings can be localised by setting the `LOCALE` environment variable before starting the app. Locale files live in `/home/runner/work/13ft/13ft/app/locales/.json`. +The UI strings can be localised by setting the `LOCALE` environment variable before starting the app. Locale files live in `app/locales/.json`. Built-in locales: @@ -179,14 +179,14 @@ Built-in locales: Examples: ```sh -# from /home/runner/work/13ft/13ft/app +# from app LOCALE=de python portable.py # or with gunicorn LOCALE=fr python -m gunicorn 'portable:app' ``` -To add a new locale, copy `/home/runner/work/13ft/13ft/app/locales/en.json` to a new file such as `/home/runner/work/13ft/13ft/app/locales/es.json` and translate the values. +To add a new locale, copy `app/locales/en.json` to a new file such as `app/locales/es.json` and translate the values. ## Screenshots diff --git a/app/__pycache__/portable.cpython-312.pyc b/app/__pycache__/portable.cpython-312.pyc index 8ee6010fc0a262c7969158ac7a06aea162f51c06..513563dfd86c00fc32fa6ca3176ce6ecc4a2faa8 100644 GIT binary patch delta 6173 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 IZG)%(53g#up8x;= diff --git a/app/locales/de.json b/app/locales/de.json index b184ca0..cfe7120 100644 --- a/app/locales/de.json +++ b/app/locales/de.json @@ -22,6 +22,6 @@ "challenge_error": "Die Website hat eine Anti-Bot-Prüfung (Cloudflare oder ähnlich) ausgeliefert und es wurde kein brauchbarer Schnappschuss auf archive.org oder archive.today gefunden. Diese Website blockiert aktiv automatisierte Anfragen.", "timeout_error": "Die Website hat zu lange zum Antworten gebraucht (30s-Timeout). Bitte versuchen Sie es später erneut.", "connection_error": "Die Verbindung zur Website konnte nicht hergestellt werden. Prüfen Sie die URL und versuchen Sie es erneut.", - "request_error": "Seite konnte nicht geladen werden: {error}", - "unexpected_error": "Unerwarteter Fehler: {error}" + "request_error": "Die Seite konnte nicht geladen werden.", + "unexpected_error": "Unerwarteter Fehler beim Laden der Seite." } diff --git a/app/locales/en.json b/app/locales/en.json index 0907a34..bd5c5b6 100644 --- a/app/locales/en.json +++ b/app/locales/en.json @@ -22,6 +22,6 @@ "challenge_error": "The site served an anti-bot challenge (Cloudflare or similar) and no usable snapshot was found on archive.org or archive.today. This site is actively blocking automated requests.", "timeout_error": "The website took too long to respond (30s timeout). Try again later.", "connection_error": "Could not connect to the website. Check the URL and try again.", - "request_error": "Failed to fetch the page: {error}", - "unexpected_error": "Unexpected error: {error}" + "request_error": "Failed to fetch the page.", + "unexpected_error": "Unexpected error while fetching the page." } diff --git a/app/locales/fr.json b/app/locales/fr.json index 3aba146..4078415 100644 --- a/app/locales/fr.json +++ b/app/locales/fr.json @@ -22,6 +22,6 @@ "challenge_error": "Le site a présenté une protection anti-bot (Cloudflare ou similaire) et aucun instantané exploitable n'a été trouvé sur archive.org ou archive.today. Ce site bloque activement les requêtes automatisées.", "timeout_error": "Le site web a mis trop de temps à répondre (délai de 30 s). Veuillez réessayer plus tard.", "connection_error": "Impossible de se connecter au site web. Vérifiez l'URL et réessayez.", - "request_error": "Échec de récupération de la page : {error}", - "unexpected_error": "Erreur inattendue : {error}" + "request_error": "Impossible de récupérer la page.", + "unexpected_error": "Erreur inattendue lors du chargement de la page." } diff --git a/app/portable.py b/app/portable.py index 3208896..6924f5a 100644 --- a/app/portable.py +++ b/app/portable.py @@ -6,6 +6,7 @@ import requests import uuid import threading import time +from functools import lru_cache from urllib.parse import urljoin, urlparse, quote from flask import request, Response from bs4 import BeautifulSoup @@ -44,16 +45,20 @@ _DEFAULT_STRINGS = { ), "timeout_error": "The website took too long to respond (30s timeout). Try again later.", "connection_error": "Could not connect to the website. Check the URL and try again.", - "request_error": "Failed to fetch the page: {error}", - "unexpected_error": "Unexpected error: {error}", + "request_error": "Failed to fetch the page.", + "unexpected_error": "Unexpected error while fetching the page.", } -def load_strings(): +def get_locale(): locale = os.environ.get("LOCALE", "en") if not re.match(r"^[a-zA-Z0-9_-]+$", locale): locale = "en" + return locale + +@lru_cache(maxsize=None) +def load_strings_for_locale(locale): strings = dict(_DEFAULT_STRINGS) locale_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "locales") locale_file = os.path.join(locale_dir, f"{locale}.json") @@ -63,10 +68,17 @@ def load_strings(): with open(locale_file, "r", encoding="utf-8") as file: strings.update(json.load(file)) except (FileNotFoundError, json.JSONDecodeError, OSError): + strings = dict(_DEFAULT_STRINGS) locale = "en" strings["lang"] = locale return strings + + +def load_strings(): + return dict(load_strings_for_locale(get_locale())) + + googlebot_headers = { "User-Agent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.119 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" } @@ -74,6 +86,12 @@ googlebot_headers = { jobs = {} jobs_lock = threading.Lock() + +class UserFacingError(Exception): + def __init__(self, user_message): + super().__init__(user_message) + self.user_message = user_message + html = """ @@ -692,7 +710,7 @@ def bypass_paywall(url, strings, job_id=None): msg = strings["medium_challenge_error"] else: msg = strings["challenge_error"] - raise RuntimeError(msg) + raise UserFacingError(msg) set_step(job_id, 'process') result = add_base_tag(html_text, final_url) @@ -717,15 +735,15 @@ def fetch_worker(job_id, url, strings): jobs[job_id]['step'] = 'error' except requests.exceptions.RequestException as e: with jobs_lock: - jobs[job_id]['error'] = strings["request_error"].format(error=e) + jobs[job_id]['error'] = strings["request_error"] jobs[job_id]['step'] = 'error' - except RuntimeError as e: + except UserFacingError as error: with jobs_lock: - jobs[job_id]['error'] = str(e) + jobs[job_id]['error'] = error.user_message jobs[job_id]['step'] = 'error' except Exception as e: with jobs_lock: - jobs[job_id]['error'] = strings["unexpected_error"].format(error=e) + jobs[job_id]['error'] = strings["unexpected_error"] jobs[job_id]['step'] = 'error' @@ -782,10 +800,16 @@ def show_article(): strings = load_strings() try: return bypass_paywall(link, strings) - except requests.exceptions.RequestException as e: - return strings["request_error"].format(error=e), 400 - except Exception as e: - raise e + except requests.exceptions.Timeout: + return strings["timeout_error"], 400 + except requests.exceptions.ConnectionError: + return strings["connection_error"], 400 + except requests.exceptions.RequestException: + return strings["request_error"], 400 + except UserFacingError as error: + return error.user_message, 400 + except Exception: + return strings["unexpected_error"], 500 @app.route("/", defaults={"path": ""}) @@ -798,10 +822,16 @@ def get_article(path): actual_url = "https://" + parts[4].lstrip("/") try: return bypass_paywall(actual_url, strings) - except requests.exceptions.RequestException as e: - return strings["request_error"].format(error=e), 400 - except Exception as e: - raise e + except requests.exceptions.Timeout: + return strings["timeout_error"], 400 + except requests.exceptions.ConnectionError: + return strings["connection_error"], 400 + except requests.exceptions.RequestException: + return strings["request_error"], 400 + except UserFacingError as error: + return error.user_message, 400 + except Exception: + return strings["unexpected_error"], 500 else: return strings["invalid_url"], 400