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 8c99e13af1
commit 61f49f02db
No known key found for this signature in database
GPG key ID: CB5738652768F7E9
7 changed files with 181 additions and 75 deletions

View file

@ -1 +1 @@
1.18.9 1.19.0

View file

@ -17,6 +17,7 @@ Git tags: `vMAJOR.FEATURE.FIX` (e.g. `v1.8.0`). File `VERSION` omits the `v` pre
| Tag | Date (UTC) | Notes | | 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.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.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. | | **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. |

View file

@ -1,48 +1,52 @@
# taler-monitoring domain profiles # taler-monitoring domain profiles
# #
# When you add a stack, declare the three public endpoints here: # 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) # - name: what -d / TALER_DOMAIN matches (aliases allowed as extra lines)
# - bank / exchange / merchant: hostname or full https:// URL # - 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 # - local: 1 = koopa SSH/inside/e2e stack; 0 = public-only
# - landing: 1 = check /intro landings + assets; 0 = skip (no GOA-style landings) # - 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 # - 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 # Alternate file: TALER_DOMAINS_CONF=/path/to/domains.conf
# Local GOA (koopa) — landings matter # Local GOA (koopa) — landings matter
koopa bank.hacktivism.ch exchange.hacktivism.ch taler.hacktivism.ch GOA 1 1 hacktivism.ch 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 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 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 # 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 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 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 # 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 # 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 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 taler-ops.ch 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 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.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 stage.taler-ops.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 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 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.) # Franc Paysan (Infomaniak) — merchant host is monnaie.* (not taler./backend.)
# Prod: no monetary launch yet; stage uses TESTPAYSAN (public HTTPS landings + shops) # UI default: French (lang=fr) for all FP prod hostnames
lefrancpaysan.ch bank.lefrancpaysan.ch exchange.lefrancpaysan.ch monnaie.lefrancpaysan.ch - 0 0 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 lefrancpaysan.ch 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 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 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/ # Stage TESTPAYSAN — landings on / + farmer shops under stage.monnaie…/shops/
# name bank exchange merchant currency local landing [canonical] # UI default: French (lang=fr)
stage.lefrancpaysan.ch stage.bank.lefrancpaysan.ch stage.exchange.lefrancpaysan.ch stage.monnaie.lefrancpaysan.ch TESTPAYSAN 0 1 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 stage.lefrancpaysan.ch 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 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 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

View file

@ -75,25 +75,15 @@ export CONTINUE_ON_ERROR="${CONTINUE_ON_ERROR:-1}"
export AUTH401_CONTINUE="${AUTH401_CONTINUE:-${CONTINUE_ON_ERROR}}" export AUTH401_CONTINUE="${AUTH401_CONTINUE:-${CONTINUE_ON_ERROR}}"
export RUN_TIMEOUT="${RUN_TIMEOUT:-600}" export RUN_TIMEOUT="${RUN_TIMEOUT:-600}"
export TALER_DOMAIN="${TALER_DOMAIN:-hacktivism.ch}" export TALER_DOMAIN="${TALER_DOMAIN:-hacktivism.ch}"
# UI language: --lang / TALER_MON_LANG_SET=1 locks; otherwise auto by domain. # UI language: --lang / TALER_MON_LANG_SET=1 locks.
# Bare TALER_MON_LANG=en in workstation env (clementine) must NOT lock language — # Otherwise domains.conf column `lang` → TALER_DOMAIN_LANG → TALER_MON_LANG
# *lefrancpaysan* / *francpaysan* always default to fr when SET≠1 (global rule). # (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 if [ "${TALER_MON_LANG_SET:-0}" = "1" ]; then
case "${TALER_MON_LANG:-en}" in case "${TALER_MON_LANG:-en}" in
fr|FR|fra|french) export TALER_MON_LANG=fr ;; fr|FR|fra|french) export TALER_MON_LANG=fr ;;
*) export TALER_MON_LANG=en ;; *) export TALER_MON_LANG=en ;;
esac 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 fi
export TALER_MON_LANG export TALER_MON_LANG

39
i18n.sh
View file

