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>
This commit is contained in:
co-authored by
wasi-master
parent
b6b831f156
commit
e7669b7e08
+118
-47
@@ -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 = """
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="{{ lang }}">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
@@ -249,21 +306,21 @@ html = """
|
||||
<body>
|
||||
<div class="dark-mode-toggle">
|
||||
<input type="checkbox" id="dark-mode-toggle">
|
||||
<label for="dark-mode-toggle" title="Toggle Dark Mode"></label>
|
||||
<label for="dark-mode-toggle" title="{{ toggle_dark_mode }}"></label>
|
||||
</div>
|
||||
|
||||
<div id="form-view">
|
||||
<form id="url-form">
|
||||
<h1>Enter Website Link</h1>
|
||||
<label for="link">Link of the website you want to remove paywall for:</label>
|
||||
<h1>{{ heading }}</h1>
|
||||
<label for="link">{{ label }}</label>
|
||||
<input type="text" id="link" name="link" required autofocus>
|
||||
<input type="submit" value="Submit">
|
||||
<input type="submit" value="{{ submit }}">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="status-view" style="display:none;">
|
||||
<div class="status-container">
|
||||
<h1>Fetching Article</h1>
|
||||
<h1>{{ status_heading }}</h1>
|
||||
<div class="url-display" id="status-url"></div>
|
||||
<div class="progress-bar-track">
|
||||
<div class="progress-bar-fill" id="progress-fill"></div>
|
||||
@@ -293,17 +350,8 @@ html = """
|
||||
}
|
||||
});
|
||||
|
||||
const STEPS = [
|
||||
{ id: 'connect', label: 'Connecting to website...' },
|
||||
{ id: 'fetch', label: 'Downloading page content...' },
|
||||
{ id: 'detect', label: 'Checking for anti-bot challenges...' },
|
||||
{ id: 'fallback_freedium', label: 'Trying Freedium (Medium bypass)...' },
|
||||
{ id: 'fallback_org', label: 'Trying archive.org snapshot...' },
|
||||
{ id: 'fallback_ph', label: 'Trying archive.today / archive.ph snapshot...' },
|
||||
{ id: 'process', label: 'Processing article...' },
|
||||
{ id: 'cleanup', label: 'Cleaning up & preparing view...' },
|
||||
{ id: 'done', label: 'Done!' }
|
||||
];
|
||||
const UI_STRINGS = {{ ui_strings | tojson }};
|
||||
const STEPS = {{ steps | tojson }};
|
||||
|
||||
function renderSteps(activeIdx) {
|
||||
const container = document.getElementById('steps-list');
|
||||
@@ -343,14 +391,14 @@ html = """
|
||||
const box = document.createElement('div');
|
||||
box.className = 'error-box';
|
||||
const strong = document.createElement('strong');
|
||||
strong.textContent = 'Something went wrong';
|
||||
strong.textContent = UI_STRINGS.error_title;
|
||||
box.appendChild(strong);
|
||||
box.appendChild(document.createTextNode(message));
|
||||
area.appendChild(box);
|
||||
const retry = document.createElement('a');
|
||||
retry.href = '/';
|
||||
retry.className = 'retry-btn';
|
||||
retry.textContent = 'Try Another URL';
|
||||
retry.textContent = UI_STRINGS.retry_button;
|
||||
area.appendChild(retry);
|
||||
}
|
||||
|
||||
@@ -366,7 +414,7 @@ html = """
|
||||
|
||||
const timer = setInterval(() => {
|
||||
const sec = ((Date.now() - startTime) / 1000).toFixed(0);
|
||||
elapsedEl.textContent = sec + 's elapsed';
|
||||
elapsedEl.textContent = UI_STRINGS.elapsed_template.replace('{seconds}', sec);
|
||||
}, 500);
|
||||
|
||||
const evtSource = new EventSource('/status/' + encodeURIComponent(link));
|
||||
@@ -404,7 +452,7 @@ html = """
|
||||
evtSource.onerror = function() {
|
||||
evtSource.close();
|
||||
clearInterval(timer);
|
||||
showError('Connection to server lost. The page may be taking too long to load.');
|
||||
showError(UI_STRINGS.connection_lost_error);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
@@ -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/<path:url>")
|
||||
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("/<path:path>", 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__":
|
||||
|
||||
Reference in New Issue
Block a user