release 1.20.0: domains.conf locale l10n + optional German UI

Add locale column (fr-CH for FP, de-CH elsewhere; ch-FR/ch-DE aliases).
Introduce UI lang=de (tags + sticky) without using de as any stock default.
This commit is contained in:
Hernâni Marques 2026-07-19 11:13:47 +02:00
parent 51b552d214
commit c7cecfc6d5
No known key found for this signature in database
8 changed files with 408 additions and 163 deletions

View file

@ -49,44 +49,61 @@ def _norm_lang(v: str) -> str:
v = (v or "").strip().lower()
if v in ("fr", "fra", "french"):
return "fr"
if v in ("de", "deu", "ger", "german", "deutsch"):
return "de"
if v in ("en", "eng", "english"):
return "en"
return ""
def domain_lang_from_conf(host_hint: str = "") -> str:
"""Look up UI lang from domains.conf (suite profile `lang` column).
def _norm_locale(v: str) -> str:
"""BCP 47 regional locale; aliases ch-FR/ch-DE → fr-CH/de-CH."""
t = (v or "").strip().lower().replace("_", "-")
aliases = {
"fr-ch": "fr-CH",
"ch-fr": "fr-CH",
"de-ch": "de-CH",
"ch-de": "de-CH",
"en-ch": "en-CH",
"ch-en": "en-CH",
}
if t in aliases:
return aliases[t]
m = re.fullmatch(r"([a-z]{2})-([a-z]{2})", t)
if m:
return f"{m.group(1)}-{m.group(2).upper()}"
return ""
Matches host_hint against profile name, bank, exchange, merchant, or
canonical fields. Returns '' if no profile matches.
"""
def _domains_conf_path() -> "object | None":
import os
from pathlib import Path
hint = (host_hint or "").lower()
if not hint.strip():
return ""
conf = os.environ.get("TALER_DOMAINS_CONF") or ""
candidates = []
if conf:
candidates.append(Path(conf))
# suite root next to site-gen/
here = Path(__file__).resolve().parent
candidates.append(here.parent / "domains.conf")
# cwd / mon root
candidates.append(Path("domains.conf"))
return next((p for p in candidates if p.is_file()), None)
path = next((p for p in candidates if p.is_file()), None)
def domain_profile_from_conf(host_hint: str = "") -> tuple[str, str]:
"""Return (lang, locale) from domains.conf for host_hint; ('','') if none."""
hint = (host_hint or "").lower()
if not hint.strip():
return "", ""
path = _domains_conf_path()
if path is None:
return ""
best = ""
return "", ""
try:
text = path.read_text(encoding="utf-8", errors="replace")
except OSError:
return ""
return "", ""
best_lang, best_loc = "", ""
for raw in text.splitlines():
line = raw.split("#", 1)[0].strip()
if not line:
@ -95,41 +112,62 @@ def domain_lang_from_conf(host_hint: str = "") -> str:
if len(parts) < 5:
continue
name, bank, exchange, merchant = parts[0], parts[1], parts[2], parts[3]
# fields: … local landing lang [canonical]
lang = ""
canon = name
lang, locale, canon = "", "", name
if len(parts) >= 8:
tok = _norm_lang(parts[7])
if tok:
lang = tok
if len(parts) >= 9:
canon = parts[8]
loc = _norm_locale(parts[8])
if loc:
locale = loc
if len(parts) >= 10:
canon = parts[9]
else:
# lang + canonical (no locale)
canon = parts[8]
if len(parts) >= 10:
locale = _norm_locale(parts[9])
else:
# legacy: field 8 = canonical
canon = parts[7]
if len(parts) >= 9:
lang = _norm_lang(parts[8])
if len(parts) >= 10:
locale = _norm_locale(parts[9])
elif len(parts) >= 9:
locale = _norm_locale(parts[8])
if not lang:
lang = "en"
if not locale:
blob = f"{name}{canon}{bank}{merchant}".lower()
locale = "fr-CH" if ("lefrancpaysan" in blob or "francpaysan" in blob) else "de-CH"
hosts = {name.lower(), bank.lower(), exchange.lower(), merchant.lower(), canon.lower()}
# strip scheme if present
hosts = {re.sub(r"^https?://", "", h).split("/")[0] for h in hosts}
if any(h and h in hint for h in hosts):
# prefer longer / more specific name match later — keep last match
# that appears in conf order; FP aliases all share fr.
best = lang
best_lang, best_loc = lang, locale
if name.lower() in hint or canon.lower() in hint:
return lang
return best
return lang, locale
return best_lang, best_loc
def domain_lang_from_conf(host_hint: str = "") -> str:
"""Look up UI lang from domains.conf (suite profile `lang` column)."""
lang, _ = domain_profile_from_conf(host_hint)
return lang
def domain_locale_from_conf(host_hint: str = "") -> str:
"""Look up regional locale from domains.conf (`locale` column)."""
_, loc = domain_profile_from_conf(host_hint)
return loc
def ui_lang(explicit: str = "", host_hint: str = "") -> str:
"""en (default) or fr.
"""en | fr | de (de never a stock profile default).
- TALER_MON_LANG_SET=1 honour TALER_MON_LANG / explicit only.
- Else TALER_DOMAIN_LANG env (set by load_domain_profile).
- Else domains.conf `lang` for host_hint.
- Else fallback *lefrancpaysan*/*francpaysan* fr.
- Else TALER_DOMAIN_LANG / domains.conf `lang`.
- Else fallback *lefrancpaysan* fr.
- Else explicit / env / en.
"""
import os
@ -151,8 +189,24 @@ def ui_lang(explicit: str = "", host_hint: str = "") -> str:
return _norm_lang(lang) or "en"
def ui_locale(host_hint: str = "") -> str:
"""Regional locale fr-CH / de-CH from env or domains.conf."""
import os
for key in ("TALER_MON_LOCALE", "TALER_DOMAIN_LOCALE"):
loc = _norm_locale(os.environ.get(key) or "")
if loc:
return loc
conf = domain_locale_from_conf(host_hint)
if conf:
return conf
if re.search(r"lefrancpaysan|francpaysan", host_hint or "", re.I):
return "fr-CH"
return "de-CH"
def ui(lang: str, key: str, **kwargs) -> str:
"""UI chrome strings for sticky bar (English default; French for FrancPaysan)."""
"""UI chrome strings (en default; fr for FP; de available via --lang de, never stock default)."""
en = {
"errors": "ERRORS",
"warnings": "WARNINGS",
@ -373,7 +427,60 @@ def ui(lang: str, key: str, **kwargs) -> str:
"ago_d": "il y a {n} j",
"age_title": "âge de la page · mise à jour chaque seconde",
}
table = fr if lang == "fr" else en
# German sticky chrome only (full pack falls back to en for missing keys).
# Not used as any domains.conf default — operators may pass --lang de.
de = {
"errors": "FEHLER",
"warnings": "WARNUNGEN",
"ok": "OK",
"error_one": "{n} Fehler",
"error_many": "{n} Fehler",
"warn_one": "{n} Warnung",
"warn_many": "{n} Warnungen",
"jump_first_error": "Zum ersten Fehler",
"jump_first_warn": "Zur ersten Warnung",
"filter_errors": "Filter: Fehler + Kontext",
"filter_warns": "Filter: Warnungen + Kontext",
"filter_infos": "Filter: Info + Kontext",
"filter_oks": "Filter: OK + Kontext",
"filter_clear": "Filter loeschen · alle Zeilen",
"filter_active_err": "Filter: Fehler + Kontext",
"filter_active_warn": "Filter: Warnungen + Kontext",
"filter_active_info": "Filter: Info + Kontext",
"filter_active_ok": "Filter: nur OK + Kontext",
"filter_show_all": "Alle anzeigen",
"info_one": "{n} Info",
"info_many": "{n} Info",
"ok_one": "{n} ok",
"ok_many": "{n} ok",
"env_context": "Monitoring-Umgebung",
"env_context_title": "Aufklappen: Agent-Env, Suite-Pin, Domain-Hosts und Flags",
"env_context_empty": "Kein Lauf-Header im Log",
"generated": "erzeugt",
"version": "Version",
"version_tree_link": "Git-Baum dieses Laufs oeffnen (Commit)",
"suite_version_title": "taler-monitoring Suite-Version · genauer Lauf-Baum",
"source": "Quelle",
"what_monitors": "Was diese Seite ueberwacht",
"what_monitors_title": "Aufklappen: was diese Seite prueft",
"stack_title": "Stack-Monitoring · {host}",
"stack_summary": "{phases} · public + inside",
"mode_ok": "OK",
"mode_err": "ERR",
"mode_redirect": "REDIR",
"footer": "Konsolen-Darstellung von taler-monitoring. Sticky: gruen = OK · gelb = Warnungen · rot = Fehler.",
"ago_s": "vor {n}s",
"ago_m": "vor {n} Min.",
"ago_h": "vor {n} Std.",
"ago_d": "vor {n} T.",
"age_title": "Seitenalter · jede Sekunde aktualisiert",
}
if lang == "fr":
table = fr
elif lang == "de":
table = de
else:
table = en
s = table.get(key) or en.get(key) or key
try:
return s.format(**kwargs)
@ -2489,7 +2596,7 @@ def main() -> None:
ap.add_argument(
"--lang",
default="",
help="UI language en|fr (default: env TALER_MON_LANG or auto)",
help="UI language en|fr|de (default: domains.conf lang / env; de never stock default)",
)
ap.add_argument(
"--page-label",