ipadm: translate to English, fix help alignment

- All ipadm-internal messages (usage, prompts, errors) are now English
- Usage text is built programmatically from a (left, desc) row list with
  computed column width instead of hand-aligned spaces, so it can't drift
  out of alignment again when rows are added/changed
- README's literal usage block updated to match; rest of the repo docs
  stay German
This commit is contained in:
2026-08-18 11:49:47 +02:00
parent 8591223aca
commit 1a39f54858
9 changed files with 137 additions and 99 deletions
+92 -56
View File
@@ -16,23 +16,59 @@ import (
const version = "1.1.0"
const usage = `ipadm - dhcp/dns management (dnsmasq backend) v` + version + `
type usageRow struct {
left string
desc string
wrap bool // put desc on its own line, indented to the description column
}
usage: ipadm <hostname> add/edit host (interactive)
ipadm -l list hosts
ipadm -a [-f] <hostname> <ip address> [mac address] add host
ipadm -c <hostname> <comment> set comment
ipadm -i <hostname> <ip address> change ip address
ipadm -m <hostname> <mac address> change mac address
ipadm -r <old hostname> <new hostname> rename host
ipadm -d <hostname> delete host
ipadm -pa [-f] <hostname> <wan-port> [lan-port] [tcp|udp|both]
add port-forward to a known host
ipadm -pl list port-forwards
ipadm -pd <wan-port> [tcp|udp|both] delete port-forward
ipadm -u update (regenerate dnsmasq+nftables config, reload)
ipadm -h this help
`
var usageRows = []usageRow{
{"ipadm <hostname>", "add/edit host (interactive)", false},
{"ipadm -l", "list hosts", false},
{"ipadm -a [-f] <hostname> <ip address> [mac address]", "add host", false},
{"ipadm -c <hostname> <comment>", "set comment", false},
{"ipadm -i <hostname> <ip address>", "change ip address", false},
{"ipadm -m <hostname> <mac address>", "change mac address", false},
{"ipadm -r <old hostname> <new hostname>", "rename host", false},
{"ipadm -d <hostname>", "delete host", false},
{"ipadm -pa [-f] <hostname> <wan-port> [lan-port] [tcp|udp|both]", "add port-forward to a known host", true},
{"ipadm -pl", "list port-forwards", false},
{"ipadm -pd <wan-port> [tcp|udp|both]", "delete port-forward", false},
{"ipadm -u", "update (regenerate dnsmasq+nftables config, reload)", false},
{"ipadm -h", "this help", false},
}
// buildUsage lays out usageRows in two aligned columns. Rows too long to
// share a line with their description (marked wrap) get the description on
// the next line instead, so one oversized entry can't drag every other
// line's alignment along with it.
func buildUsage() string {
const gap = 3
width := 0
for _, r := range usageRows {
if !r.wrap && len(r.left) > width {
width = len(r.left)
}
}
var b strings.Builder
fmt.Fprintf(&b, "ipadm - dhcp/dns management (dnsmasq backend) v%s\n\n", version)
for i, r := range usageRows {
prefix := " "
if i == 0 {
prefix = "usage: "
}
if r.wrap {
fmt.Fprintf(&b, "%s%s\n", prefix, r.left)
fmt.Fprintf(&b, "%s%s%s\n", prefix, strings.Repeat(" ", width+gap), r.desc)
} else {
fmt.Fprintf(&b, "%s%-*s%s%s\n", prefix, width, r.left, strings.Repeat(" ", gap), r.desc)
}
}
return b.String()
}
var usage = buildUsage()
// netConfig describes the LAN/WAN the tool validates against. Overridable
// via env vars, mainly so this can be tested without touching the real
@@ -118,7 +154,7 @@ func main() {
cmdPortFwdDelete(args[1:])
default:
if strings.HasPrefix(args[0], "-") {
fail("unbekannte Option %q\n\n%s", args[0], usage)
fail("unknown option %q\n\n%s", args[0], usage)
}
requireArgs(args[1:], 0, "ipadm <hostname>")
cmdInteractive(args[0])
@@ -127,7 +163,7 @@ func main() {
func requireArgs(rest []string, want int, form string) {
if len(rest) != want {
fail("falsche Anzahl Argumente für %q\n\n%s", form, usage)
fail("wrong number of arguments for %q\n\n%s", form, usage)
}
}
@@ -153,10 +189,10 @@ func withStore(fn func(hosts []Host) ([]Host, string, error)) {
sortHosts(newHosts)
if err := s.Save(newHosts); err != nil {
fail("kann %s nicht schreiben: %s", dbPath(), err)
fail("cannot write %s: %s", dbPath(), err)
}
fmt.Println(summary)
fmt.Println("Hinweis: `ipadm -u` ausführen, um dnsmasq/nftables zu aktualisieren.")
fmt.Println("Note: run `ipadm -u` to apply this to dnsmasq/nftables.")
}
// withPortFwdStore mirrors withStore for the port-forward DB. It also loads
@@ -190,10 +226,10 @@ func withPortFwdStore(fn func(hosts []Host, fwds []PortForward) ([]PortForward,
sortPortFwds(newFwds)
if err := s.Save(newFwds); err != nil {
fail("kann %s nicht schreiben: %s", portFwdDBPath(), err)
fail("cannot write %s: %s", portFwdDBPath(), err)
}
fmt.Println(summary)
fmt.Println("Hinweis: `ipadm -u` ausführen, um nftables zu aktualisieren.")
fmt.Println("Note: run `ipadm -u` to apply this to nftables.")
}
func cmdInteractive(name string) {
@@ -203,7 +239,7 @@ func cmdInteractive(name string) {
if err != nil {
return nil, "", err
}
return newHosts, fmt.Sprintf("Host %q gespeichert.", name), nil
return newHosts, fmt.Sprintf("Host %q saved.", name), nil
})
}
@@ -218,7 +254,7 @@ func cmdList() {
fail("%s", err)
}
if len(hosts) == 0 {
fmt.Println("keine Hosts eingetragen")
fmt.Println("no hosts configured")
return
}
sortHosts(hosts)
@@ -264,22 +300,22 @@ func cmdAdd(args []string) {
idx := findHost(hosts, name)
if idx >= 0 && !force {
return nil, "", fmt.Errorf("Host %q existiert bereits (benutze -f zum Überschreiben oder `ipadm %s` zum Bearbeiten)", name, name)
return nil, "", fmt.Errorf("host %q already exists (use -f to overwrite, or `ipadm %s` to edit it)", name, name)
}
if other := findByIP(hosts, ip, idx); other >= 0 {
return nil, "", fmt.Errorf("IP %s ist bereits Host %q zugewiesen", ip, hosts[other].Name)
return nil, "", fmt.Errorf("IP %s is already assigned to host %q", ip, hosts[other].Name)
}
if other := findByMAC(hosts, normMac, idx); other >= 0 {
return nil, "", fmt.Errorf("MAC %s ist bereits Host %q zugewiesen", normMac, hosts[other].Name)
return nil, "", fmt.Errorf("MAC %s is already assigned to host %q", normMac, hosts[other].Name)
}
h := Host{Name: name, IP: ip, MAC: normMac}
if idx >= 0 {
hosts[idx] = h
return hosts, fmt.Sprintf("Host %q überschrieben (-f).", name), nil
return hosts, fmt.Sprintf("Host %q overwritten (-f).", name), nil
}
hosts = append(hosts, h)
return hosts, fmt.Sprintf("Host %q hinzugefügt.", name), nil
return hosts, fmt.Sprintf("Host %q added.", name), nil
})
}
@@ -292,10 +328,10 @@ func cmdComment(args []string) {
withStore(func(hosts []Host) ([]Host, string, error) {
idx := findHost(hosts, name)
if idx < 0 {
return nil, "", fmt.Errorf("Host %q nicht gefunden", name)
return nil, "", fmt.Errorf("host %q not found", name)
}
hosts[idx].Comment = comment
return hosts, fmt.Sprintf("Kommentar von %q gesetzt.", name), nil
return hosts, fmt.Sprintf("Comment set for %q.", name), nil
})
}
@@ -308,16 +344,16 @@ func cmdChangeIP(args []string) {
withStore(func(hosts []Host) ([]Host, string, error) {
idx := findHost(hosts, name)
if idx < 0 {
return nil, "", fmt.Errorf("Host %q nicht gefunden", name)
return nil, "", fmt.Errorf("host %q not found", name)
}
if err := validateIP(ip, cfg.lanCIDR, cfg.poolStart, cfg.poolEnd); err != nil {
return nil, "", err
}
if other := findByIP(hosts, ip, idx); other >= 0 {
return nil, "", fmt.Errorf("IP %s ist bereits Host %q zugewiesen", ip, hosts[other].Name)
return nil, "", fmt.Errorf("IP %s is already assigned to host %q", ip, hosts[other].Name)
}
hosts[idx].IP = ip
return hosts, fmt.Sprintf("IP von %q auf %s geändert.", name, ip), nil
return hosts, fmt.Sprintf("IP of %q changed to %s.", name, ip), nil
})
}
@@ -332,21 +368,21 @@ func cmdChangeMAC(args []string) {
withStore(func(hosts []Host) ([]Host, string, error) {
idx := findHost(hosts, name)
if idx < 0 {
return nil, "", fmt.Errorf("Host %q nicht gefunden", name)
return nil, "", fmt.Errorf("host %q not found", name)
}
normMac, err := normalizeMAC(mac)
if err != nil {
return nil, "", err
}
if other := findByMAC(hosts, normMac, idx); other >= 0 {
return nil, "", fmt.Errorf("MAC %s ist bereits Host %q zugewiesen", normMac, hosts[other].Name)
return nil, "", fmt.Errorf("MAC %s is already assigned to host %q", normMac, hosts[other].Name)
}
hosts[idx].MAC = normMac
label := normMac
if label == "" {
label = "(entfernt)"
label = "(removed)"
}
return hosts, fmt.Sprintf("MAC von %q auf %s geändert.", name, label), nil
return hosts, fmt.Sprintf("MAC of %q changed to %s.", name, label), nil
})
}
@@ -361,13 +397,13 @@ func cmdRename(args []string) {
}
idx := findHost(hosts, oldName)
if idx < 0 {
return nil, "", fmt.Errorf("Host %q nicht gefunden", oldName)
return nil, "", fmt.Errorf("host %q not found", oldName)
}
if existing := findHost(hosts, newName); existing >= 0 && existing != idx {
return nil, "", fmt.Errorf("Host %q existiert bereits", newName)
return nil, "", fmt.Errorf("host %q already exists", newName)
}
hosts[idx].Name = newName
return hosts, fmt.Sprintf("Host %q umbenannt in %q.", oldName, newName), nil
return hosts, fmt.Sprintf("Host %q renamed to %q.", oldName, newName), nil
})
}
@@ -379,10 +415,10 @@ func cmdDelete(args []string) {
withStore(func(hosts []Host) ([]Host, string, error) {
idx := findHost(hosts, name)
if idx < 0 {
return nil, "", fmt.Errorf("Host %q nicht gefunden", name)
return nil, "", fmt.Errorf("host %q not found", name)
}
hosts = append(hosts[:idx], hosts[idx+1:]...)
return hosts, fmt.Sprintf("Host %q gelöscht.", name), nil
return hosts, fmt.Sprintf("Host %q deleted.", name), nil
})
}
@@ -403,7 +439,7 @@ func cmdPortFwdAdd(args []string) {
withPortFwdStore(func(hosts []Host, fwds []PortForward) ([]PortForward, string, error) {
if findHost(hosts, name) < 0 {
return nil, "", fmt.Errorf("Host %q ist nicht in der ipadm-Datenbank (erst mit `ipadm %s` oder `ipadm -a` anlegen)", name, name)
return nil, "", fmt.Errorf("host %q is not in the ipadm database (add it first with `ipadm %s` or `ipadm -a`)", name, name)
}
lanPort, proto, err := classifyPortFwdArgs(args[2:], wanPort)
if err != nil {
@@ -418,14 +454,14 @@ func cmdPortFwdAdd(args []string) {
conflicts := findConflictingPortFwds(fwds, wanPort, proto, -1)
if len(conflicts) > 0 && !force {
return nil, "", fmt.Errorf("wan-port %d/%s kollidiert mit bestehendem Port-Forward (benutze -f zum Überschreiben)", wanPort, proto)
return nil, "", fmt.Errorf("wan-port %d/%s conflicts with an existing port-forward (use -f to overwrite)", wanPort, proto)
}
for i := len(conflicts) - 1; i >= 0; i-- {
fwds = append(fwds[:conflicts[i]], fwds[conflicts[i]+1:]...)
}
fwds = append(fwds, PortForward{WanPort: wanPort, Proto: proto, Host: name, LanPort: lanPort})
return fwds, fmt.Sprintf("Port-Forward %d/%s -> %s:%d angelegt.", wanPort, proto, name, lanPort), nil
return fwds, fmt.Sprintf("Port-forward %d/%s -> %s:%d added.", wanPort, proto, name, lanPort), nil
})
}
@@ -454,7 +490,7 @@ func cmdPortFwdList() {
fail("%s", err)
}
if len(fwds) == 0 {
fmt.Println("keine Port-Forwards eingetragen")
fmt.Println("no port-forwards configured")
return
}
sortPortFwds(fwds)
@@ -463,7 +499,7 @@ func cmdPortFwdList() {
for _, f := range fwds {
ip, ok := ipByHost[strings.ToLower(f.Host)]
if !ok {
ip = "??? (Host fehlt)"
ip = "??? (host missing)"
}
fmt.Fprintf(tw, "%d\t%s\t%s\t%s\t%d\n", f.WanPort, f.Proto, f.Host, ip, f.LanPort)
}
@@ -482,33 +518,33 @@ func cmdPortFwdDelete(args []string) {
if len(args) == 2 {
proto = args[1]
if !validProtos[proto] {
fail("ungültiges Protokoll %q (erwarte tcp/udp/both)", proto)
fail("invalid protocol %q (expected tcp/udp/both)", proto)
}
}
withPortFwdStore(func(hosts []Host, fwds []PortForward) ([]PortForward, string, error) {
matches := findPortFwd(fwds, wanPort, proto)
if len(matches) == 0 {
return nil, "", fmt.Errorf("kein Port-Forward für wan-port %d gefunden", wanPort)
return nil, "", fmt.Errorf("no port-forward found for wan-port %d", wanPort)
}
if len(matches) > 1 {
var protos []string
for _, i := range matches {
protos = append(protos, fwds[i].Proto)
}
return nil, "", fmt.Errorf("wan-port %d ist mehrdeutig (Protokolle: %s) — bitte Protokoll angeben", wanPort, strings.Join(protos, ", "))
return nil, "", fmt.Errorf("wan-port %d is ambiguous (protocols: %s) — please specify a protocol", wanPort, strings.Join(protos, ", "))
}
idx := matches[0]
deleted := fwds[idx]
fwds = append(fwds[:idx], fwds[idx+1:]...)
return fwds, fmt.Sprintf("Port-Forward %d/%s (-> %s:%d) gelöscht.", deleted.WanPort, deleted.Proto, deleted.Host, deleted.LanPort), nil
return fwds, fmt.Sprintf("Port-forward %d/%s (-> %s:%d) deleted.", deleted.WanPort, deleted.Proto, deleted.Host, deleted.LanPort), nil
})
}
func parsePort(s string) (int, error) {
var port int
if _, err := fmt.Sscanf(s, "%d", &port); err != nil {
return 0, fmt.Errorf("ungültiger Port: %q", s)
return 0, fmt.Errorf("invalid port: %q", s)
}
return port, nil
}
@@ -530,7 +566,7 @@ func cmdUpdate() {
if err := applyDnsmasqConfig(dnsmasqHostsPath(), dnsContent); err != nil {
fail("%s", err)
}
fmt.Printf("%s aktualisiert, dnsmasq neu geladen (%d Hosts).\n", dnsmasqHostsPath(), len(hosts))
fmt.Printf("%s updated, dnsmasq reloaded (%d hosts).\n", dnsmasqHostsPath(), len(hosts))
fs, err := openPortFwdStore(portFwdDBPath())
if err != nil {
@@ -544,10 +580,10 @@ func cmdUpdate() {
nftContent, warnings := renderPortForwards(hosts, fwds, cfg.wanIface)
for _, w := range warnings {
fmt.Fprintln(os.Stderr, "warnung:", w)
fmt.Fprintln(os.Stderr, "warning:", w)
}
if err := applyPortForwards(nftPortFwdPath(), nftContent); err != nil {
fail("%s", err)
}
fmt.Printf("%s aktualisiert, nftables neu geladen (%d Port-Forwards).\n", nftPortFwdPath(), len(fwds)-len(warnings))
fmt.Printf("%s updated, nftables reloaded (%d port-forwards).\n", nftPortFwdPath(), len(fwds)-len(warnings))
}