diff --git a/README.md b/README.md index e790db1..281d177 100644 --- a/README.md +++ b/README.md @@ -166,6 +166,28 @@ 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`. + +Built-in locales: + +- `en` (default) +- `de` +- `fr` + +Examples: + +```sh +# from /home/runner/work/13ft/13ft/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. + ## Screenshots ### Step 1 diff --git a/app/locales/de.json b/app/locales/de.json new file mode 100644 index 0000000..b184ca0 --- /dev/null +++ b/app/locales/de.json @@ -0,0 +1,27 @@ +{ + "heading": "Website-Link eingeben", + "label": "Link der Website, deren Paywall Sie entfernen möchten:", + "submit": "Absenden", + "toggle_dark_mode": "Dunkelmodus umschalten", + "status_heading": "Artikel wird geladen", + "error_title": "Etwas ist schiefgelaufen", + "retry_button": "Andere URL versuchen", + "elapsed_template": "{seconds}s vergangen", + "connection_lost_error": "Verbindung zum Server verloren. Das Laden der Seite dauert möglicherweise zu lange.", + "step_connect": "Verbindung zur Website wird hergestellt...", + "step_fetch": "Seiteninhalt wird heruntergeladen...", + "step_detect": "Es wird nach Anti-Bot-Schutz gesucht...", + "step_fallback_freedium": "Freedium wird versucht (Medium-Umgehung)...", + "step_fallback_org": "Es wird ein archive.org-Schnappschuss versucht...", + "step_fallback_ph": "Es wird ein archive.today-/archive.ph-Schnappschuss versucht...", + "step_process": "Artikel wird verarbeitet...", + "step_cleanup": "Ansicht wird bereinigt und vorbereitet...", + "step_done": "Fertig!", + "invalid_url": "Ungültige URL", + "medium_challenge_error": "Medium hat eine Anti-Bot-Prüfung ausgeliefert und Freedium / archive.org / archive.today lieferten kein Ergebnis. Der Artikel ist möglicherweise noch zu neu, um archiviert worden zu sein.", + "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}" +} diff --git a/app/locales/en.json b/app/locales/en.json new file mode 100644 index 0000000..0907a34 --- /dev/null +++ b/app/locales/en.json @@ -0,0 +1,27 @@ +{ + "heading": "Enter Website Link", + "label": "Link of the website you want to remove paywall for:", + "submit": "Submit", + "toggle_dark_mode": "Toggle Dark Mode", + "status_heading": "Fetching Article", + "error_title": "Something went wrong", + "retry_button": "Try Another URL", + "elapsed_template": "{seconds}s elapsed", + "connection_lost_error": "Connection to server lost. The page may be taking too long to load.", + "step_connect": "Connecting to website...", + "step_fetch": "Downloading page content...", + "step_detect": "Checking for anti-bot challenges...", + "step_fallback_freedium": "Trying Freedium (Medium bypass)...", + "step_fallback_org": "Trying archive.org snapshot...", + "step_fallback_ph": "Trying archive.today / archive.ph snapshot...", + "step_process": "Processing article...", + "step_cleanup": "Cleaning up & preparing view...", + "step_done": "Done!", + "invalid_url": "Invalid URL", + "medium_challenge_error": "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.", + "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}" +} diff --git a/app/locales/fr.json b/app/locales/fr.json new file mode 100644 index 0000000..3aba146 --- /dev/null +++ b/app/locales/fr.json @@ -0,0 +1,27 @@ +{ + "heading": "Entrez le lien du site web", + "label": "Lien du site web dont vous souhaitez supprimer le paywall :", + "submit": "Soumettre", + "toggle_dark_mode": "Basculer en mode sombre", + "status_heading": "Récupération de l'article", + "error_title": "Une erreur s'est produite", + "retry_button": "Essayer une autre URL", + "elapsed_template": "{seconds}s écoulées", + "connection_lost_error": "Connexion au serveur perdue. Le chargement de la page prend peut-être trop de temps.", + "step_connect": "Connexion au site web...", + "step_fetch": "Téléchargement du contenu de la page...", + "step_detect": "Vérification des protections anti-bot...", + "step_fallback_freedium": "Essai de Freedium (contournement de Medium)...", + "step_fallback_org": "Essai d'un instantané archive.org...", + "step_fallback_ph": "Essai d'un instantané archive.today / archive.ph...", + "step_process": "Traitement de l'article...", + "step_cleanup": "Nettoyage et préparation de l'affichage...", + "step_done": "Terminé !", + "invalid_url": "URL invalide", + "medium_challenge_error": "Medium a présenté une protection anti-bot et Freedium / archive.org / archive.today n'ont rien renvoyé. L'article est peut-être trop récent pour avoir déjà été archivé.", + "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}" +} diff --git a/app/portable.py b/app/portable.py index 48452d5..3208896 100644 --- a/app/portable.py +++ b/app/portable.py @@ -1,3 +1,4 @@ +import json import os import flask import re @@ -10,6 +11,62 @@ from flask import request, Response from bs4 import BeautifulSoup app = flask.Flask(__name__) + +_DEFAULT_STRINGS = { + "heading": "Enter Website Link", + "label": "Link of the website you want to remove paywall for:", + "submit": "Submit", + "toggle_dark_mode": "Toggle Dark Mode", + "status_heading": "Fetching Article", + "error_title": "Something went wrong", + "retry_button": "Try Another URL", + "elapsed_template": "{seconds}s elapsed", + "connection_lost_error": "Connection to server lost. The page may be taking too long to load.", + "step_connect": "Connecting to website...", + "step_fetch": "Downloading page content...", + "step_detect": "Checking for anti-bot challenges...", + "step_fallback_freedium": "Trying Freedium (Medium bypass)...", + "step_fallback_org": "Trying archive.org snapshot...", + "step_fallback_ph": "Trying archive.today / archive.ph snapshot...", + "step_process": "Processing article...", + "step_cleanup": "Cleaning up & preparing view...", + "step_done": "Done!", + "invalid_url": "Invalid URL", + "medium_challenge_error": ( + "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." + ), + "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}", +} + + +def load_strings(): + locale = os.environ.get("LOCALE", "en") + if not re.match(r"^[a-zA-Z0-9_-]+$", locale): + locale = "en" + + 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") + + if locale != "en": + try: + with open(locale_file, "r", encoding="utf-8") as file: + strings.update(json.load(file)) + except (FileNotFoundError, json.JSONDecodeError, OSError): + locale = "en" + + strings["lang"] = locale + return strings 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)" } @@ -19,7 +76,7 @@ jobs_lock = threading.Lock() html = """ - + @@ -249,21 +306,21 @@ html = """
- +
-

Enter Website Link

- +

{{ heading }}

+ - +