From 61f49f02db1c5fc3229c4f7f5a6d94f758ab2c67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hern=C3=A2ni=20Marques?= Date: Sun, 19 Jul 2026 11:07:11 +0200 Subject: [PATCH] release 1.19.0: domains.conf lang column drives UI i18n default Per-stack en|fr lives in domains.conf; load_domain_profile sets TALER_DOMAIN_LANG. FP profiles use lang=fr; workstation LANG=en no longer overrides. HTML chrome resolves lang from the same conf. --- VERSION | 2 +- VERSIONS.md | 1 + domains.conf | 52 ++++++++-------- host-agent/run-host-report.sh | 18 ++---- i18n.sh | 39 +++++++----- lib.sh | 35 ++++++++++- site-gen/console_to_html.py | 109 ++++++++++++++++++++++++++++------ 7 files changed, 181 insertions(+), 75 deletions(-) diff --git a/VERSION b/VERSION index cafc0b7..815d5ca 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.18.9 +1.19.0 diff --git a/VERSIONS.md b/VERSIONS.md index 9f37dd4..634c6ec 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.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). | | **v1.18.8** | 2026-07-19 | **Bugfix:** e2e wallet — require `taler-helper-sqlite3` on PATH; use absolute `node` + `timeout` instead of fragile `perl -e alarm` for wcli; clearer accept-uri errors (no raw alarm-shift noise). | | **v1.18.7** | 2026-07-19 | **Bugfix:** mail/surface catalogs — TSA/Anastasis MX is **mail.anastasis.lu** (→ pixel), not non-resolving `anastasis.taler-systems.com`; add `anastasis.lu` + pixel host probes (fixes surface_err mail.*). **Also:** FP stage host-agent defaults include **e2e** + 1800s timeout; stagepaysan.env.example wires francpaysan-secrets + wallet-cli. | diff --git a/domains.conf b/domains.conf index 1165936..189b1df 100644 --- a/domains.conf +++ b/domains.conf @@ -1,48 +1,52 @@ # taler-monitoring domain profiles # # When you add a stack, declare the three public endpoints here: -# name bank exchange merchant-backend currency local landing [canonical] +# name bank exchange merchant-backend currency local landing lang [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 = report only) +# - 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*). # - canonical: optional TALER_DOMAIN label after alias match # -# CLI overrides after load: --bank --exchange --merchant --currency +# Legacy lines without lang (old 8th field = canonical only) still parse: lang→en. +# CLI overrides: --lang / TALER_MON_LANG_SET=1 # 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 hacktivism.ch -hacktivism bank.hacktivism.ch exchange.hacktivism.ch taler.hacktivism.ch GOA 1 1 hacktivism.ch -hacktivism.ch bank.hacktivism.ch exchange.hacktivism.ch taler.hacktivism.ch GOA 1 1 +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 # 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 demo.taler.net -demo.taler.net bank.demo.taler.net exchange.demo.taler.net backend.demo.taler.net KUDOS 0 1 +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 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 +test.taler.net bank.test.taler.net exchange.test.taler.net backend.test.taler.net TESTKUDOS 0 0 en # 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 -my.taler-ops.ch bank.taler-ops.ch exchange.taler-ops.ch my.taler-ops.ch CHF 0 0 taler-ops.ch +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 -stage.my.taler-ops.ch bank.stage.taler-ops.ch exchange.stage.taler-ops.ch stage.my.taler-ops.ch CHF 0 0 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 stage.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 +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 # Franc Paysan (Infomaniak) — merchant host is monnaie.* (not taler./backend.) -# Prod: no monetary launch yet; stage uses TESTPAYSAN (public HTTPS landings + shops) -lefrancpaysan.ch bank.lefrancpaysan.ch exchange.lefrancpaysan.ch monnaie.lefrancpaysan.ch - 0 0 -monnaie.lefrancpaysan.ch bank.lefrancpaysan.ch exchange.lefrancpaysan.ch monnaie.lefrancpaysan.ch - 0 0 lefrancpaysan.ch -bank.lefrancpaysan.ch bank.lefrancpaysan.ch exchange.lefrancpaysan.ch monnaie.lefrancpaysan.ch - 0 0 lefrancpaysan.ch -exchange.lefrancpaysan.ch bank.lefrancpaysan.ch exchange.lefrancpaysan.ch monnaie.lefrancpaysan.ch - 0 0 lefrancpaysan.ch +# 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 # Stage TESTPAYSAN — landings on / + farmer shops under stage.monnaie…/shops/ -# name bank exchange merchant currency local landing [canonical] -stage.lefrancpaysan.ch stage.bank.lefrancpaysan.ch stage.exchange.lefrancpaysan.ch stage.monnaie.lefrancpaysan.ch TESTPAYSAN 0 1 -stage.bank.lefrancpaysan.ch stage.bank.lefrancpaysan.ch stage.exchange.lefrancpaysan.ch stage.monnaie.lefrancpaysan.ch TESTPAYSAN 0 1 stage.lefrancpaysan.ch -stage.exchange.lefrancpaysan.ch stage.bank.lefrancpaysan.ch stage.exchange.lefrancpaysan.ch stage.monnaie.lefrancpaysan.ch TESTPAYSAN 0 1 stage.lefrancpaysan.ch -stage.monnaie.lefrancpaysan.ch stage.bank.lefrancpaysan.ch stage.exchange.lefrancpaysan.ch stage.monnaie.lefrancpaysan.ch TESTPAYSAN 0 1 stage.lefrancpaysan.ch +# 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 diff --git a/host-agent/run-host-report.sh b/host-agent/run-host-report.sh index f86b546..c34ba5b 100755 --- a/host-agent/run-host-report.sh +++ b/host-agent/run-host-report.sh @@ -75,25 +75,15 @@ export CONTINUE_ON_ERROR="${CONTINUE_ON_ERROR:-1}" export AUTH401_CONTINUE="${AUTH401_CONTINUE:-${CONTINUE_ON_ERROR}}" export RUN_TIMEOUT="${RUN_TIMEOUT:-600}" export TALER_DOMAIN="${TALER_DOMAIN:-hacktivism.ch}" -# UI language: --lang / TALER_MON_LANG_SET=1 locks; otherwise auto by domain. -# Bare TALER_MON_LANG=en in workstation env (clementine) must NOT lock language — -# *lefrancpaysan* / *francpaysan* always default to fr when SET≠1 (global rule). +# UI language: --lang / TALER_MON_LANG_SET=1 locks. +# Otherwise domains.conf column `lang` → TALER_DOMAIN_LANG → TALER_MON_LANG +# (applied in suite apply_taler_domain / i18n_init). Leave SET=0 so -d can re-apply. +# Bare workstation TALER_MON_LANG=en must not override profile lang=fr. if [ "${TALER_MON_LANG_SET:-0}" = "1" ]; then case "${TALER_MON_LANG:-en}" in fr|FR|fra|french) export TALER_MON_LANG=fr ;; *) export TALER_MON_LANG=en ;; esac -else - case "$TALER_DOMAIN" in - *lefrancpaysan*|*francpaysan*) export TALER_MON_LANG=fr ;; - *) - case "${TALER_MON_LANG:-}" in - fr|FR|fra|french) export TALER_MON_LANG=fr ;; - *) export TALER_MON_LANG=en ;; - esac - ;; - esac - # leave TALER_MON_LANG_SET unset/0 so suite i18n_init can re-apply per -d domain fi export TALER_MON_LANG diff --git a/i18n.sh b/i18n.sh index 585ab16..c1a467f 100644 --- a/i18n.sh +++ b/i18n.sh @@ -3,31 +3,37 @@ # # TALER_MON_LANG=en|fr # TALER_MON_LANG_SET=1 → language is explicit (--lang only); do not override. -# Auto (SET≠1): domain *lefrancpaysan* / *francpaysan* → fr always (global). -# else keep env or en. +# 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 # -# Important: a bare TALER_MON_LANG=en in ~/.config/taler-monitoring/env is only the -# workstation default for non-FP domains. It must NEVER lock English on FP -# (*lefrancpaysan* / *francpaysan*) — local laptop and host-agent both follow this. +# 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. : "${TALER_MON_LANG:=}" : "${TALER_MON_LANG_SET:=0}" +: "${TALER_DOMAIN_LANG:=}" i18n_init() { # Only TALER_MON_LANG_SET=1 is "explicit". Do not promote non-empty TALER_MON_LANG # to SET=1 — env files often ship LANG=en for the default GOA workstation. if [ "${TALER_MON_LANG_SET:-0}" != "1" ]; then - case "${TALER_DOMAIN:-}" in - *lefrancpaysan*|*francpaysan*) - TALER_MON_LANG=fr - ;; - *) - # Non-FP: keep pre-set env value if any, else English - if [ -z "${TALER_MON_LANG:-}" ]; then - TALER_MON_LANG=en - fi - ;; - esac + if [ -n "${TALER_DOMAIN_LANG:-}" ]; then + TALER_MON_LANG="$TALER_DOMAIN_LANG" + else + case "${TALER_DOMAIN:-}" in + *lefrancpaysan*|*francpaysan*) + # fallback if domain not (yet) in domains.conf + TALER_MON_LANG=fr + ;; + *) + if [ -z "${TALER_MON_LANG:-}" ]; then + TALER_MON_LANG=en + fi + ;; + esac + fi fi case "${TALER_MON_LANG}" in fr|FR|fra|french) TALER_MON_LANG=fr ;; @@ -35,6 +41,7 @@ i18n_init() { esac export TALER_MON_LANG export TALER_MON_LANG_SET + export TALER_DOMAIN_LANG } i18n_tag() { diff --git a/lib.sh b/lib.sh index 16a9a0e..f8dc297 100755 --- a/lib.sh +++ b/lib.sh @@ -166,13 +166,24 @@ set_taler_stack() { fi } +# Normalize domains.conf lang token → en|fr (empty if not a lang token). +_domain_lang_token() { + case "${1:-}" in + en|EN|eng|english) printf 'en' ;; + fr|FR|fra|french) printf 'fr' ;; + *) printf '' ;; + 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] [canonical_domain] +# 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). # 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 + local line name bank exchange merchant currency local_stack landing canon profile_lang f8 f9 [ -n "$want" ] || return 1 [ -n "$conf" ] && [ -f "$conf" ] || return 1 @@ -189,12 +200,30 @@ load_domain_profile() { currency="$5" local_stack="${6:-0}" landing="${7:-}" - canon="${8:-$name}" + f8="${8:-}" + f9="${9:-}" + canon="$name" + profile_lang="" # 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:-}" fi + if [ -n "$f8" ]; then + profile_lang=$(_domain_lang_token "$f8") + if [ -n "$profile_lang" ]; then + canon="${f9:-$name}" + else + # legacy: field 8 = canonical, optional field 9 = lang + canon="$f8" + profile_lang=$(_domain_lang_token "$f9") + fi + fi + [ -z "$profile_lang" ] && profile_lang=en + TALER_DOMAIN_LANG="$profile_lang" + export TALER_DOMAIN_LANG 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 6e6aa54..fe2077d 100755 --- a/site-gen/console_to_html.py +++ b/site-gen/console_to_html.py @@ -45,35 +45,110 @@ def atomic_write_text(path: Path, text: str, encoding: str = "utf-8") -> None: except OSError: pass +def _norm_lang(v: str) -> str: + v = (v or "").strip().lower() + if v in ("fr", "fra", "french"): + return "fr" + 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). + + Matches host_hint against profile name, bank, exchange, merchant, or + canonical fields. Returns '' if no profile matches. + """ + 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")) + + path = next((p for p in candidates if p.is_file()), None) + if path is None: + return "" + + best = "" + try: + text = path.read_text(encoding="utf-8", errors="replace") + except OSError: + return "" + + for raw in text.splitlines(): + line = raw.split("#", 1)[0].strip() + if not line: + continue + parts = line.split() + 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 + if len(parts) >= 8: + tok = _norm_lang(parts[7]) + if tok: + lang = tok + if len(parts) >= 9: + canon = parts[8] + else: + # legacy: field 8 = canonical + canon = parts[7] + if len(parts) >= 9: + lang = _norm_lang(parts[8]) + if not lang: + lang = "en" + 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 + if name.lower() in hint or canon.lower() in hint: + return lang + return best + + def ui_lang(explicit: str = "", host_hint: str = "") -> str: """en (default) or fr. - - TALER_MON_LANG_SET=1 → honour TALER_MON_LANG / explicit only (no host auto). - - Else: any *lefrancpaysan* / *francpaysan* host or log hint → fr - (workstation env TALER_MON_LANG=en must not force English on FP domains). - - Else: explicit / env / en. + - 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 explicit / env / en. """ import os env_lang = (os.environ.get("TALER_MON_LANG") or "").strip().lower() + domain_lang = _norm_lang(os.environ.get("TALER_DOMAIN_LANG") or "") lang = (explicit or env_lang or "").strip().lower() locked = (os.environ.get("TALER_MON_LANG_SET") or "").strip() == "1" - is_fp = bool( - re.search(r"lefrancpaysan|francpaysan", host_hint or "", re.I) - ) - - def _norm(v: str) -> str: - if v in ("fr", "fra", "french"): - return "fr" - if v in ("en", "eng", "english"): - return "en" - return "" if locked: - return _norm(lang) or "en" - if is_fp: + return _norm_lang(lang) or "en" + if domain_lang: + return domain_lang + conf_lang = domain_lang_from_conf(host_hint) + if conf_lang: + return conf_lang + if re.search(r"lefrancpaysan|francpaysan", host_hint or "", re.I): return "fr" - return _norm(lang) or "en" + return _norm_lang(lang) or "en" def ui(lang: str, key: str, **kwargs) -> str: