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.
This commit is contained in:
Hernâni Marques 2026-07-19 11:07:11 +02:00
parent d664ad0a6e
commit 793e1577b3
No known key found for this signature in database
7 changed files with 181 additions and 75 deletions

View file

@ -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: