From ef12cd0184ef16a2fe9c31d8660d5c6f72647a3e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Apr 2026 11:49:53 +0000 Subject: [PATCH 1/6] Initial plan From ce8e8c1664a389f2f1c0f38017d4a8cb87507a04 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Apr 2026 12:01:51 +0000 Subject: [PATCH 2/6] feat: make UI strings localisable via LOCALE environment variable Agent-Logs-Url: https://github.com/wasi-master/13ft/sessions/3b6e6061-c4d2-4aa1-879b-8526ccd3b1db Co-authored-by: wasi-master <63045920+wasi-master@users.noreply.github.com> --- .gitignore | 3 +++ README.md | 42 ++++++++++++++++++++++++++++++++- app/index.html | 8 +++---- app/index.py | 50 ++++++++++++++++++++++++++++++++++++--- app/locales/de.json | 7 ++++++ app/locales/en.json | 7 ++++++ app/locales/fr.json | 7 ++++++ app/portable.py | 57 ++++++++++++++++++++++++++++++++++++++------- 8 files changed, 165 insertions(+), 16 deletions(-) create mode 100644 .gitignore create mode 100644 app/locales/de.json create mode 100644 app/locales/en.json create mode 100644 app/locales/fr.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..008af35 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +__pycache__ +*.pyc +*.pyo diff --git a/README.md b/README.md index 83167f6..250083a 100644 --- a/README.md +++ b/README.md @@ -160,7 +160,47 @@ WantedBy=multi-user.target ``` -## Screenshots +## Localisation + +The UI strings can be localised by setting the `LOCALE` environment variable to a supported locale code before starting the server. Locale files live in `app/locales/.json`. + +**Built-in locales** + +| Code | Language | +|------|----------| +| `en` | English (default) | +| `de` | German | +| `fr` | French | + +**Running with a different locale** + +```sh +# Standard Python +LOCALE=de python portable.py + +# Flask dev server +LOCALE=fr FLASK_APP=app/portable.py flask run + +# Docker Compose – add to docker-compose.yaml +environment: + - LOCALE=de +``` + +**Adding a new locale** + +Create `app/locales/.json` with the following keys (any keys you omit fall back to English): + +```json +{ + "heading": "Enter Website Link", + "label": "Link of the website you want to remove paywall for:", + "submit": "Submit", + "toggle_dark_mode": "Toggle Dark Mode", + "invalid_url": "Invalid URL" +} +``` + + ### Step 1 diff --git a/app/index.html b/app/index.html index 45b6b4d..c4f4404 100644 --- a/app/index.html +++ b/app/index.html @@ -144,13 +144,13 @@
- +
-

Enter Website Link

- +

{{ heading }}

+ - +
@@ -414,6 +462,36 @@ html = """ """ +def build_steps(strings): + return [ + {"id": "connect", "label": strings["step_connect"]}, + {"id": "fetch", "label": strings["step_fetch"]}, + {"id": "detect", "label": strings["step_detect"]}, + {"id": "fallback_freedium", "label": strings["step_fallback_freedium"]}, + {"id": "fallback_org", "label": strings["step_fallback_org"]}, + {"id": "fallback_ph", "label": strings["step_fallback_ph"]}, + {"id": "process", "label": strings["step_process"]}, + {"id": "cleanup", "label": strings["step_cleanup"]}, + {"id": "done", "label": strings["step_done"]}, + ] + + +def render_main_page(): + strings = load_strings() + ui_strings = { + "error_title": strings["error_title"], + "retry_button": strings["retry_button"], + "elapsed_template": strings["elapsed_template"], + "connection_lost_error": strings["connection_lost_error"], + } + return flask.render_template_string( + html, + steps=build_steps(strings), + ui_strings=ui_strings, + **strings, + ) + + def add_base_tag(html_content, original_url): soup = BeautifulSoup(html_content, "html.parser") parsed_url = urlparse(original_url) @@ -568,7 +646,7 @@ def fetch_via_archive_ph(url, job_id=None): return None, None -def bypass_paywall(url, job_id=None): +def bypass_paywall(url, strings, job_id=None): set_step(job_id, 'connect') if not url.startswith("http"): @@ -611,17 +689,9 @@ def bypass_paywall(url, job_id=None): if not recovered: if medium: - msg = ( - "Medium served an anti-bot challenge and Freedium / archive.org / " - "archive.today all came back empty. The article may be too new to " - "have been archived yet." - ) + msg = strings["medium_challenge_error"] else: - msg = ( - "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." - ) + msg = strings["challenge_error"] raise RuntimeError(msg) set_step(job_id, 'process') @@ -631,23 +701,23 @@ def bypass_paywall(url, job_id=None): return result -def fetch_worker(job_id, url): +def fetch_worker(job_id, url, strings): try: - result = bypass_paywall(url, job_id) + result = bypass_paywall(url, strings, job_id) with jobs_lock: jobs[job_id]['result'] = result jobs[job_id]['step'] = 'done' except requests.exceptions.Timeout: with jobs_lock: - jobs[job_id]['error'] = 'The website took too long to respond (30s timeout). Try again later.' + jobs[job_id]['error'] = strings["timeout_error"] jobs[job_id]['step'] = 'error' except requests.exceptions.ConnectionError: with jobs_lock: - jobs[job_id]['error'] = 'Could not connect to the website. Check the URL and try again.' + jobs[job_id]['error'] = strings["connection_error"] jobs[job_id]['step'] = 'error' except requests.exceptions.RequestException as e: with jobs_lock: - jobs[job_id]['error'] = f'Failed to fetch the page: {e}' + jobs[job_id]['error'] = strings["request_error"].format(error=e) jobs[job_id]['step'] = 'error' except RuntimeError as e: with jobs_lock: @@ -655,24 +725,23 @@ def fetch_worker(job_id, url): jobs[job_id]['step'] = 'error' except Exception as e: with jobs_lock: - jobs[job_id]['error'] = f'Unexpected error: {e}' + jobs[job_id]['error'] = strings["unexpected_error"].format(error=e) jobs[job_id]['step'] = 'error' @app.route("/") def main_page(): - return html + return render_main_page() @app.route("/status/") def status_stream(url): - import json - + strings = load_strings() job_id = str(uuid.uuid4()) with jobs_lock: jobs[job_id] = {'step': 'queued', 'result': None, 'error': None} - thread = threading.Thread(target=fetch_worker, args=(job_id, url)) + thread = threading.Thread(target=fetch_worker, args=(job_id, url, strings)) thread.daemon = True thread.start() @@ -710,10 +779,11 @@ def status_stream(url): @app.route("/article", methods=["POST"]) def show_article(): link = flask.request.form["link"] + strings = load_strings() try: - return bypass_paywall(link) + return bypass_paywall(link, strings) except requests.exceptions.RequestException as e: - return str(e), 400 + return strings["request_error"].format(error=e), 400 except Exception as e: raise e @@ -721,18 +791,19 @@ def show_article(): @app.route("/", defaults={"path": ""}) @app.route("/", methods=["GET"]) def get_article(path): + strings = load_strings() full_url = request.url parts = full_url.split("/", 4) if len(parts) >= 5: actual_url = "https://" + parts[4].lstrip("/") try: - return bypass_paywall(actual_url) + return bypass_paywall(actual_url, strings) except requests.exceptions.RequestException as e: - return str(e), 400 + return strings["request_error"].format(error=e), 400 except Exception as e: raise e else: - return "Invalid URL", 400 + return strings["invalid_url"], 400 if __name__ == "__main__": From 9f851e320729e033ea93baf79fb05cbd2e07c4d1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 11 May 2026 18:43:54 +0000 Subject: [PATCH 5/6] 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> --- app/__pycache__/portable.cpython-312.pyc | Bin 0 -> 32335 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 app/__pycache__/portable.cpython-312.pyc diff --git a/app/__pycache__/portable.cpython-312.pyc b/app/__pycache__/portable.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8ee6010fc0a262c7969158ac7a06aea162f51c06 GIT binary patch literal 32335 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 literal 0 HcmV?d00001 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 6/6] 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