@ -3,31 +3,37 @@
# #
# TALER_MON_LANG=en|fr # TALER_MON_LANG=en|fr
# TALER_MON_LANG_SET=1 → language is explicit (--lang only); do not override. # TALER_MON_LANG_SET=1 → language is explicit (--lang only); do not override.
# Auto (SET≠1): domain *lefrancpaysan* / *francpaysan* → fr always (global). # Auto (SET≠1), in order:
# else keep env or en. # 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 # domains.conf column `lang` (see suite domains.conf) is the global definition.
# workstation default for non-FP domains. It must NEVER lock English on FP # Bare TALER_MON_LANG=en in ~/.config must not override a profile lang=fr.
# (*lefrancpaysan* / *francpaysan*) — local laptop and host-agent both follow this.
: "${TALER_MON_LANG:=}" : "${TALER_MON_LANG:=}"
: "${TALER_MON_LANG_SET:=0}" : "${TALER_MON_LANG_SET:=0}"
: "${TALER_DOMAIN_LANG:=}"
i18n_init() { i18n_init() {
# Only TALER_MON_LANG_SET=1 is "explicit". Do not promote non-empty TALER_MON_LANG # 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. # to SET=1 — env files often ship LANG=en for the default GOA workstation.
if [ "${TALER_MON_LANG_SET:-0}" != "1" ]; then if [ "${TALER_MON_LANG_SET:-0}" != "1" ]; then
case "${TALER_DOMAIN:-}" in if [ -n "${TALER_DOMAIN_LANG:-}" ]; then
*lefrancpaysan*|*francpaysan*) TALER_MON_LANG="$TALER_DOMAIN_LANG"
TALER_MON_LANG=fr else
;; case "${TALER_DOMAIN:-}" in
*) *lefrancpaysan*|*francpaysan*)
# Non-FP: keep pre-set env value if any, else English # fallback if domain not (yet) in domains.conf
if [ -z "${TALER_MON_LANG:-}" ]; then TALER_MON_LANG=fr
TALER_MON_LANG=en ;;
fi *)
;; if [ -z "${TALER_MON_LANG:-}" ]; then
esac TALER_MON_LANG=en
fi
;;
esac
fi
fi fi
case "${TALER_MON_LANG}" in case "${TALER_MON_LANG}" in
fr|FR|fra|french) TALER_MON_LANG=fr ;; fr|FR|fra|french) TALER_MON_LANG=fr ;;
@ -35,6 +41,7 @@ i18n_init() {
esac esac
export TALER_MON_LANG export TALER_MON_LANG
export TALER_MON_LANG_SET export TALER_MON_LANG_SET
export TALER_DOMAIN_LANG
} }
i18n_tag() { i18n_tag() {

35
lib.sh
View file

@ -166,13 +166,24 @@ set_taler_stack() {
fi 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. # Load first matching profile from domains.conf.
# Fields (whitespace-separated; # comments; blank lines ignored): # 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. # Returns 0 if found, 1 if not.
load_domain_profile() { load_domain_profile() {
local want="$1" conf="${TALER_DOMAINS_CONF:-}" 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 "$want" ] || return 1
[ -n "$conf" ] && [ -f "$conf" ] || return 1 [ -n "$conf" ] && [ -f "$conf" ] || return 1
@ -189,12 +200,30 @@ load_domain_profile() {
currency="$5" currency="$5"
local_stack="${6:-0}" local_stack="${6:-0}"
landing="${7:-}" landing="${7:-}"
canon="${8:-$name}" f8="${8:-}"
f9="${9:-}"
canon="$name"
profile_lang=""
# Backward compat: old 7th field was canonical domain (contains a dot) # Backward compat: old 7th field was canonical domain (contains a dot)
if [ -n "$landing" ] && [[ "$landing" == *.* && "$landing" != "0" && "$landing" != "1" ]]; then if [ -n "$landing" ] && [[ "$landing" == *.* && "$landing" != "0" && "$landing" != "1" ]]; then
canon="$landing" canon="$landing"
landing="" landing=""
f8="${7:-}"
f9="${8:-}"
fi 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" set_taler_stack "$bank" "$exchange" "$merchant" "$currency" "$local_stack" "$landing" "$canon"
return 0 return 0
done <"$conf" done <"$conf"

View file

@ -45,35 +45,110 @@ def atomic_write_text(path: Path, text: str, encoding: str = "utf-8") -> None:
except OSError: except OSError:
pass 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: def ui_lang(explicit: str = "", host_hint: str = "") -> str:
"""en (default) or fr. """en (default) or fr.
- TALER_MON_LANG_SET=1 honour TALER_MON_LANG / explicit only (no host auto). - TALER_MON_LANG_SET=1 honour TALER_MON_LANG / explicit only.
- Else: any *lefrancpaysan* / *francpaysan* host or log hint fr - Else TALER_DOMAIN_LANG env (set by load_domain_profile).
(workstation env TALER_MON_LANG=en must not force English on FP domains). - Else domains.conf `lang` for host_hint.
- Else: explicit / env / en. - Else fallback *lefrancpaysan*/*francpaysan* fr.
- Else explicit / env / en.
""" """
import os import os
env_lang = (os.environ.get("TALER_MON_LANG") or "").strip().lower() 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() lang = (explicit or env_lang or "").strip().lower()
locked = (os.environ.get("TALER_MON_LANG_SET") or "").strip() == "1" 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: if locked:
return _norm(lang) or "en" return _norm_lang(lang) or "en"
if is_fp: 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 "fr"
return _norm(lang) or "en" return _norm_lang(lang) or "en"
def ui(lang: str, key: str, **kwargs) -> str: def ui(lang: str, key: str, **kwargs) -> str: