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>
This commit is contained in:
copilot-swe-agent[bot]
2026-05-11 18:48:53 +00:00
committed by GitHub
co-authored by wasi-master
parent 6e50c9c182
commit 1cefe387ab
6 changed files with 55 additions and 25 deletions
+3 -3
View File
@@ -168,7 +168,7 @@ WantedBy=multi-user.target
## Localisation ## 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/<locale>.json`. The UI strings can be localised by setting the `LOCALE` environment variable before starting the app. Locale files live in `app/locales/<locale>.json`.
Built-in locales: Built-in locales:
@@ -179,14 +179,14 @@ Built-in locales:
Examples: Examples:
```sh ```sh
# from /home/runner/work/13ft/13ft/app # from app
LOCALE=de python portable.py LOCALE=de python portable.py
# or with gunicorn # or with gunicorn
LOCALE=fr python -m gunicorn 'portable:app' 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 ## Screenshots
Binary file not shown.
+2 -2
View File
@@ -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.", "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.", "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.", "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}", "request_error": "Die Seite konnte nicht geladen werden.",
"unexpected_error": "Unerwarteter Fehler: {error}" "unexpected_error": "Unerwarteter Fehler beim Laden der Seite."
} }
+2 -2
View File
@@ -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.", "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.", "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.", "connection_error": "Could not connect to the website. Check the URL and try again.",
"request_error": "Failed to fetch the page: {error}", "request_error": "Failed to fetch the page.",
"unexpected_error": "Unexpected error: {error}" "unexpected_error": "Unexpected error while fetching the page."
} }
+2 -2
View File
@@ -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.", "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.", "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.", "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}", "request_error": "Impossible de récupérer la page.",
"unexpected_error": "Erreur inattendue : {error}" "unexpected_error": "Erreur inattendue lors du chargement de la page."
} }
+46 -16
View File
@@ -6,6 +6,7 @@ import requests
import uuid import uuid
import threading import threading
import time import time
from functools import lru_cache
from urllib.parse import urljoin, urlparse, quote from urllib.parse import urljoin, urlparse, quote
from flask import request, Response from flask import request, Response
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
@@ -44,16 +45,20 @@ _DEFAULT_STRINGS = {
), ),
"timeout_error": "The website took too long to respond (30s timeout). Try again later.", "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.", "connection_error": "Could not connect to the website. Check the URL and try again.",
"request_error": "Failed to fetch the page: {error}", "request_error": "Failed to fetch the page.",
"unexpected_error": "Unexpected error: {error}", "unexpected_error": "Unexpected error while fetching the page.",
} }
def load_strings(): def get_locale():
locale = os.environ.get("LOCALE", "en") locale = os.environ.get("LOCALE", "en")
if not re.match(r"^[a-zA-Z0-9_-]+$", locale): if not re.match(r"^[a-zA-Z0-9_-]+$", locale):
locale = "en" locale = "en"
return locale
@lru_cache(maxsize=None)
def load_strings_for_locale(locale):
strings = dict(_DEFAULT_STRINGS) strings = dict(_DEFAULT_STRINGS)
locale_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "locales") locale_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "locales")
locale_file = os.path.join(locale_dir, f"{locale}.json") 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: with open(locale_file, "r", encoding="utf-8") as file:
strings.update(json.load(file)) strings.update(json.load(file))
except (FileNotFoundError, json.JSONDecodeError, OSError): except (FileNotFoundError, json.JSONDecodeError, OSError):
strings = dict(_DEFAULT_STRINGS)
locale = "en" locale = "en"
strings["lang"] = locale strings["lang"] = locale
return strings return strings
def load_strings():
return dict(load_strings_for_locale(get_locale()))
googlebot_headers = { 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)" "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 = {}
jobs_lock = threading.Lock() jobs_lock = threading.Lock()
class UserFacingError(Exception):
def __init__(self, user_message):
super().__init__(user_message)
self.user_message = user_message
html = """ html = """
<!DOCTYPE html> <!DOCTYPE html>
<html lang="{{ lang }}"> <html lang="{{ lang }}">
@@ -692,7 +710,7 @@ def bypass_paywall(url, strings, job_id=None):
msg = strings["medium_challenge_error"] msg = strings["medium_challenge_error"]
else: else:
msg = strings["challenge_error"] msg = strings["challenge_error"]
raise RuntimeError(msg) raise UserFacingError(msg)
set_step(job_id, 'process') set_step(job_id, 'process')
result = add_base_tag(html_text, final_url) result = add_base_tag(html_text, final_url)
@@ -717,15 +735,15 @@ def fetch_worker(job_id, url, strings):
jobs[job_id]['step'] = 'error' jobs[job_id]['step'] = 'error'
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
with jobs_lock: 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' jobs[job_id]['step'] = 'error'
except RuntimeError as e: except UserFacingError as error:
with jobs_lock: with jobs_lock:
jobs[job_id]['error'] = str(e) jobs[job_id]['error'] = error.user_message
jobs[job_id]['step'] = 'error' jobs[job_id]['step'] = 'error'
except Exception as e: except Exception as e:
with jobs_lock: 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' jobs[job_id]['step'] = 'error'
@@ -782,10 +800,16 @@ def show_article():
strings = load_strings() strings = load_strings()
try: try:
return bypass_paywall(link, strings) return bypass_paywall(link, strings)
except requests.exceptions.RequestException as e: except requests.exceptions.Timeout:
return strings["request_error"].format(error=e), 400 return strings["timeout_error"], 400
except Exception as e: except requests.exceptions.ConnectionError:
raise e 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": ""}) @app.route("/", defaults={"path": ""})
@@ -798,10 +822,16 @@ def get_article(path):
actual_url = "https://" + parts[4].lstrip("/") actual_url = "https://" + parts[4].lstrip("/")
try: try:
return bypass_paywall(actual_url, strings) return bypass_paywall(actual_url, strings)
except requests.exceptions.RequestException as e: except requests.exceptions.Timeout:
return strings["request_error"].format(error=e), 400 return strings["timeout_error"], 400
except Exception as e: except requests.exceptions.ConnectionError:
raise e 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: else:
return strings["invalid_url"], 400 return strings["invalid_url"], 400