Remove language hardcoding form html and make it dynamic.

This commit is contained in:
2026-03-18 13:34:34 +01:00
parent 9ee72ae30d
commit 63ecb15e79
10 changed files with 84 additions and 11 deletions

View File

@@ -187,6 +187,9 @@ Documentation and deployment guidance in security tooling is never "done", and e
If you see a clearer way to describe installation steps, safer container defaults, better reverse-proxy examples, SELinux improvements, or a more practical demo environment, please contribute. Small improvements (typos, wording, examples) are just as valuable as code changes.
Want to add a new UI language? Copy `internal/locales/en.json`, translate all values, save it as `internal/locales/<lang>.json`, and open a pull request.
Please use a proper lowercase locale short code for `<lang>` (for example `ch`, `ch_de`, `es`, or `pt_br`).
See [`CONTRIBUTING.md`](https://github.com/swissmakers/fail2ban-ui/blob/main/CONTRIBUTING.md) for more info.

View File

@@ -1,4 +1,5 @@
{
"meta.language_name": "Català",
"page.title": "Tauler de Fail2ban UI",
"nav.dashboard": "Tauler",
"nav.filter_debug": "Depuració de Filtres",

View File

@@ -1,4 +1,5 @@
{
"meta.language_name": "Deutsch",
"page.title": "Fail2ban UI Dashboard",
"nav.dashboard": "Dashboard",
"nav.filter_debug": "Filter-Debug",

View File

@@ -1,4 +1,5 @@
{
"meta.language_name": "Schwiizerdütsch",
"page.title": "Fail2ban UI Dashboard",
"nav.dashboard": "Dashboard",
"nav.filter_debug": "Filter Debug",
@@ -207,12 +208,12 @@
"filter_debug.no_matches": "Ke Übereinstimmige gfunde.",
"settings.title": "Istellige",
"settings.general": "Allgemeini Istellige",
"settings.language": "Sprach",
"settings.language": "Dini Sprach",
"settings.server_port": "Server-Port",
"settings.server_port_placeholder": "z.B. 8080",
"settings.port_env_set": "Port wird über d PORT-Umgebigsvariable gsetzt:",
"settings.port_env_hint": "Um de Port über d Weboberflächi z ändere, entferne d PORT-Umgebigsvariable und start de Container neu.",
"settings.port_restart_hint": "⚠️ Port-Änderige erfordere ä Neustart vom Container, zum wirksam z werde.",
"settings.port_restart_hint": "⚠️ Zum übernäh vo Port-Änderige mues dr Container neugstartet wärde.",
"settings.enable_debug": "Debug-Modus aktivierä",
"settings.enable_console": "Konsolenusgab aktivierä",
"settings.console.title": "Konsolenusgab",
@@ -221,12 +222,12 @@
"settings.alert": "Alarm-Istellige",
"settings.callback_url": "Fail2ban Callback-URL",
"settings.callback_url_placeholder": "http://127.0.0.1:8080",
"settings.callback_url_hint": "Diä URL wird vo aune Fail2Ban-Instanze brucht, zum Ban-Payloads a Fail2Ban UI z sende. Für lokali Installatione bruchts dr gliich Port wie z Fail2Ban UI (z.B. http://127.0.0.1:8080). Für Reverse-Proxy-Setups sött dr TLS-verschlüssleti Endpunkt wenn müglech brücht wärde (auso z.B. https://fail2ban.example.com).",
"settings.callback_url_hint": "Z Callback wird brucht, zum Ban-/Unban-Payloads wo uf dim remote Fail2Ban passiere as Fail2Ban UI z sende. Für lokali Installatione chasch dr gliich Port wie z Fail2Ban UI (z.B. http://127.0.0.1:8080) näh. Für Reverse-Proxy-Setups sött dr TLS-verschlüssleti Endpunkt wenn müglech brücht wärde (auso z.B. https://fail2ban.example.com).",
"settings.callback_url_env_set": "Callback-URL wird über d CALLBACK_URL-Umgebigsvariable gsetzt:",
"settings.callback_url_env_hint": "Um d Callback-URL über d Weboberflächi z ändere, entfern d CALLBACK_URL-Umgebigsvariable und start dr Container neu.",
"settings.callback_secret": "Fail2ban Callback-URL Secret",
"settings.callback_secret_placeholder": "Automatisch generierts 42-Zeiche-Secret",
"settings.callback_secret.description": "Zur Authentifizierig vo Ban-Callbacks. Wird outomatisch id Fail2ban-Action-Konfiguration abegschribe.",
"settings.callback_secret.description": "Zur Authentifizierig vo de Callbacks. (Wird outomatisch id remote Fail2ban-Action-Konf synchronisiert.)",
"settings.destination_email": "Ziiu-Email (Alarmempfänger)",
"settings.destination_email_placeholder": "alerts@swissmakers.ch",
"settings.alert_countries": "Alarm-Länder",
@@ -361,7 +362,7 @@
"settings.advanced.integration": "Integration",
"settings.advanced.integration_none": "Integration uswähle",
"settings.advanced.integration_hint": "Wähl d Firewall oder Appliance, wo diä permanenti Sperreg sött dürefüehre.",
"settings.advanced.mikrotik.note": "Gib dr SSH-Zuegriff uf di Mikrotik-Router a und d Address-Lischte, wo d'Sperrige ihtreit wärde.",
"settings.advanced.mikrotik.note": "Dr SSH-Zuegang für di Mikrotik-Router, um d sperr-Addresslischtene, outomatisch z pflege.",
"settings.advanced.mikrotik.host": "Host",
"settings.advanced.mikrotik.port": "Port",
"settings.advanced.mikrotik.username": "SSH-Benutzername",

View File

@@ -1,4 +1,5 @@
{
"meta.language_name": "English",
"page.title": "Fail2ban UI Dashboard",
"nav.dashboard": "Dashboard",
"nav.filter_debug": "Filter Debug",

View File

@@ -1,4 +1,5 @@
{
"meta.language_name": "Español",
"page.title": "Panel de control Fail2ban UI",
"nav.dashboard": "Panel de control",
"nav.filter_debug": "Depuración de filtros",

View File

@@ -1,4 +1,5 @@
{
"meta.language_name": "Français",
"page.title": "Tableau de bord Fail2ban UI",
"nav.dashboard": "Tableau de bord",
"nav.filter_debug": "Débogage des filtres",

View File

@@ -1,4 +1,5 @@
{
"meta.language_name": "Italiano",
"page.title": "Cruscotto Fail2ban UI",
"nav.dashboard": "Cruscotto",
"nav.filter_debug": "Debug Filtro",

View File

@@ -74,6 +74,11 @@ type emailDetail struct {
Value string
}
type localeOption struct {
Code string
Label string
}
type githubReleaseResponse struct {
TagName string `json:"tag_name"`
}
@@ -1579,6 +1584,7 @@ func shouldAlertForCountry(country string, alertCountries []string) bool {
func renderIndexPage(c *gin.Context) {
disableExternalIP := os.Getenv("DISABLE_EXTERNAL_IP_LOOKUP") == "true" || os.Getenv("DISABLE_EXTERNAL_IP_LOOKUP") == "1"
autoDark := os.Getenv("AUTODARK") == "true" || os.Getenv("AUTODARK") == "1"
languageOptions := listLocaleOptions()
// Checks if OIDC is enabled and skip login page setting
oidcEnabled := auth.IsEnabled()
@@ -1600,11 +1606,71 @@ func renderIndexPage(c *gin.Context) {
"updateCheckEnabled": updateCheckEnabled,
"disableExternalIP": disableExternalIP,
"autoDark": autoDark,
"languageOptions": languageOptions,
"oidcEnabled": oidcEnabled,
"skipLoginPage": skipLoginPage,
})
}
func listLocaleOptions() []localeOption {
localeDir := "./internal/locales"
if _, container := os.LookupEnv("CONTAINER"); container {
localeDir = "/app/locales"
}
entries, err := os.ReadDir(localeDir)
if err != nil {
return []localeOption{{Code: "en", Label: "en"}}
}
options := make([]localeOption, 0, len(entries))
for _, entry := range entries {
if entry.IsDir() {
continue
}
name := entry.Name()
if filepath.Ext(name) != ".json" {
continue
}
code := strings.TrimSuffix(name, ".json")
if code == "" {
continue
}
label := localeLabelFromFile(filepath.Join(localeDir, name), code)
options = append(options, localeOption{
Code: code,
Label: label,
})
}
if len(options) == 0 {
return []localeOption{{Code: "en", Label: "en"}}
}
sort.Slice(options, func(i, j int) bool {
if options[i].Code == "en" {
return true
}
if options[j].Code == "en" {
return false
}
return strings.ToLower(options[i].Label) < strings.ToLower(options[j].Label)
})
return options
}
func localeLabelFromFile(path, fallback string) string {
data, err := os.ReadFile(path)
if err != nil {
return fallback
}
var translations map[string]string
if err := json.Unmarshal(data, &translations); err != nil {
return fallback
}
if label, ok := translations["meta.language_name"]; ok && strings.TrimSpace(label) != "" {
return label
}
return fallback
}
// =========================================================================
// Version
// =========================================================================

View File

@@ -280,12 +280,9 @@
<div class="mb-4">
<label for="languageSelect" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.language">Language</label>
<select id="languageSelect" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="en">English</option>
<option value="de">Deutsch</option>
<option value="es">Español</option>
<option value="fr">Français</option>
<option value="it">Italiano</option>
<option value="de_ch">Schwiizerdütsch</option>
{{range .languageOptions}}
<option value="{{.Code}}">{{.Label}}</option>
{{end}}
</select>
</div>
<div class="mb-4">