From c435df9728172be83a1880e5cac9b09d3936fdc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hern=C3=A2ni=20Marques?= Date: Sun, 19 Jul 2026 11:13:47 +0200 Subject: [PATCH] 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. --- README.md | 9 +- VERSION | 2 +- VERSIONS.md | 1 + domains.conf | 69 +++++------ i18n.sh | 233 ++++++++++++++++++++++++------------ lib.sh | 70 +++++++++-- site-gen/console_to_html.py | 175 +++++++++++++++++++++------ taler-monitoring.sh | 12 +- 8 files changed, 408 insertions(+), 163 deletions(-) diff --git a/README.md b/README.md index cb0700b..998c65b 100644 --- a/README.md +++ b/README.md @@ -203,9 +203,9 @@ Each monitored stack must say **what is bank, exchange, and merchant-backend**. Edit **[domains.conf](./domains.conf)** (or set `TALER_DOMAINS_CONF=`): ```text -# name bank exchange merchant-backend currency local landing lang [canonical] -taler-ops.ch bank.taler-ops.ch exchange.taler-ops.ch my.taler-ops.ch CHF 0 0 en -stage.lefrancpaysan.ch stage.bank.… stage.exchange.… stage.monnaie.… TESTPAYSAN 0 1 fr +# name bank exchange merchant currency local landing lang locale [canonical] +taler-ops.ch bank.… exchange.… my.… CHF 0 0 en de-CH +stage.lefrancpaysan.ch stage.bank.… stage.exchange.… stage.monnaie.… TESTPAYSAN 0 1 fr fr-CH ``` | Field | Meaning | @@ -217,7 +217,8 @@ stage.lefrancpaysan.ch stage.bank.… stage.exchange.… stage.monnaie.… T | **currency** | `GOA` / `KUDOS` / `CHF` / … | | **local** | `1` = koopa SSH stack; `0` = public only | | **landing** | `1` = check `/intro` landings + assets; `0` = skip (TOPS / mytops) | -| **lang** | UI default **`en`** or **`fr`** when `TALER_MON_LANG_SET≠1` (source of truth; FP rows use **`fr`**) | +| **lang** | UI language **`en`** / **`fr`** / **`de`** when `TALER_MON_LANG_SET≠1`. Stock profiles use **en** or **fr** only — **`de` is never a default** (opt-in via `--lang de` or an explicit profile edit). | +| **locale** | Regional l10n (BCP 47): **`fr-CH`** for FrancPaysan (Suisse romande); **`de-CH`** for other Swiss stacks. Aliases `ch-FR` / `ch-DE` accepted. Not the same as UI `lang`. | | **canonical** | optional label after alias (e.g. `my.taler-ops.ch` → `taler-ops.ch`) | Workstation `TALER_MON_LANG=en` does **not** override a profile with `lang=fr`. Force English on FP only with `--lang en` / `TALER_MON_LANG_SET=1`. diff --git a/VERSION b/VERSION index 66e2ae6..3989355 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.19.1 +1.20.0 diff --git a/VERSIONS.md b/VERSIONS.md index 8f975c7..76bb76b 100644 --- a/VERSIONS.md +++ b/VERSIONS.md @@ -17,6 +17,7 @@ Git tags: `vMAJOR.FEATURE.FIX` (e.g. `v1.8.0`). File `VERSION` omits the `v` pre | Tag | Date (UTC) | Notes | |-----|------------|--------| +| **v1.20.0** | 2026-07-19 | **Feature:** domains.conf **`locale`** (l10n) — FP **`fr-CH`**, others **`de-CH`** (aliases ch-FR/ch-DE); UI **`lang=de`** supported (sticky + console tags) but **never** a stock profile default; env `TALER_MON_LOCALE` / `TALER_DOMAIN_LOCALE`. | | **v1.19.1** | 2026-07-19 | **Bugfix / docs:** DEPENDENCIES — e2e requires `taler-helper-sqlite3` on mon PATH + preflight; README domains.conf documents **`lang`** column. | | **v1.19.0** | 2026-07-19 | **Feature:** domains.conf **`lang`** column (en\|fr) is the global UI language default per stack; i18n + HTML read `TALER_DOMAIN_LANG` / conf (FP profiles `lang=fr`). | | **v1.18.9** | 2026-07-19 | **Bugfix:** UI lang — *lefrancpaysan*/*francpaysan* always default **fr** unless `TALER_MON_LANG_SET=1` / `--lang`; workstation `TALER_MON_LANG=en` no longer locks English (host-agent + console_to_html). | diff --git a/domains.conf b/domains.conf index 189b1df..1002e69 100644 --- a/domains.conf +++ b/domains.conf @@ -1,52 +1,53 @@ # taler-monitoring domain profiles # # When you add a stack, declare the three public endpoints here: -# name bank exchange merchant-backend currency local landing lang [canonical] +# name bank exchange merchant-backend currency local landing lang locale [canonical] # # - name: what -d / TALER_DOMAIN matches (aliases allowed as extra lines) # - bank / exchange / merchant: hostname or full https:// URL # - currency: GOA | KUDOS | CHF | … (empty or "-" = report only) # - local: 1 = koopa SSH/inside/e2e stack; 0 = public-only # - landing: 1 = check /intro landings + assets; 0 = skip (no GOA-style landings) -# - lang: UI language default for this profile (en|fr). Source of truth for i18n -# when TALER_MON_LANG_SET≠1. Workstation env TALER_MON_LANG=en does not -# override a profile that sets fr (e.g. *lefrancpaysan*). +# - lang: UI language for mon console/HTML (en|fr|de). Source of truth when +# TALER_MON_LANG_SET≠1. Workstation TALER_MON_LANG=en does not override. +# **de is supported but must not be used as a profile default** (no row +# sets lang=de unless an operator deliberately chooses it). +# - locale: BCP 47 regional locale for l10n defaults (not the same as UI lang). +# Use **fr-CH** (Suisse romande) for FrancPaysan; **de-CH** (Deutschschweiz) +# for other Swiss / default stacks. Accepts also ch-FR / ch-DE as aliases. # - canonical: optional TALER_DOMAIN label after alias match # -# Legacy lines without lang (old 8th field = canonical only) still parse: lang→en. -# CLI overrides: --lang / TALER_MON_LANG_SET=1 +# Legacy lines without locale still parse (locale → de-CH except FP heuristic). +# CLI: --lang en|fr|de · TALER_MON_LANG_SET=1 locks UI lang # Alternate file: TALER_DOMAINS_CONF=/path/to/domains.conf -# Local GOA (koopa) — landings matter -koopa bank.hacktivism.ch exchange.hacktivism.ch taler.hacktivism.ch GOA 1 1 en hacktivism.ch -hacktivism bank.hacktivism.ch exchange.hacktivism.ch taler.hacktivism.ch GOA 1 1 en hacktivism.ch -hacktivism.ch bank.hacktivism.ch exchange.hacktivism.ch taler.hacktivism.ch GOA 1 1 en +# Local GOA (koopa) — landings matter · UI en · locale de-CH +koopa bank.hacktivism.ch exchange.hacktivism.ch taler.hacktivism.ch GOA 1 1 en de-CH hacktivism.ch +hacktivism bank.hacktivism.ch exchange.hacktivism.ch taler.hacktivism.ch GOA 1 1 en de-CH hacktivism.ch +hacktivism.ch bank.hacktivism.ch exchange.hacktivism.ch taler.hacktivism.ch GOA 1 1 en de-CH -# Public demo (KUDOS) — has intro-style pages; keep soft landing checks -taler.net bank.demo.taler.net exchange.demo.taler.net backend.demo.taler.net KUDOS 0 1 en demo.taler.net -demo.taler.net bank.demo.taler.net exchange.demo.taler.net backend.demo.taler.net KUDOS 0 1 en +# Public demo (KUDOS) +taler.net bank.demo.taler.net exchange.demo.taler.net backend.demo.taler.net KUDOS 0 1 en de-CH demo.taler.net +demo.taler.net bank.demo.taler.net exchange.demo.taler.net backend.demo.taler.net KUDOS 0 1 en de-CH -# Public test stack (TESTKUDOS) — no GOA-style landings -test.taler.net bank.test.taler.net exchange.test.taler.net backend.test.taler.net TESTKUDOS 0 0 en +# Public test stack (TESTKUDOS) +test.taler.net bank.test.taler.net exchange.test.taler.net backend.test.taler.net TESTKUDOS 0 0 en de-CH -# TOPS CHF — multi-tenant merchant my.taler-ops.ch; no public GOA landings -taler-ops.ch bank.taler-ops.ch exchange.taler-ops.ch my.taler-ops.ch CHF 0 0 en -my.taler-ops.ch bank.taler-ops.ch exchange.taler-ops.ch my.taler-ops.ch CHF 0 0 en taler-ops.ch -# Stage merchant FQDN is stage.my.taler-ops.ch (on betel); mon HTML /monitoring* stage-only -stage.taler-ops.ch bank.stage.taler-ops.ch exchange.stage.taler-ops.ch stage.my.taler-ops.ch CHF 0 0 en -stage.my.taler-ops.ch bank.stage.taler-ops.ch exchange.stage.taler-ops.ch stage.my.taler-ops.ch CHF 0 0 en stage.taler-ops.ch -my.stage.taler-ops.ch bank.stage.taler-ops.ch exchange.stage.taler-ops.ch stage.my.taler-ops.ch CHF 0 0 en stage.taler-ops.ch +# TOPS CHF +taler-ops.ch bank.taler-ops.ch exchange.taler-ops.ch my.taler-ops.ch CHF 0 0 en de-CH +my.taler-ops.ch bank.taler-ops.ch exchange.taler-ops.ch my.taler-ops.ch CHF 0 0 en de-CH taler-ops.ch +stage.taler-ops.ch bank.stage.taler-ops.ch exchange.stage.taler-ops.ch stage.my.taler-ops.ch CHF 0 0 en de-CH +stage.my.taler-ops.ch bank.stage.taler-ops.ch exchange.stage.taler-ops.ch stage.my.taler-ops.ch CHF 0 0 en de-CH stage.taler-ops.ch +my.stage.taler-ops.ch bank.stage.taler-ops.ch exchange.stage.taler-ops.ch stage.my.taler-ops.ch CHF 0 0 en de-CH stage.taler-ops.ch -# Franc Paysan (Infomaniak) — merchant host is monnaie.* (not taler./backend.) -# UI default: French (lang=fr) for all FP prod hostnames -lefrancpaysan.ch bank.lefrancpaysan.ch exchange.lefrancpaysan.ch monnaie.lefrancpaysan.ch - 0 0 fr -monnaie.lefrancpaysan.ch bank.lefrancpaysan.ch exchange.lefrancpaysan.ch monnaie.lefrancpaysan.ch - 0 0 fr lefrancpaysan.ch -bank.lefrancpaysan.ch bank.lefrancpaysan.ch exchange.lefrancpaysan.ch monnaie.lefrancpaysan.ch - 0 0 fr lefrancpaysan.ch -exchange.lefrancpaysan.ch bank.lefrancpaysan.ch exchange.lefrancpaysan.ch monnaie.lefrancpaysan.ch - 0 0 fr lefrancpaysan.ch +# Franc Paysan — UI fr · regional locale fr-CH (Suisse romande) +lefrancpaysan.ch bank.lefrancpaysan.ch exchange.lefrancpaysan.ch monnaie.lefrancpaysan.ch - 0 0 fr fr-CH +monnaie.lefrancpaysan.ch bank.lefrancpaysan.ch exchange.lefrancpaysan.ch monnaie.lefrancpaysan.ch - 0 0 fr fr-CH lefrancpaysan.ch +bank.lefrancpaysan.ch bank.lefrancpaysan.ch exchange.lefrancpaysan.ch monnaie.lefrancpaysan.ch - 0 0 fr fr-CH lefrancpaysan.ch +exchange.lefrancpaysan.ch bank.lefrancpaysan.ch exchange.lefrancpaysan.ch monnaie.lefrancpaysan.ch - 0 0 fr fr-CH lefrancpaysan.ch -# Stage TESTPAYSAN — landings on / + farmer shops under stage.monnaie…/shops/ -# UI default: French (lang=fr) -stage.lefrancpaysan.ch stage.bank.lefrancpaysan.ch stage.exchange.lefrancpaysan.ch stage.monnaie.lefrancpaysan.ch TESTPAYSAN 0 1 fr -stage.bank.lefrancpaysan.ch stage.bank.lefrancpaysan.ch stage.exchange.lefrancpaysan.ch stage.monnaie.lefrancpaysan.ch TESTPAYSAN 0 1 fr stage.lefrancpaysan.ch -stage.exchange.lefrancpaysan.ch stage.bank.lefrancpaysan.ch stage.exchange.lefrancpaysan.ch stage.monnaie.lefrancpaysan.ch TESTPAYSAN 0 1 fr stage.lefrancpaysan.ch -stage.monnaie.lefrancpaysan.ch stage.bank.lefrancpaysan.ch stage.exchange.lefrancpaysan.ch stage.monnaie.lefrancpaysan.ch TESTPAYSAN 0 1 fr stage.lefrancpaysan.ch +# Stage TESTPAYSAN — UI fr · locale fr-CH +stage.lefrancpaysan.ch stage.bank.lefrancpaysan.ch stage.exchange.lefrancpaysan.ch stage.monnaie.lefrancpaysan.ch TESTPAYSAN 0 1 fr fr-CH +stage.bank.lefrancpaysan.ch stage.bank.lefrancpaysan.ch stage.exchange.lefrancpaysan.ch stage.monnaie.lefrancpaysan.ch TESTPAYSAN 0 1 fr fr-CH stage.lefrancpaysan.ch +stage.exchange.lefrancpaysan.ch stage.bank.lefrancpaysan.ch stage.exchange.lefrancpaysan.ch stage.monnaie.lefrancpaysan.ch TESTPAYSAN 0 1 fr fr-CH stage.lefrancpaysan.ch +stage.monnaie.lefrancpaysan.ch stage.bank.lefrancpaysan.ch stage.exchange.lefrancpaysan.ch stage.monnaie.lefrancpaysan.ch TESTPAYSAN 0 1 fr fr-CH stage.lefrancpaysan.ch diff --git a/i18n.sh b/i18n.sh index c1a467f..417a751 100644 --- a/i18n.sh +++ b/i18n.sh @@ -1,19 +1,24 @@ #!/usr/bin/env bash -# i18n.sh — language for taler-monitoring console + HTML chrome +# i18n.sh — UI language + regional locale for taler-monitoring console + HTML # -# TALER_MON_LANG=en|fr -# TALER_MON_LANG_SET=1 → language is explicit (--lang only); do not override. -# Auto (SET≠1), in order: -# 1) TALER_DOMAIN_LANG from domains.conf profile (preferred — source of truth) -# 2) fallback hostname heuristic *lefrancpaysan*/*francpaysan* → fr -# 3) else keep workstation env TALER_MON_LANG or en +# UI language (mon badges / sticky chrome): +# TALER_MON_LANG=en|fr|de +# TALER_MON_LANG_SET=1 → explicit (--lang); do not override. +# Auto (SET≠1): TALER_DOMAIN_LANG from domains.conf `lang` column. +# de is supported but never a stock profile default (no domains.conf row uses lang=de). # -# domains.conf column `lang` (see suite domains.conf) is the global definition. -# Bare TALER_MON_LANG=en in ~/.config must not override a profile lang=fr. +# Regional locale (l10n defaults, BCP 47 — not the same as UI lang): +# TALER_MON_LOCALE / TALER_DOMAIN_LOCALE e.g. fr-CH, de-CH +# From domains.conf `locale` column (fr-CH for FP, de-CH for others). +# Aliases ch-FR / ch-DE accepted and normalized to fr-CH / de-CH. +# +# domains.conf is the source of truth for both lang and locale. : "${TALER_MON_LANG:=}" : "${TALER_MON_LANG_SET:=0}" : "${TALER_DOMAIN_LANG:=}" +: "${TALER_DOMAIN_LOCALE:=}" +: "${TALER_MON_LOCALE:=}" i18n_init() { # Only TALER_MON_LANG_SET=1 is "explicit". Do not promote non-empty TALER_MON_LANG @@ -24,7 +29,6 @@ i18n_init() { else case "${TALER_DOMAIN:-}" in *lefrancpaysan*|*francpaysan*) - # fallback if domain not (yet) in domains.conf TALER_MON_LANG=fr ;; *) @@ -37,89 +41,162 @@ i18n_init() { fi case "${TALER_MON_LANG}" in fr|FR|fra|french) TALER_MON_LANG=fr ;; + de|DE|deu|ger|german|deutsch) TALER_MON_LANG=de ;; *) TALER_MON_LANG=en ;; esac + + # Regional locale from profile (or FP/other fallback) + if [ -n "${TALER_DOMAIN_LOCALE:-}" ]; then + TALER_MON_LOCALE="$TALER_DOMAIN_LOCALE" + elif [ -z "${TALER_MON_LOCALE:-}" ]; then + case "${TALER_DOMAIN:-}" in + *lefrancpaysan*|*francpaysan*) TALER_MON_LOCALE=fr-CH ;; + *) TALER_MON_LOCALE=de-CH ;; + esac + fi + # normalize aliases + case "$(printf '%s' "$TALER_MON_LOCALE" | tr '[:upper:]' '[:lower:]' | tr '_' '-')" in + fr-ch|ch-fr) TALER_MON_LOCALE=fr-CH ;; + de-ch|ch-de) TALER_MON_LOCALE=de-CH ;; + en-ch|ch-en) TALER_MON_LOCALE=en-CH ;; + esac + export TALER_MON_LANG export TALER_MON_LANG_SET export TALER_DOMAIN_LANG + export TALER_DOMAIN_LOCALE + export TALER_MON_LOCALE } i18n_tag() { local t="$1" - if [ "${TALER_MON_LANG:-en}" != "fr" ]; then - printf '%s' "$t" - return 0 - fi - case "$t" in - OK) printf 'OK' ;; - ERROR) printf 'ERREUR' ;; - WARN) printf 'AVERT' ;; - INFO) printf 'INFO' ;; - BLOCKER) printf 'BLOCAGE' ;; - BLOCK) printf 'BLOC' ;; - SUMMARY) printf 'RESUME' ;; - PROG|PROGRESS) printf 'PROG' ;; - *) printf '%s' "$t" ;; + case "${TALER_MON_LANG:-en}" in + fr) + case "$t" in + OK) printf 'OK' ;; + ERROR) printf 'ERREUR' ;; + WARN) printf 'AVERT' ;; + INFO) printf 'INFO' ;; + BLOCKER) printf 'BLOCAGE' ;; + BLOCK) printf 'BLOC' ;; + SUMMARY) printf 'RESUME' ;; + PROG|PROGRESS) printf 'PROG' ;; + *) printf '%s' "$t" ;; + esac + ;; + de) + case "$t" in + OK) printf 'OK' ;; + ERROR) printf 'FEHLER' ;; + WARN) printf 'WARNUNG' ;; + INFO) printf 'INFO' ;; + BLOCKER) printf 'BLOCKER' ;; + BLOCK) printf 'BLOCK' ;; + SUMMARY) printf 'ZUSAMMENFASSUNG' ;; + PROG|PROGRESS) printf 'PROG' ;; + *) printf '%s' "$t" ;; + esac + ;; + *) + printf '%s' "$t" + ;; esac } i18n_text() { local s="$*" - if [ "${TALER_MON_LANG:-en}" != "fr" ] || [ -z "$s" ]; then - printf '%s' "$s" - return 0 - fi - case "$s" in - "numbered checks this run:") printf 'controles numerotes de cette execution :' ;; - "numbered checks:") printf 'controles numerotes :' ;; - "(global done/total)") printf '(fait/total global)' ;; - "global checks:") printf 'controles globaux :' ;; - "failed — see list above"|"failed - see list above") printf 'echec — voir la liste ci-dessus' ;; - "BLOCKERS · pay/withdraw cannot finish") printf 'BLOCAGES · paiement/retrait impossible' ;; - "BLOCKERS (pay/withdraw cannot finish)") printf 'BLOCAGES (paiement/retrait impossible)' ;; - "ERRORS · failed checks") printf 'ERREURS · controles en echec' ;; - "ERRORS (failed checks)") printf 'ERREURS (controles en echec)' ;; - "pay/withdraw cannot finish") printf 'paiement/retrait impossible a terminer' ;; - "SUMMARY") printf 'RESUME' ;; - "totals:") printf 'totaux :' ;; - "all clear") printf 'tout est clair' ;; - "warnings only (no hard fail)") printf 'avertissements seulement (pas d'\''echec dur)' ;; - "monitoring") printf 'surveillance' ;; - "surface") printf 'surface' ;; - "aptdeploy") printf 'aptdeploy' ;; - "container") printf 'conteneur' ;; - "disk") printf 'disque' ;; - "package") printf 'paquet' ;; - "version") printf 'version' ;; - "reachability") printf 'accessibilite' ;; - "server-header") printf 'en-tete Server' ;; - "inventory") printf 'inventaire' ;; - "flags") printf 'options' ;; - "target domain") printf 'domaine cible' ;; - "currency") printf 'devise' ;; - "phases") printf 'phases' ;; - "access") printf 'acces' ;; + [ -z "$s" ] && { printf '%s' "$s"; return 0; } + case "${TALER_MON_LANG:-en}" in + fr) + case "$s" in + "numbered checks this run:") printf 'controles numerotes de cette execution :' ;; + "numbered checks:") printf 'controles numerotes :' ;; + "(global done/total)") printf '(fait/total global)' ;; + "global checks:") printf 'controles globaux :' ;; + "failed — see list above"|"failed - see list above") printf 'echec — voir la liste ci-dessus' ;; + "BLOCKERS · pay/withdraw cannot finish") printf 'BLOCAGES · paiement/retrait impossible' ;; + "BLOCKERS (pay/withdraw cannot finish)") printf 'BLOCAGES (paiement/retrait impossible)' ;; + "ERRORS · failed checks") printf 'ERREURS · controles en echec' ;; + "ERRORS (failed checks)") printf 'ERREURS (controles en echec)' ;; + "pay/withdraw cannot finish") printf 'paiement/retrait impossible a terminer' ;; + "SUMMARY") printf 'RESUME' ;; + "totals:") printf 'totaux :' ;; + "all clear") printf 'tout est clair' ;; + "warnings only (no hard fail)") printf 'avertissements seulement (pas d'\''echec dur)' ;; + "monitoring") printf 'surveillance' ;; + "surface") printf 'surface' ;; + "aptdeploy") printf 'aptdeploy' ;; + "container") printf 'conteneur' ;; + "disk") printf 'disque' ;; + "package") printf 'paquet' ;; + "version") printf 'version' ;; + "reachability") printf 'accessibilite' ;; + "server-header") printf 'en-tete Server' ;; + "inventory") printf 'inventaire' ;; + "flags") printf 'options' ;; + "target domain") printf 'domaine cible' ;; + "currency") printf 'devise' ;; + "phases") printf 'phases' ;; + "access") printf 'acces' ;; + *) + local out="$s" + out=${out//shared library missing/bibliotheque partagee manquante} + out=${out//not active/non actif} + out=${out//does not resolve/ne resout pas} + out=${out//from Server header/depuis en-tete Server} + out=${out//public HTTPS/HTTPS public} + out=${out//no SSH/sans SSH} + out=${out//failed — see list above/echec — voir la liste ci-dessus} + printf '%s' "$out" + ;; + esac + ;; + de) + case "$s" in + "numbered checks this run:") printf 'Nummerierte Pruefungen dieser Ausfuehrung :' ;; + "numbered checks:") printf 'Nummerierte Pruefungen :' ;; + "(global done/total)") printf '(fertig/gesamt global)' ;; + "global checks:") printf 'Globale Pruefungen :' ;; + "failed — see list above"|"failed - see list above") printf 'fehlgeschlagen — siehe Liste oben' ;; + "BLOCKERS · pay/withdraw cannot finish") printf 'BLOCKER · Zahlung/Abhebung nicht moeglich' ;; + "BLOCKERS (pay/withdraw cannot finish)") printf 'BLOCKER (Zahlung/Abhebung nicht moeglich)' ;; + "ERRORS · failed checks") printf 'FEHLER · fehlgeschlagene Pruefungen' ;; + "ERRORS (failed checks)") printf 'FEHLER (fehlgeschlagene Pruefungen)' ;; + "pay/withdraw cannot finish") printf 'Zahlung/Abhebung kann nicht beendet werden' ;; + "SUMMARY") printf 'ZUSAMMENFASSUNG' ;; + "totals:") printf 'Summen :' ;; + "all clear") printf 'alles in Ordnung' ;; + "warnings only (no hard fail)") printf 'nur Warnungen (kein harter Fehler)' ;; + "monitoring") printf 'Ueberwachung' ;; + "surface") printf 'Surface' ;; + "aptdeploy") printf 'aptdeploy' ;; + "container") printf 'Container' ;; + "disk") printf 'Disk' ;; + "package") printf 'Paket' ;; + "version") printf 'Version' ;; + "reachability") printf 'Erreichbarkeit' ;; + "server-header") printf 'Server-Header' ;; + "inventory") printf 'Inventar' ;; + "flags") printf 'Flags' ;; + "target domain") printf 'Zieldomaene' ;; + "currency") printf 'Waehrung' ;; + "phases") printf 'Phasen' ;; + "access") printf 'Zugang' ;; + *) + local outd="$s" + outd=${outd//shared library missing/gemeinsame Bibliothek fehlt} + outd=${outd//not active/nicht aktiv} + outd=${outd//does not resolve/loest nicht auf} + outd=${outd//from Server header/aus Server-Header} + outd=${outd//public HTTPS/oeffentliches HTTPS} + outd=${outd//no SSH/ohne SSH} + outd=${outd//failed — see list above/fehlgeschlagen — siehe Liste oben} + printf '%s' "$outd" + ;; + esac + ;; *) - local out="$s" - out=${out//shared library missing/bibliotheque partagee manquante} - out=${out//shared library error/erreur de bibliotheque partagee} - out=${out//not active/non actif} - out=${out//after_start/apres_demarrage} - out=${out//catalogued but not reachable/catalogue mais inaccessible} - out=${out//does not resolve/ne resout pas} - out=${out//protocol confirmed/protocole confirme} - out=${out//catalogued service OK/service catalogue OK} - out=${out//from Server header/depuis en-tete Server} - out=${out//OSV clean/OSV sans vulnerabilite} - out=${out//actionable vuln/vuln. actionnable} - out=${out//header version · not Debian pkg/version en-tete · pas paquet Debian} - out=${out//RUN_TIMEOUT exceeded/RUN_TIMEOUT depasse} - out=${out//public HTTPS/HTTPS public} - out=${out//container status/etat du conteneur} - out=${out//remote-only/uniquement distant} - out=${out//no SSH/sans SSH} - out=${out//failed — see list above/echec — voir la liste ci-dessus} - printf '%s' "$out" + printf '%s' "$s" ;; esac } diff --git a/lib.sh b/lib.sh index f8dc297..0f92126 100755 --- a/lib.sh +++ b/lib.sh @@ -166,24 +166,53 @@ set_taler_stack() { fi } -# Normalize domains.conf lang token → en|fr (empty if not a lang token). +# Normalize domains.conf UI lang token → en|fr|de (empty if not a lang token). +# Note: de is valid for --lang / explicit profile, but no stock profile defaults to de. _domain_lang_token() { case "${1:-}" in en|EN|eng|english) printf 'en' ;; fr|FR|fra|french) printf 'fr' ;; + de|DE|deu|ger|german|deutsch) printf 'de' ;; *) printf '' ;; esac } +# Normalize regional locale → BCP 47 (fr-CH, de-CH, en-CH, …). Empty if unknown. +# Accepts fr-CH, fr_CH, ch-FR / de-CH, ch-DE (user-friendly aliases). +_domain_locale_token() { + local t="${1:-}" + t=$(printf '%s' "$t" | tr '[:upper:]' '[:lower:]' | tr '_' '-') + case "$t" in + fr-ch|ch-fr|fr_ch) printf 'fr-CH' ;; + de-ch|ch-de|de_ch) printf 'de-CH' ;; + en-ch|ch-en|en_ch) printf 'en-CH' ;; + en-us|en-gb|fr-fr|de-de|it-ch|rm-ch) + # pass through other xx-YY + printf '%s' "$t" | awk -F- '{printf "%s-%s", tolower($1), toupper($2)}' + ;; + *) + # bare xx-YY pattern + if printf '%s' "$t" | grep -qE '^[a-z]{2}-[a-z]{2}$'; then + printf '%s' "$t" | awk -F- '{printf "%s-%s", tolower($1), toupper($2)}' + else + printf '' + fi + ;; + esac +} + # Load first matching profile from domains.conf. # Fields (whitespace-separated; # comments; blank lines ignored): -# name bank exchange merchant currency local[0|1] landing[0|1] lang[en|fr] [canonical] -# Legacy: 8th field may be canonical only (no lang) — lang defaults to en. -# Sets TALER_DOMAIN_LANG from the profile (i18n source of truth when SET≠1). +# name bank exchange merchant currency local landing lang locale [canonical] +# Legacy: +# … landing lang [canonical] → locale defaulted +# … landing [canonical] → lang=en, locale defaulted +# Sets TALER_DOMAIN_LANG + TALER_DOMAIN_LOCALE from the profile. # Returns 0 if found, 1 if not. load_domain_profile() { local want="$1" conf="${TALER_DOMAINS_CONF:-}" - local line name bank exchange merchant currency local_stack landing canon profile_lang f8 f9 + local line name bank exchange merchant currency local_stack landing canon + local profile_lang profile_locale f8 f9 f10 [ -n "$want" ] || return 1 [ -n "$conf" ] && [ -f "$conf" ] || return 1 @@ -202,28 +231,53 @@ load_domain_profile() { landing="${7:-}" f8="${8:-}" f9="${9:-}" + f10="${10:-}" canon="$name" profile_lang="" + profile_locale="" # Backward compat: old 7th field was canonical domain (contains a dot) if [ -n "$landing" ] && [[ "$landing" == *.* && "$landing" != "0" && "$landing" != "1" ]]; then canon="$landing" landing="" f8="${7:-}" f9="${8:-}" + f10="${9:-}" fi if [ -n "$f8" ]; then profile_lang=$(_domain_lang_token "$f8") if [ -n "$profile_lang" ]; then - canon="${f9:-$name}" + # modern: lang [locale] [canonical] + profile_locale=$(_domain_locale_token "$f9") + if [ -n "$profile_locale" ]; then + canon="${f10:-$name}" + else + # lang + canonical (no locale column) + if [ -n "$f9" ] && [ -z "$(_domain_lang_token "$f9")" ]; then + canon="$f9" + else + canon="${f9:-$name}" + fi + profile_locale=$(_domain_locale_token "$f10") + fi else - # legacy: field 8 = canonical, optional field 9 = lang + # legacy: field 8 = canonical only canon="$f8" profile_lang=$(_domain_lang_token "$f9") + profile_locale=$(_domain_locale_token "${f10:-$f9}") + [ -z "$profile_lang" ] && profile_lang=en fi fi [ -z "$profile_lang" ] && profile_lang=en + if [ -z "$profile_locale" ]; then + # default regional locale: FP → fr-CH, else de-CH + case "$name$canon$bank$merchant" in + *lefrancpaysan*|*francpaysan*) profile_locale=fr-CH ;; + *) profile_locale=de-CH ;; + esac + fi TALER_DOMAIN_LANG="$profile_lang" - export TALER_DOMAIN_LANG + TALER_DOMAIN_LOCALE="$profile_locale" + export TALER_DOMAIN_LANG TALER_DOMAIN_LOCALE set_taler_stack "$bank" "$exchange" "$merchant" "$currency" "$local_stack" "$landing" "$canon" return 0 done <"$conf" diff --git a/site-gen/console_to_html.py b/site-gen/console_to_html.py index fe2077d..d49c839 100755 --- a/site-gen/console_to_html.py +++ b/site-gen/console_to_html.py @@ -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", diff --git a/taler-monitoring.sh b/taler-monitoring.sh index 4e08116..35c2a99 100755 --- a/taler-monitoring.sh +++ b/taler-monitoring.sh @@ -122,7 +122,7 @@ Options: --merchant URL merchant-backend base — overrides profile --currency CODE expected currency (GOA, KUDOS, CHF, …); empty = report only --no-probe do not probe alternate hosts (unknown domains only) - --lang en|fr UI/console language (sticky bar + badges) + --lang en|fr|de UI/console language (sticky bar + badges; de available, not a profile default) overrides auto (FrancPaysan→fr, else en) --ver, --version, -V print suite version, git commit, and repo URLs -h, --help @@ -165,7 +165,8 @@ Env (same meaning): SURFACE_CVE_LEVEL=warn|error bare Server-header versions default warn (Debian package versions default error) SURFACE_CATALOG=path override surface-catalog.conf - TALER_MON_LANG=en|fr same as --lang (default en; auto fr for *lefrancpaysan*) + TALER_MON_LANG=en|fr|de same as --lang (profile lang from domains.conf; de never stock default) + TALER_MON_LOCALE=fr-CH|de-CH regional l10n from domains.conf locale column Full load (GOA / hacktivism): ./taler-monitoring.sh -d hacktivism.ch full @@ -225,12 +226,13 @@ NO_PROBE=0 while [ $# -gt 0 ]; do case "$1" in --lang) - [ $# -ge 2 ] || { echo "error: --lang needs en|fr" >&2; exit 2; } + [ $# -ge 2 ] || { echo "error: --lang needs en|fr|de" >&2; exit 2; } _lang="$2" case "$_lang" in fr|FR|fra|french) TALER_MON_LANG=fr ;; + de|DE|deu|ger|german|deutsch) TALER_MON_LANG=de ;; en|EN|eng|english) TALER_MON_LANG=en ;; - *) echo "error: unknown language '$_lang' (use en or fr)" >&2; exit 2 ;; + *) echo "error: unknown language '$_lang' (use en, fr, or de)" >&2; exit 2 ;; esac TALER_MON_LANG_SET=1 export TALER_MON_LANG TALER_MON_LANG_SET @@ -547,6 +549,8 @@ elif [ "${LOCAL_STACK:-0}" = "1" ] && command -v podman >/dev/null 2>&1 \ fi printf 'target domain=%s\n' "${TALER_DOMAIN}" +printf ' ui_lang %s\n' "${TALER_MON_LANG:-en}" +printf ' locale %s\n' "${TALER_MON_LOCALE:-${TALER_DOMAIN_LOCALE:-?}}" printf ' bank %s\n' "$BANK_PUBLIC" printf ' exchange %s\n' "$EXCHANGE_PUBLIC" printf ' merchant %s\n' "$MERCHANT_PUBLIC"