From d866a13c200629f8fe99a721918f803ad0656b2f Mon Sep 17 00:00:00 2001 From: bes-r Date: Tue, 2 Jun 2026 23:07:27 +0200 Subject: [PATCH] UI: grotere/mobielvriendelijke restyle, inklapbaar menu, taalmodule add-on --- server-up/app.py | 35 ++++++ server-up/core/i18n.py | 77 +++++++++++- server-up/templates/index.html | 214 ++++++++++++++++++++++++++------- server-up/translations/en.json | 18 ++- server-up/translations/nl.json | 18 ++- 5 files changed, 316 insertions(+), 46 deletions(-) diff --git a/server-up/app.py b/server-up/app.py index 6765cf3..e57815d 100644 --- a/server-up/app.py +++ b/server-up/app.py @@ -1012,6 +1012,41 @@ def api_i18n_lang(lang): return jsonify(strings=i18n.strings(lang)) +@app.route("/api/i18n/keys") +def api_i18n_keys(): + """Referentie: alle vertaalsleutels + Engelse waardes (voor de editor).""" + return jsonify(keys=i18n.reference_keys(), en=i18n.strings("en")) + + +@app.route("/api/i18n//raw") +def api_i18n_lang_raw(lang): + """Alleen de eigen strings van deze taal (voor bewerken).""" + return jsonify(strings=i18n.raw(lang), builtin=i18n.is_builtin(lang)) + + +@app.route("/api/i18n", methods=["POST"]) +def api_i18n_save(): + """Voeg een taal toe of werk er een bij (add-on).""" + body = request.get_json(silent=True) or {} + res = i18n.save( + code=body.get("code", ""), + name=body.get("name", ""), + flag=body.get("flag", ""), + entries=body.get("strings", {}), + ) + if res.get("ok"): + audit.log("i18n", "save", "ok", ref=res.get("code")) + return jsonify(res), (200 if res.get("ok") else 400) + + +@app.route("/api/i18n/", methods=["DELETE"]) +def api_i18n_delete(lang): + res = i18n.delete(lang) + if res.get("ok"): + audit.log("i18n", "delete", "ok", ref=lang) + return jsonify(res), (200 if res.get("ok") else 400) + + # ── Wizard ──────────────────────────────────────────────────────────────────── @app.route("/api/wizard") diff --git a/server-up/core/i18n.py b/server-up/core/i18n.py index e02881d..0f6aca6 100644 --- a/server-up/core/i18n.py +++ b/server-up/core/i18n.py @@ -1,11 +1,16 @@ -"""Internationalisation — built-in NL + EN.""" +"""Internationalisation — built-in NL + EN, plus add-on talen via UI.""" import json +import re from pathlib import Path _DIR = Path(__file__).parent.parent / "translations" _cache: dict[str, dict] = {} _langs: dict[str, dict] = {} +# Ingebouwde talen worden meegeleverd en kunnen niet verwijderd worden. +BUILTIN = frozenset({"nl", "en"}) +_CODE_RE = re.compile(r"^[a-z]{2,3}(-[a-z0-9]{2,8})?$", re.IGNORECASE) + def load(): _cache.clear() @@ -26,14 +31,82 @@ def load(): def available() -> list[dict]: if not _langs: load() - return [{"code": k, "name": v.get("name", k), "flag": v.get("flag", "")} + return [{"code": k, + "name": v.get("name", k), + "flag": v.get("flag", ""), + "builtin": k in BUILTIN, + "count": len(_cache.get(k, {}))} for k, v in _langs.items()] def strings(lang: str) -> dict: + """Samengevoegde strings: Engelse basis + taal-overrides (voor weergave).""" if not _cache: load() base = dict(_cache.get("en", {})) if lang != "en" and lang in _cache: base.update(_cache[lang]) return base + + +def raw(lang: str) -> dict: + """Alleen de eigen strings van deze taal (voor bewerken).""" + if not _cache: + load() + return dict(_cache.get(lang, {})) + + +def reference_keys() -> list[str]: + """Alle vertaalsleutels (op basis van de Engelse bron) als referentie.""" + if not _cache: + load() + return sorted(_cache.get("en", {}).keys()) + + +def is_builtin(code: str) -> bool: + return code in BUILTIN + + +def _path_for(code: str) -> Path: + return _DIR / f"{code}.json" + + +def save(code: str, name: str, flag: str, entries: dict) -> dict: + """Sla een (nieuwe of bestaande) taal op als add-on. Retourneert {ok, msg}.""" + code = (code or "").strip().lower() + if not _CODE_RE.match(code): + return {"ok": False, "msg": "Ongeldige taalcode (bv. 'de', 'fr', 'pt-br')."} + if code in BUILTIN: + return {"ok": False, "msg": f"'{code}' is een ingebouwde taal en kan niet overschreven worden."} + if not isinstance(entries, dict): + return {"ok": False, "msg": "Strings moeten een object zijn."} + + _DIR.mkdir(parents=True, exist_ok=True) + clean = {k: str(v) for k, v in entries.items() + if not str(k).startswith("_") and v not in (None, "")} + doc = {"_meta": {"code": code, + "name": (name or code).strip(), + "flag": (flag or "").strip()}} + doc.update(clean) + try: + _path_for(code).write_text( + json.dumps(doc, ensure_ascii=False, indent=2), "utf-8") + except Exception as e: + return {"ok": False, "msg": f"Schrijven mislukt: {e}"} + load() + return {"ok": True, "msg": f"Taal '{name or code}' opgeslagen.", "code": code} + + +def delete(code: str) -> dict: + code = (code or "").strip().lower() + if code in BUILTIN: + return {"ok": False, "msg": "Ingebouwde talen kunnen niet verwijderd worden."} + p = _path_for(code) + if not p.exists(): + return {"ok": False, "msg": "Taal niet gevonden."} + try: + p.unlink() + except Exception as e: + return {"ok": False, "msg": f"Verwijderen mislukt: {e}"} + load() + return {"ok": True, "msg": "Taal verwijderd."} diff --git a/server-up/templates/index.html b/server-up/templates/index.html index 7734e08..6831ef2 100644 --- a/server-up/templates/index.html +++ b/server-up/templates/index.html @@ -56,54 +56,54 @@ tailwind.config = { .log-section { color:#22d3ee; font-weight:600 } - + -
- - ServerUp - +
+ + ServerUp +
-
-