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
ef12cd0184
commit
ce8e8c1664
@@ -0,0 +1,3 @@
|
|||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
@@ -160,7 +160,47 @@ WantedBy=multi-user.target
|
|||||||
</VirtualHost>
|
</VirtualHost>
|
||||||
```
|
```
|
||||||
|
|
||||||
## 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/<locale>.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/<code>.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
|
### Step 1
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -144,13 +144,13 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="dark-mode-toggle">
|
<div class="dark-mode-toggle">
|
||||||
<input type="checkbox" id="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>
|
||||||
<form action="/article" method="post">
|
<form action="/article" method="post">
|
||||||
<h1>Enter Website Link</h1>
|
<h1>{{ heading }}</h1>
|
||||||
<label for="link">Link of the website you want to remove paywall for:</label>
|
<label for="link">{{ label }}</label>
|
||||||
<input type="text" id="link" name="link" required autofocus>
|
<input type="text" id="link" name="link" required autofocus>
|
||||||
<input type="submit" value="Submit">
|
<input type="submit" value="{{ submit }}">
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
+47
-3
@@ -1,3 +1,7 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
import requests
|
import requests
|
||||||
from flask import request
|
from flask import request
|
||||||
@@ -5,6 +9,43 @@ from bs4 import BeautifulSoup
|
|||||||
from urllib.parse import urlparse, urljoin
|
from urllib.parse import urlparse, urljoin
|
||||||
|
|
||||||
app = flask.Flask(__name__)
|
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",
|
||||||
|
"invalid_url": "Invalid URL",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def load_strings():
|
||||||
|
"""Load UI strings from a locale JSON file selected by the LOCALE env var.
|
||||||
|
|
||||||
|
Falls back to built-in English strings when the locale file is missing or
|
||||||
|
cannot be parsed. Only alphanumeric characters, hyphens, and underscores
|
||||||
|
are accepted as locale names to prevent path traversal.
|
||||||
|
"""
|
||||||
|
locale = os.environ.get("LOCALE", "en")
|
||||||
|
if not re.match(r'^[a-zA-Z0-9_-]+$', locale):
|
||||||
|
locale = "en"
|
||||||
|
|
||||||
|
if locale == "en":
|
||||||
|
return 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")
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(locale_file, "r", encoding="utf-8") as f:
|
||||||
|
loaded = json.load(f)
|
||||||
|
return {**_DEFAULT_STRINGS, **loaded}
|
||||||
|
except (FileNotFoundError, json.JSONDecodeError, OSError):
|
||||||
|
return dict(_DEFAULT_STRINGS)
|
||||||
|
|
||||||
|
|
||||||
|
strings = load_strings()
|
||||||
|
|
||||||
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)"
|
||||||
}
|
}
|
||||||
@@ -48,7 +89,10 @@ def bypass_paywall(url):
|
|||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def main_page():
|
def main_page():
|
||||||
return flask.send_from_directory(".", "index.html")
|
template_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "index.html")
|
||||||
|
with open(template_path, "r", encoding="utf-8") as f:
|
||||||
|
template = f.read()
|
||||||
|
return flask.render_template_string(template, **strings)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/article", methods=["POST"])
|
@app.route("/article", methods=["POST"])
|
||||||
@@ -73,10 +117,10 @@ def get_article(path):
|
|||||||
return bypass_paywall(actual_url)
|
return bypass_paywall(actual_url)
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
return str(e), 400
|
return str(e), 400
|
||||||
except e:
|
except Exception as e:
|
||||||
raise e
|
raise e
|
||||||
else:
|
else:
|
||||||
return "Invalid URL", 400
|
return strings["invalid_url"], 400
|
||||||
|
|
||||||
|
|
||||||
app.run(debug=False)
|
app.run(debug=False)
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"heading": "Website-Link eingeben",
|
||||||
|
"label": "Link der Website, von der Sie die Paywall entfernen möchten:",
|
||||||
|
"submit": "Absenden",
|
||||||
|
"toggle_dark_mode": "Dunkelmodus umschalten",
|
||||||
|
"invalid_url": "Ungültige URL"
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"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",
|
||||||
|
"invalid_url": "URL invalide"
|
||||||
|
}
|
||||||
+49
-8
@@ -1,3 +1,7 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
import requests
|
import requests
|
||||||
from flask import request
|
from flask import request
|
||||||
@@ -5,6 +9,43 @@ from bs4 import BeautifulSoup
|
|||||||
from urllib.parse import urlparse, urljoin
|
from urllib.parse import urlparse, urljoin
|
||||||
|
|
||||||
app = flask.Flask(__name__)
|
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",
|
||||||
|
"invalid_url": "Invalid URL",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def load_strings():
|
||||||
|
"""Load UI strings from a locale JSON file selected by the LOCALE env var.
|
||||||
|
|
||||||
|
Falls back to built-in English strings when the locale file is missing or
|
||||||
|
cannot be parsed. Only alphanumeric characters, hyphens, and underscores
|
||||||
|
are accepted as locale names to prevent path traversal.
|
||||||
|
"""
|
||||||
|
locale = os.environ.get("LOCALE", "en")
|
||||||
|
if not re.match(r'^[a-zA-Z0-9_-]+$', locale):
|
||||||
|
locale = "en"
|
||||||
|
|
||||||
|
if locale == "en":
|
||||||
|
return 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")
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(locale_file, "r", encoding="utf-8") as f:
|
||||||
|
loaded = json.load(f)
|
||||||
|
return {**_DEFAULT_STRINGS, **loaded}
|
||||||
|
except (FileNotFoundError, json.JSONDecodeError, OSError):
|
||||||
|
return dict(_DEFAULT_STRINGS)
|
||||||
|
|
||||||
|
|
||||||
|
strings = load_strings()
|
||||||
|
|
||||||
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)"
|
||||||
}
|
}
|
||||||
@@ -154,13 +195,13 @@ html = """
|
|||||||
<body>
|
<body>
|
||||||
<div class="dark-mode-toggle">
|
<div class="dark-mode-toggle">
|
||||||
<input type="checkbox" id="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>
|
||||||
<form action="/article" method="post">
|
<form action="/article" method="post">
|
||||||
<h1>Enter Website Link</h1>
|
<h1>{{ heading }}</h1>
|
||||||
<label for="link">Link of the website you want to remove paywall for:</label>
|
<label for="link">{{ label }}</label>
|
||||||
<input type="text" id="link" name="link" required autofocus>
|
<input type="text" id="link" name="link" required autofocus>
|
||||||
<input type="submit" value="Submit">
|
<input type="submit" value="{{ submit }}">
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -226,7 +267,7 @@ def bypass_paywall(url):
|
|||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def main_page():
|
def main_page():
|
||||||
return html
|
return flask.render_template_string(html, **strings)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/article", methods=["POST"])
|
@app.route("/article", methods=["POST"])
|
||||||
@@ -236,7 +277,7 @@ def show_article():
|
|||||||
return bypass_paywall(link)
|
return bypass_paywall(link)
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
return str(e), 400
|
return str(e), 400
|
||||||
except e:
|
except Exception as e:
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
|
||||||
@@ -251,10 +292,10 @@ def get_article(path):
|
|||||||
return bypass_paywall(actual_url)
|
return bypass_paywall(actual_url)
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
return str(e), 400
|
return str(e), 400
|
||||||
except e:
|
except Exception as e:
|
||||||
raise e
|
raise e
|
||||||
else:
|
else:
|
||||||
return "Invalid URL", 400
|
return strings["invalid_url"], 400
|
||||||
|
|
||||||
|
|
||||||
app.run(host="0.0.0.0", port=5000, debug=False)
|
app.run(host="0.0.0.0", port=5000, debug=False)
|
||||||
|
|||||||
Reference in New Issue
Block a user