diff --git a/README.md b/README.md
index a34576e..a3e8081 100644
--- a/README.md
+++ b/README.md
@@ -7,6 +7,8 @@
A real-time Terminal User Interface (TUI) for system monitoring, Docker container management, network diagnostics, and security auditing on Linux servers.
+**This fork adds bilingual UI (English / 简体中文) with runtime language switching.**
+

## Features
@@ -19,6 +21,15 @@ A real-time Terminal User Interface (TUI) for system monitoring, Docker containe
- **Log Viewer** — Filter system logs by time range, service, and log level
- **Network Diagnostics** — Interface stats, ping, traceroute, speed tests, routing table, DNS servers, connection analysis
- **Report Generation** — Generate, save, and load comprehensive system health reports (`~/.server-pulse/reports/`)
+- **i18n** — Switch between English and Simplified Chinese at runtime (`L` / `Ctrl+L`); preference saved to `~/.server-pulse/config.json`
+
+## Language
+
+| Key | Action |
+|-----|--------|
+| `L` or `Ctrl+L` | Toggle English ↔ 中文 |
+
+Default language is Simplified Chinese (中文). After switching, the choice is persisted.
## Requirements
@@ -29,166 +40,44 @@ A real-time Terminal User Interface (TUI) for system monitoring, Docker containe
## Installation
-### Pre-built Binaries (Recommended)
-
-**curl:**
+### From source (this fork)
```bash
-curl -fsSL https://raw.githubusercontent.com/Server-Pulse/server-pulse/main/scripts/install.sh | bash
-```
-
-**wget:**
-
-```bash
-wget -qO- https://raw.githubusercontent.com/Server-Pulse/server-pulse/main/scripts/install.sh | bash
-```
-
-The install script auto-detects your platform, downloads the latest release, verifies the SHA256 checksum, and installs the binary to `/usr/local/bin/`.
-
-### Manual Download
-
-Download a specific release for Linux amd64:
-
-```bash
-sudo wget https://github.com/Server-Pulse/server-pulse/releases/download/vX.X.X/server-pulse-X.X.X-linux-amd64 -O /usr/local/bin/server-pulse
-sudo chmod +x /usr/local/bin/server-pulse
+git clone https://github.com/zhangyang-crazy-one/server-pulse.git
+cd server-pulse
+git checkout feat/chinese-ui
+make build
+sudo mv server-pulse /usr/local/bin/
```
-### From Source
-
-#### Prerequisites
-- Go 1.24+
-- Git
+Or:
```bash
-git clone https://github.com/Server-Pulse/server-pulse.git
-cd server-pulse
-make build
-sudo mv server-pulse /usr/local/bin/
+go build -o server-pulse .
+sudo install -m 755 server-pulse /usr/local/bin/server-pulse
```
## Usage
-After installation, launch the TUI:
-
```bash
server-pulse
```
-The application opens an interactive dashboard. Use the number keys to jump between the four main sections:
-
-| Key | Section | Description |
-|-----|---------|-------------|
-| `1` | Monitor | System resources, processes, and containers |
-| `2` | Diagnostics | Security checks, performance analysis, and logs |
-| `3` | Network | Interfaces, connectivity tests, routing, and protocol analysis |
-| `4` | Reporting | Generate and manage system health reports |
-
-## Keybindings
-
-### Global
-
-| Key | Action |
-|-----|--------|
-| `?` | Toggle help |
-| `q` / `Esc` / `Ctrl+C` | Quit |
-| `b` | Go back |
-| `Tab` / `←` `→` | Navigate tabs |
-| `Enter` | Select |
-
-### Process View
-
-| Key | Action |
-|-----|--------|
-| `/` | Search |
-| `s` | Sort by CPU |
-| `m` | Sort by memory |
-| `k` | Kill process |
-
-### Containers View
-
-| Key | Action |
-|-----|--------|
-| `/` | Search |
-| `Enter` | Open container actions (details, logs, restart, delete, exec shell, etc.) |
-
-### Container Logs
-
-| Key | Action |
-|-----|--------|
-| `s` | Toggle live streaming |
-| `r` | Refresh |
-| `Home` / `End` | Jump to start/end |
-
-### Network View
+Main shortcuts:
| Key | Action |
|-----|--------|
-| `p` | Start ping |
-| `t` | Start traceroute |
-| `c` | Clear results |
-
-### Diagnostics View
-
-| Key | Action |
-|-----|--------|
-| `1` `2` `3` | Switch sub-tabs (Security / Performance / Logs) |
-| `/` | Search |
-| `r` | Reload |
-| `Shift+←` `Shift+→` | Switch filters |
-
-### Reporting View
-
-| Key | Action |
-|-----|--------|
-| `g` | Generate report |
-| `s` | Save report |
-| `l` | Load saved reports |
-| `d` | Delete report |
-
-## Uninstallation
-
-**curl:**
-
-```bash
-curl -fsSL https://raw.githubusercontent.com/Server-Pulse/server-pulse/main/scripts/uninstall.sh | bash
-```
-
-**wget:**
-
-```bash
-wget -qO- https://raw.githubusercontent.com/Server-Pulse/server-pulse/main/scripts/uninstall.sh | bash
-```
-
-## Building
-
-1. Clone the repository:
-```bash
-git clone https://github.com/Server-Pulse/server-pulse.git
-cd server-pulse
-```
-
-2. Build for your current platform:
-```bash
-make build
-```
-
-3. For cross-platform builds:
-```bash
-make build-all
-```
-
-Build artifacts will be placed in the `_build` directory.
+| `1`–`4` | Monitor / Diagnostics / Network / Reporting |
+| `L` / `Ctrl+L` | Switch language |
+| `?` | Help |
+| `q` / `Esc` | Quit |
+| `b` | Back |
-### Supported Build Targets
+## Upstream
-| Architecture | Platform |
-|-------------|----------|
-| amd64 | Linux |
-| arm | Linux |
-| arm64 | Linux |
-| ppc64le | Linux |
+- Upstream: https://github.com/Server-Pulse/server-pulse
+- Fork: https://github.com/zhangyang-crazy-one/server-pulse
## License
-This project is licensed under the [GPL-3.0 License](LICENSE).
+GPL-3.0 (same as upstream)
diff --git a/i18n/i18n.go b/i18n/i18n.go
new file mode 100644
index 0000000..0ce0eaa
--- /dev/null
+++ b/i18n/i18n.go
@@ -0,0 +1,132 @@
+package i18n
+
+import (
+ "encoding/json"
+ "os"
+ "path/filepath"
+ "sync"
+)
+
+type Lang string
+
+const (
+ En Lang = "en"
+ Zh Lang = "zh"
+)
+
+var (
+ mu sync.RWMutex
+ current = Zh
+)
+
+type configFile struct {
+ Language string `json:"language"`
+}
+
+func Current() Lang {
+ mu.RLock()
+ defer mu.RUnlock()
+ return current
+}
+
+func SetLang(lang Lang) {
+ if lang != En && lang != Zh {
+ lang = Zh
+ }
+ mu.Lock()
+ current = lang
+ mu.Unlock()
+ _ = Save()
+}
+
+func Toggle() Lang {
+ if Current() == Zh {
+ SetLang(En)
+ } else {
+ SetLang(Zh)
+ }
+ return Current()
+}
+
+// T translates an English source string when the active language is Chinese.
+// Unknown keys fall back to the English source.
+func T(s string) string {
+ if s == "" {
+ return s
+ }
+ mu.RLock()
+ lang := current
+ mu.RUnlock()
+ if lang != Zh {
+ return s
+ }
+ if v, ok := zh[s]; ok {
+ return v
+ }
+ return s
+}
+
+func Ts(ss ...string) []string {
+ out := make([]string, len(ss))
+ for i, s := range ss {
+ out[i] = T(s)
+ }
+ return out
+}
+
+func configPath() (string, error) {
+ home, err := os.UserHomeDir()
+ if err != nil {
+ return "", err
+ }
+ dir := filepath.Join(home, ".server-pulse")
+ if err := os.MkdirAll(dir, 0o755); err != nil {
+ return "", err
+ }
+ return filepath.Join(dir, "config.json"), nil
+}
+
+func Load() {
+ path, err := configPath()
+ if err != nil {
+ return
+ }
+ data, err := os.ReadFile(path)
+ if err != nil {
+ return
+ }
+ var cfg configFile
+ if err := json.Unmarshal(data, &cfg); err != nil {
+ return
+ }
+ switch Lang(cfg.Language) {
+ case En:
+ mu.Lock()
+ current = En
+ mu.Unlock()
+ case Zh, "":
+ mu.Lock()
+ current = Zh
+ mu.Unlock()
+ }
+}
+
+func Save() error {
+ path, err := configPath()
+ if err != nil {
+ return err
+ }
+ cfg := configFile{Language: string(Current())}
+ data, err := json.MarshalIndent(cfg, "", " ")
+ if err != nil {
+ return err
+ }
+ return os.WriteFile(path, data, 0o644)
+}
+
+func Label() string {
+ if Current() == Zh {
+ return "中文"
+ }
+ return "English"
+}
diff --git a/i18n/zh.go b/i18n/zh.go
new file mode 100644
index 0000000..79fef5a
--- /dev/null
+++ b/i18n/zh.go
@@ -0,0 +1,516 @@
+package i18n
+
+// Code generated from EN/ZH UI map. DO NOT edit by hand unless needed.
+
+var zh = map[string]string{
+ "All": "全部",
+ "\nRestoring terminal after panic...": "\n程序崩溃后正在恢复终端...",
+ "Shell execution failed: %v\n": "Shell 执行失败:%v\n",
+ "Custom": "自定义",
+ "Error": "错误",
+ "Warn": "警告",
+ "Info": "信息",
+ "Debug": "调试",
+ "Routing Table": "路由表",
+ "Ports:": "端口:",
+ "Command:": "命令:",
+ "\nCurrently viewing: %s | Press SPACE to switch": "\n当前查看:%s | 按空格切换",
+ "Traceroute is not installed. Would you like to install it?": "尚未安装 Traceroute,是否安装?",
+ "Traceroute installed successfully. Running traceroute...": "Traceroute 安装成功,正在运行...",
+ "Installation failed: ": "安装失败:",
+ "Installation failed: %v\n%s": "安装失败:%v\n%s",
+ "Invalid target: must be a valid hostname or IP address": "目标无效:必须是有效的主机名或 IP 地址",
+ "Ping failed: %v": "Ping 失败:%v",
+ "No response from target": "目标无响应",
+ "traceroute failed: %v": "路由追踪失败:%v",
+ "Could not detect package manager. Please install traceroute manually.": "未能识别包管理器,请手动安装 traceroute。",
+ "Incorrect password. Please try again.": "密码错误,请重试。",
+ "Server selection failed: %v": "服务器选择失败:%v",
+ "Ping test failed: %v": "Ping 测试失败:%v",
+ "Download test failed: %v": "下载测试失败:%v",
+ "Upload test failed: %v": "上传测试失败:%v",
+ "Selecting server...": "正在选择服务器...",
+ "Testing latency...": "正在测试延迟...",
+ "Testing download speed...": "正在测试下载速度...",
+ "Testing upload speed...": "正在测试上传速度...",
+ "Speed test completed": "测速完成",
+ "The 'docker' command was not found. Please ensure Docker is installed.": "未找到 'docker' 命令,请确保已安装 Docker。",
+ "The user has permissions to run Docker.": "当前用户拥有运行 Docker 的权限。",
+ "The user is not in the 'docker' group. To add them, run 'sudo usermod -aG docker ' and then log out and log back in.": "当前用户不在 'docker' 组中。可运行 'sudo usermod -aG docker <你的用户名>' 添加,然后重新登录。",
+ " %d variables": " %d 个变量",
+ " No core data available\\n\\n": " 暂无核心数据\\n\\n",
+ " Overall CPU Usage: %s\\n": " CPU 总体使用率: %s\\n",
+ " - System has sufficient RAM\\n": " - 系统内存充足\\n",
+ "%d days, %d hours": "%d 天 %d 小时",
+ "%d hours, %d minutes": "%d 小时 %d 分钟",
+ "%d minutes": "%d 分钟",
+ "0 minutes": "0 分钟",
+ "⏳ Operation in progress...": "⏳ 操作进行中...",
+ "1 days, 1 hours": "1 天 1 小时",
+ "1 hours, 1 minutes": "1 小时 1 分钟",
+ "2 days, 0 hours": "2 天 0 小时",
+ "5 minutes": "5 分钟",
+ "Admin privileges required for: %s\\nEnter password:": "需要管理员权限:%s\\n请输入密码:",
+ "Algorithm: ": "算法: ",
+ "Alternative Names": "备用名称",
+ "Application Memory:": "应用内存:",
+ "Are you sure?": "确认操作?",
+ "Authenticating...": "正在认证...",
+ "Authentication failed": "认证失败",
+ "Authentication failed: %v": "认证失败:%v",
+ "Authentication successful!": "认证成功!",
+ "Auto Ban": "自动封禁",
+ "Auto Ban Details - %s": "自动封禁详情 - %s",
+ "Available Actions:": "可用操作:",
+ "Available:": "可用:",
+ "Buffers:": "缓冲区:",
+ "CHECKS PERFORMED": "已执行检查",
+ "CPU State Breakdown": "CPU 状态分解",
+ "CPU Steal Time": "CPU Steal Time",
+ "CPU Steal Time elevated (%.2f%%)": "CPU Steal Time 偏高 (%.2f%%)",
+ "CPU Steal Time high (%.2f%%)": "CPU Steal Time 过高 (%.2f%%)",
+ "Major Page Faults": "主缺页错误",
+ "Check for processes with high disk I/O, consider upgrading storage": "检查高磁盘 I/O 的进程,考虑升级存储设备",
+ "Check host machine load, consider migrating VM or upgrading host": "检查宿主机负载,考虑迁移虚拟机或升级宿主机",
+ "Investigate disk activity, upgrade to faster storage (SSD/NVMe)": "排查磁盘活动,升级为更快的存储(SSD/NVMe)",
+ "Investigate memory usage, consider increasing RAM": "排查内存使用情况,考虑增加内存",
+ "Monitor host machine performance": "监控宿主机性能",
+ "Optimize multi-threaded applications, check for excessive context switching": "优化多线程应用,检查是否存在过多上下文切换",
+ "CPU Usage": "CPU 使用率",
+ "CPU Usage (%)": "CPU 使用率 (%)",
+ "CPU: %s %.1f%% | Load: %.2f, %.2f, %.2f": "CPU: %s %.1f%% | 负载: %.2f, %.2f, %.2f",
+ "Cached:": "缓存:",
+ "Collecting data...": "正在采集数据...",
+ "Command": "命令",
+ "Commit functionality not yet implemented": "提交功能尚未实现",
+ "Complete Configuration:": "完整配置:",
+ "Complete Firewall Configuration:": "完整防火墙配置:",
+ "Configuration": "配置",
+ "Connected": "已连接",
+ "Connectivity": "连通性",
+ "Container '%s' is unhealthy. Check its logs with 'docker logs %s'.": "容器 '%s' 状态不健康。请用 'docker logs %s' 查看日志。",
+ "Container deleted": "容器已删除",
+ "Container pause state changed": "容器暂停状态已变更",
+ "Container paused": "容器已暂停",
+ "Container restarted": "容器已重启",
+ "Container resumed": "容器已恢复",
+ "Container started": "容器已启动",
+ "Container state changed": "容器状态已变更",
+ "Container stopped": "容器已停止",
+ "Container stopped, streaming disabled (status: %s)": "容器已停止,已禁用实时流(状态:%s)",
+ "Container: %s": "容器: %s",
+ "Container: %s\\n": "容器: %s\\n",
+ "Containers": "容器",
+ "Context Switches": "上下文切换",
+ "Core %2d": "核心 %2d",
+ "Current Domain: ": "当前域名: ",
+ "DIAGNOSTICS": "诊断",
+ "DOWN": "断开",
+ "Data currently being written to disk": "正在写入磁盘的数据",
+ "Days Until Expiry: ": "剩余天数: ",
+ "Delete": "删除",
+ "Delete container '%s'?\\nThis action cannot be undone.": "确定删除容器 '%s'?\\n此操作不可撤销。",
+ "Destination": "目标",
+ "DNS Servers": "DNS 服务器",
+ "Details": "详情",
+ "Details: ": "详情: ",
+ "Diagnostic": "诊断",
+ "Dirty Pages:": "脏页:",
+ "Disconnected": "已断开",
+ "Disk I/O": "磁盘 I/O",
+ "Disk I/O history charts coming soon...": "磁盘 I/O 历史图表即将推出...",
+ "Disks": "磁盘",
+ "ENV": "环境变量",
+ "Enter Custom Time Range": "输入自定义时间范围",
+ "Enter Domain Name for SSL Check": "输入域名进行 SSL 检查",
+ "Enter Password:": "输入密码:",
+ "Enter domain name (e.g., google.com)": "输入域名(如 google.com)",
+ "Enter host or IP to ping (e.g., 8.8.8.8)": "输入要 Ping 的主机或 IP(如 8.8.8.8)",
+ "Enter host or IP to trace (e.g., google.com)": "输入要追踪的主机或 IP(如 google.com)",
+ "Enter password for admin access:": "输入管理员密码:",
+ "Enter root password...": "输入 root 密码...",
+ "Enter sudo password...": "输入 sudo 密码...",
+ "Enter target (IP or domain)...": "输入目标(IP 或域名)...",
+ "Environment Variables": "环境变量",
+ "Environment:": "环境变量:",
+ "Erreur lors du chargement des conteneurs: %v\\n": "加载容器时出错: %v\\n",
+ "Erreur: %v": "错误: %v",
+ "Error getting the current user: %v": "获取当前用户失败:%v",
+ "Error getting user's groups: %v": "获取用户组失败:%v",
+ "Error loading logs: %v": "加载日志出错:%v",
+ "Error retrieving raw output: %v": "获取原始输出出错:%v",
+ "Error: ": "错误: ",
+ "Examples: '2 hours ago', '2025-01-08 14:30:00', '3 days ago'": "示例:'2 hours ago'、'2025-01-08 14:30:00'、'3 days ago'",
+ "Exec shell": "进入终端",
+ "Failed to check SSH config: %v": "检查 SSH 配置失败:%v",
+ "Failed to connect to %v: %v": "连接失败 %v: %v",
+ "Failed to get SSH config: %v": "获取 SSH 配置失败:%v",
+ "Failed to get open ports: %v": "获取开放端口失败:%v",
+ "File system cache (can be freed if needed)": "文件系统缓存(需要时可释放)",
+ "Filter by service...": "按服务筛选...",
+ "Firewall Details - %s": "防火墙详情 - %s",
+ "Firewall Rule": "防火墙规则",
+ "Firewall Rules (%d total)": "防火墙规则(共 %d 条)",
+ "Firewall Status": "防火墙状态",
+ "Firewall is not active. Enable it with 'sudo ufw enable'.": "防火墙未启用。可用 'sudo ufw enable' 开启。",
+ "(Full raw output from firewall command)": "(防火墙命令的完整原始输出)",
+ "(Full raw output from service)": "(服务的完整原始输出)",
+ "Flags": "标志",
+ "Force remove container": "强制移除容器",
+ "Force remove container '%s'?\\nThis action cannot be undone.": "确定强制删除容器 '%s'?\\n此操作不可撤销。",
+ "Foreign Address": "远程地址",
+ "Free:": "空闲:",
+ "Gateway": "网关",
+ "General": "概览",
+ "Generate new system health report": "生成新的系统健康报告",
+ "Genmask": "子网掩码",
+ "Health": "健康状态",
+ "High": "高",
+ "High number of context switches (%s)": "上下文切换次数过高 (%s)",
+ "High number of major page faults (%s)": "主缺页次数过高 (%s)",
+ "Host: %s\t|\tOS: %s\t|\tKernel: %s\t|\tUptime: %s": "主机: %s\t|\t系统: %s\t|\t内核: %s\t|\t运行时间: %s",
+ "Hostname Verified: ": "主机名校验: ",
+ "I/O History:": "I/O 历史:",
+ "I/O buffers for disk operations": "磁盘操作的 I/O 缓冲区",
+ "ID: %s\\nName: %s\\nImage: %s\\nStatus: %s\\nProject: %s\\nCreated: %s\\nUptime: %s\\nHealth: %s\\nIP Address: %s\\nGateway: %s": "ID: %s\\n名称: %s\\n镜像: %s\\n状态: %s\\n项目: %s\\n创建时间: %s\\n运行时间: %s\\n健康: %s\\nIP 地址: %s\\n网关: %s",
+ "IOWait": "IO 等待",
+ "IOWait elevated (%.2f%%)": "IOWait 偏高 (%.2f%%)",
+ "IOWait very high (%.2f%%)": "IOWait 非常高 (%.2f%%)",
+ "IP Addresses": "IP 地址",
+ "IRQ": "硬中断",
+ "Idle": "空闲",
+ "Iface": "接口",
+ "Image": "镜像",
+ "Installing traceroute...": "正在安装 traceroute...",
+ "Interface": "网卡",
+ "Interrupts": "中断",
+ "Invalid password or insufficient privileges": "密码错误或权限不足",
+ "Issuer: ": "颁发者: ",
+ "Jail/Service Details": "Jail/服务详情",
+ "Jails/Services (%d total)": "Jail/服务(共 %d 个)",
+ "Kernel data structure cache": "内核数据结构缓存",
+ "Language": "语言",
+ "Last Update: %s": "最后更新:%s",
+ "Last report generated: %s": "上次生成报告:%s",
+ "Latency: %v, Packet Loss: %.1f%%": "延迟: %v,丢包率: %.1f%%",
+ "Level": "级别",
+ "Load Avg: %.2f, %.2f, %.2f": "负载均值:%.2f, %.2f, %.2f",
+ "Load and view saved reports": "加载并查看已保存报告",
+ "Loading CPU metrics...": "正在加载 CPU 指标...",
+ "Loading container details...": "正在加载容器详情...",
+ "Loading disk metrics...": "正在加载磁盘指标...",
+ "Loading environment variables...": "正在加载环境变量...",
+ "Loading logs...": "正在加载日志...",
+ "Loading memory data...": "正在加载内存数据...",
+ "Loading memory metrics...": "正在加载内存指标...",
+ "Loading memory usage data...": "正在加载内存使用数据...",
+ "Loading network metrics...": "正在加载网络指标...",
+ "Loading security checks...": "正在加载安全检查...",
+ "Loading swap data...": "正在加载交换分区数据...",
+ "Loading system memory data...": "正在加载系统内存数据...",
+ "Loading...": "加载中...",
+ "Local Address": "本地地址",
+ "Log Entry Details": "日志条目详情",
+ "Logs": "日志",
+ "Logs loaded": "日志已加载",
+ "MEM": "内存",
+ "MEMORY OVERVIEW": "内存概览",
+ "Medium": "中",
+ "Mem%": "内存%",
+ "Memory": "内存",
+ "Memory Usage": "内存使用",
+ "Memory Usage (%)": "内存使用率 (%)",
+ "Memory actively used by applications": "应用程序实际使用的内存",
+ "Memory available for applications": "应用程序可用内存",
+ "Memory shared between processes": "进程间共享内存",
+ "Memory usage very high (%.1f%%). Process '%s' is consuming %.1f%% memory. Consider optimizing or allocating more resources.": "内存使用率很高 (%.1f%%)。进程 '%s' 占用 %.1f%% 内存。建议优化或增加资源。",
+ "Memory used for page table management": "页表管理占用的内存",
+ "Message": "消息",
+ "Message:": "消息:",
+ "Minimum character classes: %s": "最少字符类别:%s",
+ "Minimum length: %s": "最小长度:%s",
+ "Modified data waiting to be written to disk": "等待写入磁盘的已修改数据",
+ "Monitor": "监控",
+ "NET": "网络",
+ "Name": "名称",
+ "Network": "网络",
+ "Network ": "网络 ",
+ "Network Connectivity Tools": "网络连通性工具",
+ "Network Interfaces:": "网络接口:",
+ "Network RX": "网络接收",
+ "Network TX": "网络发送",
+ "Network Usage": "网络使用",
+ "No DNS servers configured": "未配置 DNS 服务器",
+ "No DNS servers found": "未找到 DNS 服务器",
+ "No IP": "无 IP",
+ "No SSH root login information available": "暂无 SSH Root 登录信息",
+ "No SSL certificate found": "未找到 SSL 证书",
+ "No active connections": "无活动连接",
+ "No active firewall detected": "未检测到活动防火墙",
+ "No active firewall detected (UFW, firewalld, or iptables)": "未检测到活动防火墙(UFW、firewalld 或 iptables)",
+ "No alternative names": "无备用名称",
+ "No auto-ban information available": "暂无自动封禁信息",
+ "No certificate information available": "暂无证书信息",
+ "No container selected": "未选择容器",
+ "No container selected.": "未选择容器。",
+ "No environment variables found": "未找到环境变量",
+ "No firewall detected": "未检测到防火墙",
+ "No firewall information available": "暂无防火墙信息",
+ "No firewall rules configured": "未配置防火墙规则",
+ "No intrusion prevention service detected": "未检测到入侵防护服务",
+ "No intrusion prevention service detected (fail2ban, crowdsec, ossec, suricata, snort, denyhosts, sshguard)": "未检测到入侵防护服务(fail2ban、crowdsec、ossec、suricata、snort、denyhosts、sshguard)",
+ "No jails/services configured": "未配置 Jail/服务",
+ "No log entries found matching filters": "没有匹配筛选条件的日志",
+ "No log entry selected.": "未选择日志条目。",
+ "No logs available": "暂无日志",
+ "No network interfaces": "无网络接口",
+ "No open ports detected": "未检测到开放端口",
+ "No opened ports information available": "暂无开放端口信息",
+ "No password complexity policy found": "未找到密码复杂度策略",
+ "No pending system restart required": "无需系统重启",
+ "No report content available. Generate a report first.": "暂无报告内容,请先生成报告。",
+ "No route found\\n": "未找到路由\\n",
+ "No routing table entries found": "未找到路由表项",
+ "No saved reports found. Generate a report first.": "没有已保存报告,请先生成报告。",
+ "No swap configured": "未配置交换分区",
+ "Normal": "正常",
+ "Not implemented": "尚未实现",
+ "Open Ports": "开放端口",
+ "Open detailed view": "打开详情",
+ "Open interactive shell": "打开交互式 Shell",
+ "Opened Ports Details": "开放端口详情",
+ "Operation '%s'": "操作 '%s'",
+ "Overview": "概览",
+ "Page %d/%d (Results %d-%d of %d)": "第 %d/%d 页(结果 %d-%d / 共 %d)",
+ "Page Tables:": "页表:",
+ "Password Policy": "密码策略",
+ "Per-Core Performance": "每核性能",
+ "Performances": "性能",
+ "Ping Results": "Ping 结果",
+ "Ping Results\\n": "Ping 结果\\n",
+ "Please wait while we collect system data...": "正在采集系统数据,请稍候...",
+ "Port": "端口",
+ "Ports": "端口",
+ "Press 'a' to authenticate for detailed connection information": "按 a 认证以查看详细连接信息",
+ "Press 'a' to retry authentication": "按 a 重试认证",
+ "Press 'b' to go back": "按 b 返回",
+ "Press 'b', 'esc', or 'enter' to go back.": "按 b、esc 或 enter 返回。",
+ "Press 'd' to change domain, 'r' to refresh checks": "按 d 更改域名,按 r 刷新检查",
+ "Press 'd' to enter domain name for SSL check": "按 d 输入域名进行 SSL 检查",
+ "Press 'g' to generate a new report, 'l' to load saved reports": "按 g 生成新报告,按 l 加载已保存报告",
+ "Press 'p' to ping, 't' to traceroute, 's' for speed test, 'c' to clear results": "按 p 进行 Ping,按 t 路由追踪,按 s 测速,按 c 清除结果",
+ "Press 'r' to load CPU metrics": "按 r 加载 CPU 指标",
+ "Press 'r' to load logs or Enter to apply filters": "按 r 加载日志,或按 Enter 应用筛选",
+ "Press 'r' to load memory metrics": "按 r 加载内存指标",
+ "Press 'r' to load system health metrics.": "按 r 加载系统健康指标。",
+ "Press 'y' to confirm or 'n' to cancel": "按 y 确认,按 n 取消",
+ "Press ENTER to view selected report, 'd' to delete, 'b' to go back": "按 Enter 查看选中报告,按 d 删除,按 b 返回",
+ "Press Enter to apply, ESC to cancel": "按 Enter 应用,按 ESC 取消",
+ "Press Enter to authenticate, Esc to cancel": "按 Enter 认证,按 Esc 取消",
+ "Press Enter to check SSL, Esc to cancel": "按 Enter 检查 SSL,按 Esc 取消",
+ "Press Enter to continue...": "按 Enter 继续...",
+ "Process": "进程",
+ "Processes": "进程数",
+ "Project": "项目",
+ "Proto": "协议",
+ "Protocol": "协议",
+ "Protocol Analysis": "协议分析",
+ "RAM: %s %.1f%% | Total: %s | Used: %s | Free: %s": "内存: %s %.1f%% | 总量: %s | 已用: %s | 空闲: %s",
+ "RX": "接收",
+ "RX Total: %s\\n": "接收总量: %s\\n",
+ "Read: %s\\n": "读取: %s\\n",
+ "Recv-Q": "接收队列",
+ "Remove": "强制删除",
+ "Remove container": "删除容器",
+ "Reporting": "报告",
+ "Restart": "重启",
+ "Restart container": "重启容器",
+ "Restart container '%s'?\\nThis will stop and start the container.": "确定重启容器 '%s'?\\n将先停止再启动该容器。",
+ "Risky ports are open. Close unnecessary ports to reduce attack surface.": "存在高风险开放端口。请关闭不必要端口以减小攻击面。",
+ "SSH Password Authentication": "SSH 密码认证",
+ "SSH Root Login": "SSH Root 登录",
+ "SSH Root Login Details": "SSH Root 登录详情",
+ "SSH password authentication is enabled. Consider using SSH keys only for better security.": "已启用 SSH 密码认证。建议仅使用 SSH 密钥以提高安全性。",
+ "SSL Certificate": "SSL 证书",
+ "SSL Certificate Details": "SSL 证书详情",
+ "STATUS": "状态",
+ "SWAP ANALYSIS": "交换分区分析",
+ "SWP: %s %.1f%% | Total: %s | Used: %s | Free: %s": "交换: %s %.1f%% | 总量: %s | 已用: %s | 空闲: %s",
+ "SYSTEM MEMORY": "系统内存",
+ "Saved Reports:": "已保存报告:",
+ "Saving the current report to file...": "正在将当前报告保存到文件...",
+ "Search a container...": "搜索容器...",
+ "Search a process...": "搜索进程...",
+ "Search logs...": "搜索日志...",
+ "Search: ": "搜索:",
+ "Service: ": "服务:",
+ "Showing %d entries": "显示 %d 条记录",
+ "Security Check": "安全检查",
+ "Security Checks": "安全检查",
+ "Security Information": "安全信息",
+ "Send-Q": "发送队列",
+ "Serial Number: ": "序列号: ",
+ "Server: %s": "服务器: %s",
+ "Service": "服务",
+ "Shared:": "共享:",
+ "Slab:": "内核缓存:",
+ "Shell opened": "已打开 Shell",
+ "Show container logs": "显示容器日志",
+ "Signature Algorithm: ": "签名算法: ",
+ "SoftIRQ": "软中断",
+ "Some checks require admin privileges. Press 'a' to authenticate.": "部分检查需要管理员权限。按 a 进行认证。",
+ "Speed Test Results\\n": "测速结果\\n",
+ "Speed test failed: ": "测速失败: ",
+ "Speed test: %s (%.0f%%)": "测速:%s (%.0f%%)",
+ "State": "状态",
+ "Status": "状态",
+ "Status: ": "状态: ",
+ "Status: %s\\n": "状态: %s\\n",
+ "Steal": "被偷用",
+ "Streaming not available: %v\\nShowing static logs instead...": "实时流不可用:%v\\n改为显示静态日志...",
+ "Subject: ": "主题: ",
+ "Sudo is not available. Please run as root.": "Sudo 不可用,请以 root 运行。",
+ "Swap Analysis": "交换分区分析",
+ "Swap Cached:": "交换缓存:",
+ "Swap Free:": "交换空闲:",
+ "Swap Status:": "交换分区状态:",
+ "Swap Total:": "交换总量:",
+ "Swap Used:": "交换已用:",
+ "Swap memory also present in RAM": "同时存在于内存中的交换数据",
+ "System": "系统",
+ "System Activity Metrics": "系统活动指标",
+ "System Health": "系统健康",
+ "System Memory": "系统内存",
+ "System Restart": "系统重启",
+ "System Restart Required": "系统重启",
+ "System Updates": "系统更新",
+ "System restart required for kernel updates. Schedule a maintenance window to reboot the system.": "内核更新需要重启系统。请安排维护窗口重启。",
+ "System updates are available. Run 'sudo apt update && sudo apt upgrade' to apply security patches.": "系统有可用更新。请运行 'sudo apt update && sudo apt upgrade' 应用安全补丁。",
+ "TCP: %d | UDP: %d | Listening: %d | Established: %d": "TCP: %d | UDP: %d | 监听: %d | 已建立: %d",
+ "TX": "发送",
+ "TX Total: %s\\n\\n": "发送总量: %s\\n\\n",
+ "Terminal too small!\\nMinimum size: (min: 80x20)\\nCurrent: ": "终端窗口太小!\\n最小尺寸:80x20\\n当前:",
+ "Test Duration: %v": "测试时长: %v",
+ "Test Duration: %v\\n\\n": "测试时长: %v\\n\\n",
+ "Threads": "线程数",
+ "Time": "时间",
+ "Toggle container state": "切换容器运行状态",
+ "Toggle pause state": "切换暂停状态",
+ "Total Memory:": "内存总量:",
+ "Total Read": "读取总量",
+ "Total Read IOPS": "读取 IOPS 总量",
+ "Total Write": "写入总量",
+ "Total Write IOPS": "写入 IOPS 总量",
+ "Device": "设备",
+ "Read IOPS": "读取 IOPS",
+ "Write IOPS": "写入 IOPS",
+ "Read MB/s": "读取 MB/s",
+ "Write MB/s": "写入 MB/s",
+ "Read MB": "读取 MB",
+ "Write MB": "写入 MB",
+ "Queue": "队列",
+ "Traceroute to %s": "路由追踪:%s",
+ "Traceroute to %s\\n": "路由追踪:%s\\n",
+ "USAGE BREAKDOWN": "使用分解",
+ "Unknown state: %v": "未知状态:%v",
+ "Unsupported operating system: %s": "不支持的操作系统: %s",
+ "Usage Breakdown": "使用分解",
+ "Usage History:": "使用历史:",
+ "Usage: %.1f%% | Used: %s | Limit: %s | Available: %s\\n\\n": "使用率: %.1f%% | 已用: %s | 限制: %s | 可用: %s\\n\\n",
+ "Usage: %.1f%%\\n": "使用率: %.1f%%\\n",
+ "Use '↑' and '↓' to navigate results pages": "用 ↑ ↓ 翻页查看结果",
+ "Use arrow keys to scroll, 'q' to return to report menu, 's' to save": "用方向键滚动,按 q 返回报告菜单,按 s 保存",
+ "Used:": "已用:",
+ "User": "用户",
+ "Valid From: ": "生效时间: ",
+ "Valid To: ": "到期时间: ",
+ "Validity Period": "有效期",
+ "Version: ": "版本: ",
+ "View detailed container information": "查看容器详细信息",
+ "View logs": "查看日志",
+ "Write: %s\\n\\n": "写入: %s\\n\\n",
+ "WriteBack:": "回写中:",
+ "activate tab": "激活标签",
+ "back": "返回",
+ "clear": "清除",
+ "containers": "容器",
+ "details": "详情",
+ "diagnostics": "诊断",
+ "e.g., '2 hours ago', '2025-01-08', '3 days ago'": "例如:'2 小时前'、'2025-01-08'、'3 天前'",
+ "enter CPU sub-tabs": "进入CPU子标签",
+ "enter Memory sub-tabs": "进入内存子标签",
+ "generate report": "生成报告",
+ "kill": "结束进程",
+ "level": "级别",
+ "load reports": "加载报告",
+ "menu": "菜单",
+ "monitor": "监控",
+ "navigate": "导航",
+ "navigate tables/pages": "浏览表格/翻页",
+ "network": "网络",
+ "performance": "性能",
+ "ping": "Ping",
+ "process": "进程",
+ "quit": "退出",
+ "refresh": "刷新",
+ "reload": "刷新",
+ "reload data": "刷新数据",
+ "reporting": "报告",
+ "save report": "保存报告",
+ "scroll": "滚动",
+ "search": "搜索",
+ "security": "安全",
+ "select": "选择",
+ "service": "服务",
+ "sort by cpu": "按CPU排序",
+ "sort by mem": "按内存排序",
+ "switch CPU sub-tabs": "切换CPU子标签",
+ "switch Memory sub-tabs": "切换内存子标签",
+ "switch filter": "切换筛选",
+ "switch language": "切换语言",
+ "switch performance tabs": "切换性能标签",
+ "switch tabs": "切换标签",
+ "switch view": "切换视图",
+ "system": "系统",
+ "time range": "时间范围",
+ "toggle help": "切换帮助",
+ "toggle stream": "切换实时流",
+ "trace route": "路由追踪",
+ "⏳ Loading CPU Performance Metrics...": "⏳ 正在加载 CPU 性能指标...",
+ "⏳ Loading I/O Performance Metrics...": "⏳ 正在加载 I/O 性能指标...",
+ "⏳ Loading System Health...": "⏳ 正在加载系统健康...",
+ "─ I/O PERFORMANCE ANALYSIS ─": "─ I/O 性能分析 ─",
+ "─ SYSTEM HEALTH ANALYSIS ─": "─ 系统健康分析 ─",
+ "│ Last Update: %s\\n": "│ 最后更新: %s\\n",
+ "│ Average Latency: %s ": "│ 平均延迟:%s ",
+ "│ Health Score: %s%s%s \\n\\n": "│ 健康评分:%s%s%s \\n\\n",
+ "⚠ Moderate swap usage": "⚠ 交换分区使用率中等",
+ "⚠ Swap used with available RAM": "⚠ 仍有可用内存却使用了交换分区",
+ "⚠️ DETECTED ISSUES": "⚠️ 发现问题",
+ "⚡ CPU OVERVIEW": "⚡ CPU 概览",
+ "✅ %s successfully": "✅ %s成功",
+ "✅ Container restarted successfully": "✅ 容器已重启成功",
+ "✅ Operation '' successfully": "✅ 操作 ''成功",
+ "✅ Operation 'unknown' successfully": "✅ 操作 'unknown'成功",
+ "✓ No swap used": "✓ 未使用交换分区",
+ "✗ High swap usage": "✗ 交换分区使用率过高",
+ "❌ %s failed": "❌ %s失败",
+ "❌ %s failed: %v": "❌ %s失败: %v",
+ "❌ Container restarted failed": "❌ 容器已重启失败",
+ "❌ Container started failed: permission denied": "❌ 容器已启动失败: permission denied",
+ "❌ Operation 'unknown' failed": "❌ 操作 'unknown'失败",
+ "💡 RECOMMENDATIONS": "💡 建议",
+ "💾 DISK I/O PERFORMANCE": "💾 磁盘 I/O 性能",
+ "📈 SYSTEM ACTIVITY METRICS": "📈 系统活动指标",
+ "📊 CPU STATE BREAKDOWN": "📊 CPU 状态分解",
+ "📊 I/O SUMMARY": "📊 I/O 摘要",
+ "🔥 PER-CORE PERFORMANCE": "🔥 每核性能",
+ "🔥 TOP I/O PROCESSES": "🔥 I/O 占用最高进程",
+ "Language: %s": "语言:%s",
+ "🔐 Admin Authentication Required": "🔐 需要管理员认证",
+ "✅ Authentication Successful": "✅ 认证成功",
+ "❌ Authentication Failed": "❌ 认证失败",
+ "⏳ Authenticating...": "⏳ 正在认证...",
+ "✅ Admin access granted": "✅ 已获得管理员权限",
+ "🔒 Some checks require admin privileges. Press 'a' to authenticate": "🔒 部分检查需要管理员权限。按 a 进行认证",
+}
diff --git a/main.go b/main.go
index 913f3dc..1d85a82 100644
--- a/main.go
+++ b/main.go
@@ -3,6 +3,7 @@ package main
import (
"bufio"
"fmt"
+ "github.com/Server-Pulse/server-pulse/i18n"
"log"
"os"
"time"
@@ -18,6 +19,8 @@ var dockerManager *app.DockerManager
var currentModel tea.Model
func main() {
+ i18n.Load()
+
if ok, err := utils.CheckDockerPermissions(); !ok {
fmt.Println(err)
os.Exit(1)
@@ -43,7 +46,7 @@ func runTUI() bool {
defer func() {
if r := recover(); r != nil {
log.Printf("Recovered from panic: %v", r)
- fmt.Println("\nRestoring terminal after panic...")
+ fmt.Println(i18n.T("\nRestoring terminal after panic..."))
time.Sleep(2 * time.Second)
}
}()
@@ -71,7 +74,7 @@ func runTUI() bool {
finalModel, err := p.Run()
if err != nil {
- fmt.Printf("Erreur: %v", err)
+ fmt.Printf(i18n.T("Erreur: %v"), err)
os.Exit(1)
}
@@ -84,8 +87,8 @@ func runTUI() bool {
err := dockerManager.ExecInteractiveShellAlternative(shellRequest.ContainerID)
if err != nil {
- fmt.Printf("Shell execution failed: %v\n", err)
- fmt.Println("Press Enter to continue...")
+ fmt.Printf(i18n.T("Shell execution failed: %v\n"), err)
+ fmt.Println(i18n.T("Press Enter to continue..."))
bufio.NewScanner(os.Stdin).Scan()
}
time.Sleep(300 * time.Millisecond)
diff --git a/system/logs/logs.go b/system/logs/logs.go
index 6b511ec..3b38619 100644
--- a/system/logs/logs.go
+++ b/system/logs/logs.go
@@ -3,6 +3,7 @@ package logs
import (
"encoding/json"
"fmt"
+ "github.com/Server-Pulse/server-pulse/i18n"
"os/exec"
"strconv"
"strings"
@@ -60,7 +61,7 @@ func (lm *LogManager) GetSystemLogs(filters LogFilters) tea.Cmd {
Entries: []LogEntry{},
TotalCount: 0,
Filters: filters,
- ErrorMsg: fmt.Sprintf("Error loading logs: %v", err),
+ ErrorMsg: fmt.Sprintf(i18n.T("Error loading logs: %v"), err),
})
}
diff --git a/system/network/connectivity.go b/system/network/connectivity.go
index ab70f7d..90083e8 100644
--- a/system/network/connectivity.go
+++ b/system/network/connectivity.go
@@ -9,6 +9,7 @@ import (
"strings"
"time"
+ "github.com/Server-Pulse/server-pulse/i18n"
tea "github.com/charmbracelet/bubbletea"
)
@@ -30,7 +31,7 @@ func Ping(target string, count int) tea.Cmd {
return PingMsg(PingResult{
Target: target,
Success: false,
- Error: "Invalid target: must be a valid hostname or IP address",
+ Error: i18n.T("Invalid target: must be a valid hostname or IP address"),
})
}
if count < 1 || count > 100 {
@@ -47,7 +48,7 @@ func Ping(target string, count int) tea.Cmd {
return PingMsg(PingResult{
Target: target,
Success: false,
- Error: fmt.Sprintf("Ping failed: %v", err),
+ Error: fmt.Sprintf(i18n.T("Ping failed: %v"), err),
})
}
@@ -81,7 +82,7 @@ func parseSystemPingOutput(target, output string) PingMsg {
return PingMsg{
Target: target,
Success: false,
- Error: "No response from target",
+ Error: i18n.T("No response from target"),
}
}
@@ -90,7 +91,7 @@ func Traceroute(target string) tea.Cmd {
if !isValidTarget(target) {
return TracerouteMsg(TracerouteResult{
Target: target,
- Error: "Invalid target: must be a valid hostname or IP address",
+ Error: i18n.T("Invalid target: must be a valid hostname or IP address"),
})
}
checkCmd := exec.Command("which", "traceroute")
@@ -105,7 +106,7 @@ func Traceroute(target string) tea.Cmd {
if err != nil {
return TracerouteMsg(TracerouteResult{
Target: target,
- Error: fmt.Sprintf("traceroute failed: %v", err),
+ Error: fmt.Sprintf(i18n.T("traceroute failed: %v"), err),
})
}
@@ -194,7 +195,7 @@ func InstallTraceroute(target string, sudoPassword string) tea.Cmd {
return TracerouteInstallResultMsg{
Success: false,
Target: target,
- Error: "Could not detect package manager. Please install traceroute manually.",
+ Error: i18n.T("Could not detect package manager. Please install traceroute manually."),
}
}
@@ -281,14 +282,14 @@ func InstallTraceroute(target string, sudoPassword string) tea.Cmd {
return TracerouteInstallResultMsg{
Success: false,
Target: target,
- Error: "Incorrect password. Please try again.",
+ Error: i18n.T("Incorrect password. Please try again."),
PasswordInvalid: true,
}
}
return TracerouteInstallResultMsg{
Success: false,
Target: target,
- Error: fmt.Sprintf("Installation failed: %v\n%s", err, stderrOutput),
+ Error: fmt.Sprintf(i18n.T("Installation failed: %v\n%s"), err, stderrOutput),
}
}
} else {
@@ -297,7 +298,7 @@ func InstallTraceroute(target string, sudoPassword string) tea.Cmd {
return TracerouteInstallResultMsg{
Success: false,
Target: target,
- Error: fmt.Sprintf("Installation failed: %v\n%s", err, string(output)),
+ Error: fmt.Sprintf(i18n.T("Installation failed: %v\n%s"), err, string(output)),
}
}
}
diff --git a/system/network/dns.go b/system/network/dns.go
index ed0c786..8fc52be 100644
--- a/system/network/dns.go
+++ b/system/network/dns.go
@@ -7,6 +7,7 @@ import (
"os"
"strings"
+ "github.com/Server-Pulse/server-pulse/i18n"
"github.com/Server-Pulse/server-pulse/utils"
tea "github.com/charmbracelet/bubbletea"
)
@@ -42,7 +43,7 @@ func GetDNS() tea.Cmd {
}
if len(dnsServers) == 0 {
- return DNSMsg([]DNSInfo{{Server: "No DNS servers found"}})
+ return DNSMsg([]DNSInfo{{Server: i18n.T("No DNS servers found")}})
}
return DNSMsg(dnsServers)
diff --git a/system/network/speedtest.go b/system/network/speedtest.go
index 3e5722b..f2ef772 100644
--- a/system/network/speedtest.go
+++ b/system/network/speedtest.go
@@ -10,6 +10,7 @@ import (
"sync/atomic"
"time"
+ "github.com/Server-Pulse/server-pulse/i18n"
tea "github.com/charmbracelet/bubbletea"
)
@@ -399,25 +400,25 @@ func RunSpeedTest() tea.Cmd {
// Server selection
if err := selectServer(client, pingURL, 3); err != nil {
- return SpeedTestErrorMsg{Error: fmt.Sprintf("Server selection failed: %v", err)}
+ return SpeedTestErrorMsg{Error: fmt.Sprintf(i18n.T("Server selection failed: %v"), err)}
}
// Ping test
pingResult, err := testPing(client, pingURL, pingCount)
if err != nil {
- return SpeedTestErrorMsg{Error: fmt.Sprintf("Ping test failed: %v", err)}
+ return SpeedTestErrorMsg{Error: fmt.Sprintf(i18n.T("Ping test failed: %v"), err)}
}
// Download test
downloadMbps, err := testDownload(client, downloadURL, downloadConcurrency, downloadTestDuration, nil)
if err != nil {
- return SpeedTestErrorMsg{Error: fmt.Sprintf("Download test failed: %v", err)}
+ return SpeedTestErrorMsg{Error: fmt.Sprintf(i18n.T("Download test failed: %v"), err)}
}
// Upload test
uploadMbps, err := testUpload(client, uploadURL, uploadConcurrency, uploadChunkSize, uploadTestDuration, nil)
if err != nil {
- return SpeedTestErrorMsg{Error: fmt.Sprintf("Upload test failed: %v", err)}
+ return SpeedTestErrorMsg{Error: fmt.Sprintf(i18n.T("Upload test failed: %v"), err)}
}
testDuration := time.Since(startTime)
@@ -445,12 +446,12 @@ func RunSpeedTestWithProgress(progressChan chan<- SpeedTestProgressMsg) tea.Cmd
progressChan <- SpeedTestProgressMsg{
Stage: "server",
Progress: 0.1,
- Message: "Selecting server...",
+ Message: i18n.T("Selecting server..."),
}
}
if err := selectServer(client, pingURL, 3); err != nil {
- return SpeedTestErrorMsg{Error: fmt.Sprintf("Server selection failed: %v", err)}
+ return SpeedTestErrorMsg{Error: fmt.Sprintf(i18n.T("Server selection failed: %v"), err)}
}
// Ping test
@@ -458,13 +459,13 @@ func RunSpeedTestWithProgress(progressChan chan<- SpeedTestProgressMsg) tea.Cmd
progressChan <- SpeedTestProgressMsg{
Stage: "ping",
Progress: 0.2,
- Message: "Testing latency...",
+ Message: i18n.T("Testing latency..."),
}
}
pingResult, err := testPing(client, pingURL, pingCount)
if err != nil {
- return SpeedTestErrorMsg{Error: fmt.Sprintf("Ping test failed: %v", err)}
+ return SpeedTestErrorMsg{Error: fmt.Sprintf(i18n.T("Ping test failed: %v"), err)}
}
// Download test with progress
@@ -472,13 +473,13 @@ func RunSpeedTestWithProgress(progressChan chan<- SpeedTestProgressMsg) tea.Cmd
progressChan <- SpeedTestProgressMsg{
Stage: "download",
Progress: 0.4,
- Message: "Testing download speed...",
+ Message: i18n.T("Testing download speed..."),
}
}
downloadMbps, err := testDownload(client, downloadURL, downloadConcurrency, downloadTestDuration, nil)
if err != nil {
- return SpeedTestErrorMsg{Error: fmt.Sprintf("Download test failed: %v", err)}
+ return SpeedTestErrorMsg{Error: fmt.Sprintf(i18n.T("Download test failed: %v"), err)}
}
// Upload test
@@ -486,13 +487,13 @@ func RunSpeedTestWithProgress(progressChan chan<- SpeedTestProgressMsg) tea.Cmd
progressChan <- SpeedTestProgressMsg{
Stage: "upload",
Progress: 0.7,
- Message: "Testing upload speed...",
+ Message: i18n.T("Testing upload speed..."),
}
}
uploadMbps, err := testUpload(client, uploadURL, uploadConcurrency, uploadChunkSize, uploadTestDuration, nil)
if err != nil {
- return SpeedTestErrorMsg{Error: fmt.Sprintf("Upload test failed: %v", err)}
+ return SpeedTestErrorMsg{Error: fmt.Sprintf(i18n.T("Upload test failed: %v"), err)}
}
testDuration := time.Since(startTime)
@@ -501,7 +502,7 @@ func RunSpeedTestWithProgress(progressChan chan<- SpeedTestProgressMsg) tea.Cmd
progressChan <- SpeedTestProgressMsg{
Stage: "complete",
Progress: 1.0,
- Message: "Speed test completed",
+ Message: i18n.T("Speed test completed"),
}
}
diff --git a/system/performance/cpu.go b/system/performance/cpu.go
index 18a6e56..942436d 100644
--- a/system/performance/cpu.go
+++ b/system/performance/cpu.go
@@ -3,6 +3,7 @@ package performance
import (
"bufio"
"fmt"
+ "github.com/Server-Pulse/server-pulse/i18n"
"os"
"strconv"
"strings"
@@ -191,7 +192,7 @@ func getProcessCounts() (int, int, error) {
}
func RenderCPU() string {
- return "⏳ Loading CPU Performance Metrics..."
+ return i18n.T("⏳ Loading CPU Performance Metrics...")
}
// RenderCPUOverview renders the CPU overview section (always visible at top)
@@ -199,7 +200,7 @@ func RenderCPUOverview(metrics *model.CPUMetrics) string {
var b strings.Builder
// Overall CPU Usage section
- b.WriteString(lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("51")).Render("⚡ CPU OVERVIEW"))
+ b.WriteString(lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("51")).Render(i18n.T("⚡ CPU OVERVIEW")))
b.WriteString("\n")
b.WriteString(strings.Repeat("─", 70))
b.WriteString("\n\n")
@@ -207,15 +208,15 @@ func RenderCPUOverview(metrics *model.CPUMetrics) string {
// CPU Usage with visual indicator
usageColor := lipgloss.Color("46") // Green by default
usageIcon := "●"
- usageStatus := "Normal"
+ usageStatus := i18n.T("Normal")
if metrics.OverallUsage > 80 {
usageColor = lipgloss.Color("196") // Red for high usage
usageIcon = "●"
- usageStatus = "High"
+ usageStatus = i18n.T("High")
} else if metrics.OverallUsage > 60 {
usageColor = lipgloss.Color("214") // Orange for medium usage
usageIcon = "●"
- usageStatus = "Medium"
+ usageStatus = i18n.T("Medium")
}
usageText := lipgloss.NewStyle().Foreground(usageColor).Bold(true).Render(
@@ -228,15 +229,15 @@ func RenderCPUOverview(metrics *model.CPUMetrics) string {
prog.Empty = '░'
usageBar := prog.ViewAs(metrics.OverallUsage / 100.0)
- b.WriteString(fmt.Sprintf(" Overall CPU Usage: %s\n", usageText))
+ b.WriteString(fmt.Sprintf(i18n.T(" Overall CPU Usage: %s\n"), usageText))
b.WriteString(fmt.Sprintf(" %s\n\n", usageBar))
// Last update and load averages in columns
lastUpdateText := lipgloss.NewStyle().Foreground(lipgloss.Color("244")).Render(
- fmt.Sprintf("Last Update: %s", metrics.LastUpdate.Format("15:04:05")),
+ fmt.Sprintf(i18n.T("Last Update: %s"), metrics.LastUpdate.Format("15:04:05")),
)
loadAvgText := lipgloss.NewStyle().Foreground(lipgloss.Color("117")).Render(
- fmt.Sprintf("Load Avg: %.2f, %.2f, %.2f", metrics.LoadAverage[0], metrics.LoadAverage[1], metrics.LoadAverage[2]),
+ fmt.Sprintf(i18n.T("Load Avg: %.2f, %.2f, %.2f"), metrics.LoadAverage[0], metrics.LoadAverage[1], metrics.LoadAverage[2]),
)
b.WriteString(fmt.Sprintf(" %s %s\n", lastUpdateText, loadAvgText))
b.WriteString("\n")
@@ -249,7 +250,7 @@ func RenderCPUStateBreakdown(metrics *model.CPUMetrics) string {
var b strings.Builder
// CPU State Breakdown
- b.WriteString(lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("51")).Render("📊 CPU STATE BREAKDOWN"))
+ b.WriteString(lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("51")).Render(i18n.T("📊 CPU STATE BREAKDOWN")))
b.WriteString("\n")
b.WriteString(strings.Repeat("─", 70))
b.WriteString("\n\n")
@@ -260,13 +261,13 @@ func RenderCPUStateBreakdown(metrics *model.CPUMetrics) string {
icon string
desc string
}{
- {"User", metrics.StateBreakdown.User, "👤", "User space processes"},
- {"System", metrics.StateBreakdown.System, "⚙️ ", "Kernel space processes"},
- {"Idle", metrics.StateBreakdown.Idle, "💤", "CPU idle time"},
- {"IOWait", metrics.StateBreakdown.IOWait, "⏳", "Waiting for I/O operations"},
- {"IRQ", metrics.StateBreakdown.IRQ, "⚡", "Hardware interrupts"},
- {"SoftIRQ", metrics.StateBreakdown.SoftIRQ, "📡", "Software interrupts"},
- {"Steal", metrics.StateBreakdown.Steal, "🔒", "Stolen by hypervisor"},
+ {i18n.T("User"), metrics.StateBreakdown.User, "👤", "User space processes"},
+ {i18n.T("System"), metrics.StateBreakdown.System, "⚙️ ", "Kernel space processes"},
+ {i18n.T("Idle"), metrics.StateBreakdown.Idle, "💤", "CPU idle time"},
+ {i18n.T("IOWait"), metrics.StateBreakdown.IOWait, "⏳", "Waiting for I/O operations"},
+ {i18n.T("IRQ"), metrics.StateBreakdown.IRQ, "⚡", "Hardware interrupts"},
+ {i18n.T("SoftIRQ"), metrics.StateBreakdown.SoftIRQ, "📡", "Software interrupts"},
+ {i18n.T("Steal"), metrics.StateBreakdown.Steal, "🔒", "Stolen by hypervisor"},
{"Nice", metrics.StateBreakdown.Nice, "✨", "Nice priority processes"},
}
@@ -306,7 +307,7 @@ func RenderCPUPerCore(metrics *model.CPUMetrics) string {
// Per-Core Performance
if len(metrics.Cores) > 0 {
- b.WriteString(lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("51")).Render("🔥 PER-CORE PERFORMANCE"))
+ b.WriteString(lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("51")).Render(i18n.T("🔥 PER-CORE PERFORMANCE")))
b.WriteString("\n")
b.WriteString(strings.Repeat("─", 70))
b.WriteString("\n\n")
@@ -328,7 +329,7 @@ func RenderCPUPerCore(metrics *model.CPUMetrics) string {
coreProg1.Empty = '░'
bar1 := coreProg1.ViewAs(core1.Usage / 100.0)
- coreLabel1 := lipgloss.NewStyle().Width(8).Render(fmt.Sprintf("Core %2d", core1.CoreID))
+ coreLabel1 := lipgloss.NewStyle().Width(8).Render(fmt.Sprintf(i18n.T("Core %2d"), core1.CoreID))
coreValue1 := lipgloss.NewStyle().Width(7).Foreground(coreColor1).Bold(true).Render(fmt.Sprintf("%5.1f%%", core1.Usage))
line := fmt.Sprintf(" %s %s %s", coreLabel1, coreValue1, bar1)
@@ -349,7 +350,7 @@ func RenderCPUPerCore(metrics *model.CPUMetrics) string {
coreProg2.Empty = '░'
bar2 := coreProg2.ViewAs(core2.Usage / 100.0)
- coreLabel2 := lipgloss.NewStyle().Width(8).Render(fmt.Sprintf("Core %2d", core2.CoreID))
+ coreLabel2 := lipgloss.NewStyle().Width(8).Render(fmt.Sprintf(i18n.T("Core %2d"), core2.CoreID))
coreValue2 := lipgloss.NewStyle().Width(7).Foreground(coreColor2).Bold(true).Render(fmt.Sprintf("%5.1f%%", core2.Usage))
line += fmt.Sprintf(" %s %s %s", coreLabel2, coreValue2, bar2)
@@ -359,7 +360,7 @@ func RenderCPUPerCore(metrics *model.CPUMetrics) string {
}
b.WriteString("\n")
} else {
- b.WriteString(" No core data available\n\n")
+ b.WriteString(i18n.T(" No core data available\n\n"))
}
return b.String()
@@ -370,7 +371,7 @@ func RenderCPUSystemActivity(metrics *model.CPUMetrics) string {
var b strings.Builder
// System Activity Metrics
- b.WriteString(lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("51")).Render("📈 SYSTEM ACTIVITY METRICS"))
+ b.WriteString(lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("51")).Render(i18n.T("📈 SYSTEM ACTIVITY METRICS")))
b.WriteString("\n")
b.WriteString(strings.Repeat("─", 70))
b.WriteString("\n\n")
@@ -381,10 +382,10 @@ func RenderCPUSystemActivity(metrics *model.CPUMetrics) string {
value string
icon string
}{
- {"Context Switches", formatNumber(metrics.ContextSwitches), "🔄"},
- {"Interrupts", formatNumber(metrics.Interrupts), "⚡"},
- {"Processes", fmt.Sprintf("%d", metrics.ProcessCount), "📋"},
- {"Threads", fmt.Sprintf("%d", metrics.ThreadCount), "🧵"},
+ {i18n.T("Context Switches"), formatNumber(metrics.ContextSwitches), "🔄"},
+ {i18n.T("Interrupts"), formatNumber(metrics.Interrupts), "⚡"},
+ {i18n.T("Processes"), fmt.Sprintf("%d", metrics.ProcessCount), "📋"},
+ {i18n.T("Threads"), fmt.Sprintf("%d", metrics.ThreadCount), "🧵"},
}
for _, item := range leftCol {
@@ -422,11 +423,11 @@ func RenderCPUContent(metrics *model.CPUMetrics) string {
func RenderCPUWithData(metrics *model.CPUMetrics, loading bool) string {
if loading {
- return vars.CardStyle.Render("⏳ Loading CPU Performance Metrics...")
+ return vars.CardStyle.Render(i18n.T("⏳ Loading CPU Performance Metrics..."))
}
if metrics == nil {
- return vars.CardStyle.Render("Press 'r' to load CPU metrics")
+ return vars.CardStyle.Render(i18n.T("Press 'r' to load CPU metrics"))
}
return vars.CardStyle.Render(RenderCPUContent(metrics))
diff --git a/system/performance/io.go b/system/performance/io.go
index bb64cc0..01fe627 100644
--- a/system/performance/io.go
+++ b/system/performance/io.go
@@ -3,6 +3,7 @@ package performance
import (
"bufio"
"fmt"
+ "github.com/Server-Pulse/server-pulse/i18n"
"os"
"sort"
"strconv"
@@ -199,27 +200,27 @@ func getTopIOProcesses() ([]model.ProcessIOInfo, error) {
}
func RenderInputOutput() string {
- return vars.CardStyle.Render("⏳ Loading I/O Performance Metrics...")
+ return vars.CardStyle.Render(i18n.T("⏳ Loading I/O Performance Metrics..."))
}
func RenderInputOutputWithData(metrics *model.IOMetrics, loading bool) string {
if loading {
- return vars.CardStyle.Render("⏳ Loading I/O Performance Metrics...")
+ return vars.CardStyle.Render(i18n.T("⏳ Loading I/O Performance Metrics..."))
}
if metrics == nil {
- return vars.CardStyle.Render("⏳ Loading I/O Performance Metrics...")
+ return vars.CardStyle.Render(i18n.T("⏳ Loading I/O Performance Metrics..."))
}
var b strings.Builder
// Title
titleStyle := lipgloss.NewStyle().Bold(true).Foreground(vars.AccentColor).MarginBottom(1)
- b.WriteString(titleStyle.Render("─ I/O PERFORMANCE ANALYSIS ─"))
+ b.WriteString(titleStyle.Render(i18n.T("─ I/O PERFORMANCE ANALYSIS ─")))
b.WriteString("\n")
// Summary section
- b.WriteString(lipgloss.NewStyle().Bold(true).Render("📊 I/O SUMMARY"))
+ b.WriteString(lipgloss.NewStyle().Bold(true).Render(i18n.T("📊 I/O SUMMARY")))
b.WriteString("\n\n")
// Average latency with color coding
@@ -236,21 +237,21 @@ func RenderInputOutputWithData(metrics *model.IOMetrics, loading bool) string {
latencyText := lipgloss.NewStyle().Foreground(latencyColor).Bold(true).Render(
fmt.Sprintf("%s %.2f ms", latencyIcon, metrics.AverageLatency),
)
- b.WriteString(fmt.Sprintf("│ Average Latency: %s ", latencyText))
+ b.WriteString(fmt.Sprintf(i18n.T("│ Average Latency: %s "), latencyText))
// Last update
lastUpdateText := lipgloss.NewStyle().Foreground(lipgloss.Color("244")).Render(
metrics.LastUpdate.Format("15:04:05"),
)
- b.WriteString(fmt.Sprintf("│ Last Update: %s\n", lastUpdateText))
+ b.WriteString(fmt.Sprintf(i18n.T("│ Last Update: %s\n"), lastUpdateText))
b.WriteString("\n")
// Create summary table
summaryColumns := []table.Column{
- {Title: "Total Read IOPS", Width: 16},
- {Title: "Total Write IOPS", Width: 17},
- {Title: "Total Read", Width: 15},
- {Title: "Total Write", Width: 16},
+ {Title: i18n.T("Total Read IOPS"), Width: 16},
+ {Title: i18n.T("Total Write IOPS"), Width: 17},
+ {Title: i18n.T("Total Read"), Width: 15},
+ {Title: i18n.T("Total Write"), Width: 16},
}
summaryRows := []table.Row{
@@ -287,18 +288,18 @@ func RenderInputOutputWithData(metrics *model.IOMetrics, loading bool) string {
// Disk I/O table
if len(metrics.Disks) > 0 {
- b.WriteString(lipgloss.NewStyle().Bold(true).Render("💾 DISK I/O PERFORMANCE"))
+ b.WriteString(lipgloss.NewStyle().Bold(true).Render(i18n.T("💾 DISK I/O PERFORMANCE")))
b.WriteString("\n\n")
// Create table
columns := []table.Column{
- {Title: "Device", Width: 8},
- {Title: "Read IOPS", Width: 10},
- {Title: "Write IOPS", Width: 11},
- {Title: "Read MB/s", Width: 10},
- {Title: "Write MB/s", Width: 11},
+ {Title: i18n.T("Device"), Width: 8},
+ {Title: i18n.T("Read IOPS"), Width: 10},
+ {Title: i18n.T("Write IOPS"), Width: 11},
+ {Title: i18n.T("Read MB/s"), Width: 10},
+ {Title: i18n.T("Write MB/s"), Width: 11},
{Title: "Util%", Width: 6},
- {Title: "Queue", Width: 6},
+ {Title: i18n.T("Queue"), Width: 6},
}
var rows []table.Row
@@ -341,16 +342,16 @@ func RenderInputOutputWithData(metrics *model.IOMetrics, loading bool) string {
// Top I/O Processes
if len(metrics.TopProcesses) > 0 {
- b.WriteString(lipgloss.NewStyle().Bold(true).Render("🔥 TOP I/O PROCESSES"))
+ b.WriteString(lipgloss.NewStyle().Bold(true).Render(i18n.T("🔥 TOP I/O PROCESSES")))
b.WriteString("\n\n")
columns := []table.Column{
{Title: "PID", Width: 6},
- {Title: "Command", Width: 20},
- {Title: "Read IOPS", Width: 10},
- {Title: "Write IOPS", Width: 11},
- {Title: "Read MB", Width: 10},
- {Title: "Write MB", Width: 11},
+ {Title: i18n.T("Command"), Width: 20},
+ {Title: i18n.T("Read IOPS"), Width: 10},
+ {Title: i18n.T("Write IOPS"), Width: 11},
+ {Title: i18n.T("Read MB"), Width: 10},
+ {Title: i18n.T("Write MB"), Width: 11},
}
var rows []table.Row
diff --git a/system/performance/memory.go b/system/performance/memory.go
index c147778..e4f6c62 100644
--- a/system/performance/memory.go
+++ b/system/performance/memory.go
@@ -2,6 +2,7 @@ package performance
import (
"fmt"
+ "github.com/Server-Pulse/server-pulse/i18n"
"strings"
"time"
@@ -15,7 +16,7 @@ import (
// RenderMemoryOverview renders the always-visible memory overview section
func RenderMemoryOverview(metrics *model.MemoryMetrics) string {
if metrics == nil {
- return vars.CardStyle.Render("Loading memory data...")
+ return vars.CardStyle.Render(i18n.T("Loading memory data..."))
}
titleStyle := lipgloss.NewStyle().
@@ -37,19 +38,19 @@ func RenderMemoryOverview(metrics *model.MemoryMetrics) string {
// Create progress bar for memory usage
usageBar := createMemoryProgressBar(metrics.UsedPercent, 50)
- overview := titleStyle.Render("MEMORY OVERVIEW") + "\n\n"
+ overview := titleStyle.Render(i18n.T("MEMORY OVERVIEW")) + "\n\n"
- overview += labelStyle.Render("Total Memory:") + " " +
+ overview += labelStyle.Render(i18n.T("Total Memory:")) + " " +
valueStyle.Render(formatBytes(metrics.Total)) + "\n"
- overview += labelStyle.Render("Used:") + " " +
+ overview += labelStyle.Render(i18n.T("Used:")) + " " +
valueStyle.Render(fmt.Sprintf("%s (%.1f%%)",
formatBytes(metrics.Used), metrics.UsedPercent)) + "\n"
- overview += labelStyle.Render("Available:") + " " +
+ overview += labelStyle.Render(i18n.T("Available:")) + " " +
valueStyle.Render(formatBytes(metrics.Available)) + "\n"
- overview += labelStyle.Render("Free:") + " " +
+ overview += labelStyle.Render(i18n.T("Free:")) + " " +
valueStyle.Render(formatBytes(metrics.Free)) + "\n\n"
overview += progressStyle.Render(usageBar) + "\n"
@@ -60,7 +61,7 @@ func RenderMemoryOverview(metrics *model.MemoryMetrics) string {
// RenderMemoryUsageBreakdown renders the Usage Breakdown sub-tab
func RenderMemoryUsageBreakdown(metrics *model.MemoryMetrics) string {
if metrics == nil {
- return vars.CardStyle.Render("Loading memory usage data...")
+ return vars.CardStyle.Render(i18n.T("Loading memory usage data..."))
}
titleStyle := lipgloss.NewStyle().
@@ -80,42 +81,42 @@ func RenderMemoryUsageBreakdown(metrics *model.MemoryMetrics) string {
Foreground(lipgloss.Color("241")).
Italic(true)
- content := titleStyle.Render("USAGE BREAKDOWN") + "\n\n"
+ content := titleStyle.Render(i18n.T("USAGE BREAKDOWN")) + "\n\n"
// Application Memory
appMemPercent := float64(metrics.ApplicationMem) / float64(metrics.Total) * 100
- content += labelStyle.Render("Application Memory:") + " " +
+ content += labelStyle.Render(i18n.T("Application Memory:")) + " " +
valueStyle.Render(fmt.Sprintf("%s (%.1f%%)",
formatBytes(metrics.ApplicationMem), appMemPercent)) + "\n"
- content += " " + descStyle.Render("Memory actively used by applications") + "\n\n"
+ content += " " + descStyle.Render(i18n.T("Memory actively used by applications")) + "\n\n"
// Buffers
buffersPercent := float64(metrics.Buffers) / float64(metrics.Total) * 100
- content += labelStyle.Render("Buffers:") + " " +
+ content += labelStyle.Render(i18n.T("Buffers:")) + " " +
valueStyle.Render(fmt.Sprintf("%s (%.1f%%)",
formatBytes(metrics.Buffers), buffersPercent)) + "\n"
- content += " " + descStyle.Render("I/O buffers for disk operations") + "\n\n"
+ content += " " + descStyle.Render(i18n.T("I/O buffers for disk operations")) + "\n\n"
// Cached
cachedPercent := float64(metrics.Cached) / float64(metrics.Total) * 100
- content += labelStyle.Render("Cached:") + " " +
+ content += labelStyle.Render(i18n.T("Cached:")) + " " +
valueStyle.Render(fmt.Sprintf("%s (%.1f%%)",
formatBytes(metrics.Cached), cachedPercent)) + "\n"
- content += " " + descStyle.Render("File system cache (can be freed if needed)") + "\n\n"
+ content += " " + descStyle.Render(i18n.T("File system cache (can be freed if needed)")) + "\n\n"
// Available
availablePercent := float64(metrics.Available) / float64(metrics.Total) * 100
- content += labelStyle.Render("Available:") + " " +
+ content += labelStyle.Render(i18n.T("Available:")) + " " +
valueStyle.Render(fmt.Sprintf("%s (%.1f%%)",
formatBytes(metrics.Available), availablePercent)) + "\n"
- content += " " + descStyle.Render("Memory available for applications") + "\n\n"
+ content += " " + descStyle.Render(i18n.T("Memory available for applications")) + "\n\n"
// Shared
sharedPercent := float64(metrics.Shared) / float64(metrics.Total) * 100
- content += labelStyle.Render("Shared:") + " " +
+ content += labelStyle.Render(i18n.T("Shared:")) + " " +
valueStyle.Render(fmt.Sprintf("%s (%.1f%%)",
formatBytes(metrics.Shared), sharedPercent)) + "\n"
- content += " " + descStyle.Render("Memory shared between processes") + "\n"
+ content += " " + descStyle.Render(i18n.T("Memory shared between processes")) + "\n"
return vars.CardStyle.Render(content)
}
@@ -123,7 +124,7 @@ func RenderMemoryUsageBreakdown(metrics *model.MemoryMetrics) string {
// RenderMemorySwapAnalysis renders the Swap Analysis sub-tab
func RenderMemorySwapAnalysis(metrics *model.MemoryMetrics) string {
if metrics == nil {
- return vars.CardStyle.Render("Loading swap data...")
+ return vars.CardStyle.Render(i18n.T("Loading swap data..."))
}
titleStyle := lipgloss.NewStyle().
@@ -142,21 +143,21 @@ func RenderMemorySwapAnalysis(metrics *model.MemoryMetrics) string {
progressStyle := lipgloss.NewStyle().
Width(50)
- content := titleStyle.Render("SWAP ANALYSIS") + "\n\n"
+ content := titleStyle.Render(i18n.T("SWAP ANALYSIS")) + "\n\n"
// Swap Overview
if metrics.SwapTotal == 0 {
- content += labelStyle.Render("Swap Status:") + " " +
- lipgloss.NewStyle().Foreground(lipgloss.Color("241")).Render("No swap configured") + "\n"
+ content += labelStyle.Render(i18n.T("Swap Status:")) + " " +
+ lipgloss.NewStyle().Foreground(lipgloss.Color("241")).Render(i18n.T("No swap configured")) + "\n"
} else {
- content += labelStyle.Render("Swap Total:") + " " +
+ content += labelStyle.Render(i18n.T("Swap Total:")) + " " +
valueStyle.Render(formatBytes(metrics.SwapTotal)) + "\n"
- content += labelStyle.Render("Swap Used:") + " " +
+ content += labelStyle.Render(i18n.T("Swap Used:")) + " " +
valueStyle.Render(fmt.Sprintf("%s (%.1f%%)",
formatBytes(metrics.SwapUsed), metrics.SwapUsedPercent)) + "\n"
- content += labelStyle.Render("Swap Free:") + " " +
+ content += labelStyle.Render(i18n.T("Swap Free:")) + " " +
valueStyle.Render(formatBytes(metrics.SwapFree)) + "\n\n"
// Swap progress bar
@@ -164,13 +165,13 @@ func RenderMemorySwapAnalysis(metrics *model.MemoryMetrics) string {
content += progressStyle.Render(swapBar) + "\n\n"
// SwapCached
- content += labelStyle.Render("Swap Cached:") + " " +
+ content += labelStyle.Render(i18n.T("Swap Cached:")) + " " +
valueStyle.Render(formatBytes(metrics.SwapCached)) + "\n"
content += " " + lipgloss.NewStyle().Foreground(lipgloss.Color("241")).Italic(true).
- Render("Swap memory also present in RAM") + "\n\n"
+ Render(i18n.T("Swap memory also present in RAM")) + "\n\n"
// Health Status
- content += titleStyle.Render("STATUS") + "\n\n"
+ content += titleStyle.Render(i18n.T("STATUS")) + "\n\n"
status := getSwapHealthStatus(metrics)
content += status
}
@@ -181,7 +182,7 @@ func RenderMemorySwapAnalysis(metrics *model.MemoryMetrics) string {
// RenderMemorySystemMemory renders the System Memory sub-tab
func RenderMemorySystemMemory(metrics *model.MemoryMetrics) string {
if metrics == nil {
- return vars.CardStyle.Render("Loading system memory data...")
+ return vars.CardStyle.Render(i18n.T("Loading system memory data..."))
}
titleStyle := lipgloss.NewStyle().
@@ -201,38 +202,38 @@ func RenderMemorySystemMemory(metrics *model.MemoryMetrics) string {
Foreground(lipgloss.Color("241")).
Italic(true)
- content := titleStyle.Render("SYSTEM MEMORY") + "\n\n"
+ content := titleStyle.Render(i18n.T("SYSTEM MEMORY")) + "\n\n"
// Dirty Pages
dirtyPercent := float64(metrics.Dirty) / float64(metrics.Total) * 100
- content += labelStyle.Render("Dirty Pages:") + " " +
+ content += labelStyle.Render(i18n.T("Dirty Pages:")) + " " +
valueStyle.Render(fmt.Sprintf("%s (%.1f%%)",
formatBytes(metrics.Dirty), dirtyPercent)) + "\n"
- content += " " + descStyle.Render("Modified data waiting to be written to disk") + "\n\n"
+ content += " " + descStyle.Render(i18n.T("Modified data waiting to be written to disk")) + "\n\n"
// WriteBack
writebackPercent := float64(metrics.WriteBack) / float64(metrics.Total) * 100
- content += labelStyle.Render("WriteBack:") + " " +
+ content += labelStyle.Render(i18n.T("WriteBack:")) + " " +
valueStyle.Render(fmt.Sprintf("%s (%.1f%%)",
formatBytes(metrics.WriteBack), writebackPercent)) + "\n"
- content += " " + descStyle.Render("Data currently being written to disk") + "\n\n"
+ content += " " + descStyle.Render(i18n.T("Data currently being written to disk")) + "\n\n"
// Slab
slabPercent := float64(metrics.Slab) / float64(metrics.Total) * 100
- content += labelStyle.Render("Slab:") + " " +
+ content += labelStyle.Render(i18n.T("Slab:")) + " " +
valueStyle.Render(fmt.Sprintf("%s (%.1f%%)",
formatBytes(metrics.Slab), slabPercent)) + "\n"
- content += " " + descStyle.Render("Kernel data structure cache") + "\n\n"
+ content += " " + descStyle.Render(i18n.T("Kernel data structure cache")) + "\n\n"
// PageTables
pageTablesPercent := float64(metrics.PageTables) / float64(metrics.Total) * 100
- content += labelStyle.Render("Page Tables:") + " " +
+ content += labelStyle.Render(i18n.T("Page Tables:")) + " " +
valueStyle.Render(fmt.Sprintf("%s (%.1f%%)",
formatBytes(metrics.PageTables), pageTablesPercent)) + "\n"
- content += " " + descStyle.Render("Memory used for page table management") + "\n\n"
+ content += " " + descStyle.Render(i18n.T("Memory used for page table management")) + "\n\n"
// Diagnostics
- content += titleStyle.Render("DIAGNOSTICS") + "\n\n"
+ content += titleStyle.Render(i18n.T("DIAGNOSTICS")) + "\n\n"
if metrics.Dirty > 1024*1024*1024 { // > 1GB
content += lipgloss.NewStyle().Foreground(lipgloss.Color("220")).Render("⚠ ") +
@@ -267,26 +268,26 @@ func getSwapHealthStatus(metrics *model.MemoryMetrics) string {
// No swap used - good
if metrics.SwapUsed == 0 {
- return goodStyle.Render("✓ No swap used") + " - System has sufficient RAM\n"
+ return goodStyle.Render(i18n.T("✓ No swap used")) + i18n.T(" - System has sufficient RAM\n")
}
// Swap used but memory available - configuration issue
availablePercent := float64(metrics.Available) / float64(metrics.Total) * 100
if metrics.SwapUsed > 0 && availablePercent > 20 {
- return warningStyle.Render("⚠ Swap used with available RAM") +
+ return warningStyle.Render(i18n.T("⚠ Swap used with available RAM")) +
" - Check swappiness setting\n" +
" Recommendation: Reduce vm.swappiness value\n"
}
// Swap used and memory low - critical
if metrics.SwapUsedPercent > 50 {
- return criticalStyle.Render("✗ High swap usage") +
+ return criticalStyle.Render(i18n.T("✗ High swap usage")) +
" - System under memory pressure\n" +
" Recommendation: Add more RAM or reduce memory usage\n"
}
// Moderate swap usage
- return warningStyle.Render("⚠ Moderate swap usage") +
+ return warningStyle.Render(i18n.T("⚠ Moderate swap usage")) +
" - Monitor for performance issues\n"
}
diff --git a/system/performance/systemHealth.go b/system/performance/systemHealth.go
index cb293cd..7906b9d 100644
--- a/system/performance/systemHealth.go
+++ b/system/performance/systemHealth.go
@@ -3,6 +3,7 @@ package performance
import (
"bufio"
"fmt"
+ "github.com/Server-Pulse/server-pulse/i18n"
"os"
"strconv"
"strings"
@@ -115,43 +116,43 @@ func CalculateHealthScore(metrics *HealthMetrics) *HealthScore {
var checksPerformed []string
// IOWait
- checksPerformed = append(checksPerformed, "IOWait")
+ checksPerformed = append(checksPerformed, i18n.T("IOWait"))
if metrics.IOWait > 20 {
score -= 30
- issues = append(issues, fmt.Sprintf("IOWait very high (%.2f%%)", metrics.IOWait))
- recommendations = append(recommendations, "Investigate disk activity, upgrade to faster storage (SSD/NVMe)")
+ issues = append(issues, fmt.Sprintf(i18n.T("IOWait very high (%.2f%%)"), metrics.IOWait))
+ recommendations = append(recommendations, i18n.T("Investigate disk activity, upgrade to faster storage (SSD/NVMe)"))
} else if metrics.IOWait > 10 {
score -= 15
- issues = append(issues, fmt.Sprintf("IOWait elevated (%.2f%%)", metrics.IOWait))
- recommendations = append(recommendations, "Check for processes with high disk I/O, consider upgrading storage")
+ issues = append(issues, fmt.Sprintf(i18n.T("IOWait elevated (%.2f%%)"), metrics.IOWait))
+ recommendations = append(recommendations, i18n.T("Check for processes with high disk I/O, consider upgrading storage"))
}
// CPU Steal Time
- checksPerformed = append(checksPerformed, "CPU Steal Time")
+ checksPerformed = append(checksPerformed, i18n.T("CPU Steal Time"))
if metrics.StealTime > 10 {
score -= 20
- issues = append(issues, fmt.Sprintf("CPU Steal Time high (%.2f%%)", metrics.StealTime))
- recommendations = append(recommendations, "Check host machine load, consider migrating VM or upgrading host")
+ issues = append(issues, fmt.Sprintf(i18n.T("CPU Steal Time high (%.2f%%)"), metrics.StealTime))
+ recommendations = append(recommendations, i18n.T("Check host machine load, consider migrating VM or upgrading host"))
} else if metrics.StealTime > 5 {
score -= 10
- issues = append(issues, fmt.Sprintf("CPU Steal Time elevated (%.2f%%)", metrics.StealTime))
- recommendations = append(recommendations, "Monitor host machine performance")
+ issues = append(issues, fmt.Sprintf(i18n.T("CPU Steal Time elevated (%.2f%%)"), metrics.StealTime))
+ recommendations = append(recommendations, i18n.T("Monitor host machine performance"))
}
// Major Page Faults
- checksPerformed = append(checksPerformed, "Major Page Faults")
+ checksPerformed = append(checksPerformed, i18n.T("Major Page Faults"))
if metrics.MajorFaults > 10000 {
score -= 15
- issues = append(issues, fmt.Sprintf("High number of major page faults (%s)", formatNumber(metrics.MajorFaults)))
- recommendations = append(recommendations, "Investigate memory usage, consider increasing RAM")
+ issues = append(issues, fmt.Sprintf(i18n.T("High number of major page faults (%s)"), formatNumber(metrics.MajorFaults)))
+ recommendations = append(recommendations, i18n.T("Investigate memory usage, consider increasing RAM"))
}
// Context Switches
- checksPerformed = append(checksPerformed, "Context Switches")
+ checksPerformed = append(checksPerformed, i18n.T("Context Switches"))
if metrics.ContextSwitches > 100000000 { // 100 million
score -= 10
- issues = append(issues, fmt.Sprintf("High number of context switches (%s)", formatNumber(metrics.ContextSwitches)))
- recommendations = append(recommendations, "Optimize multi-threaded applications, check for excessive context switching")
+ issues = append(issues, fmt.Sprintf(i18n.T("High number of context switches (%s)"), formatNumber(metrics.ContextSwitches)))
+ recommendations = append(recommendations, i18n.T("Optimize multi-threaded applications, check for excessive context switching"))
}
if score < 0 {
diff --git a/system/performance/view.go b/system/performance/view.go
index 4f1569b..6b7a9bb 100644
--- a/system/performance/view.go
+++ b/system/performance/view.go
@@ -2,6 +2,7 @@ package performance
import (
"fmt"
+ "github.com/Server-Pulse/server-pulse/i18n"
"strings"
"github.com/Server-Pulse/server-pulse/widgets/vars"
@@ -11,18 +12,18 @@ import (
func RenderSystemHealthView(healthLoading bool, healthMetrics *HealthMetrics, healthScore *HealthScore) string {
if healthLoading {
- return vars.CardStyle.Render("⏳ Loading System Health...")
+ return vars.CardStyle.Render(i18n.T("⏳ Loading System Health..."))
}
if healthMetrics == nil || healthScore == nil {
- return vars.CardStyle.Render("Press 'r' to load system health metrics.")
+ return vars.CardStyle.Render(i18n.T("Press 'r' to load system health metrics."))
}
var b strings.Builder
// Title
titleStyle := lipgloss.NewStyle().Bold(true).Foreground(vars.AccentColor).MarginBottom(1)
- b.WriteString(titleStyle.Render("─ SYSTEM HEALTH ANALYSIS ─"))
+ b.WriteString(titleStyle.Render(i18n.T("─ SYSTEM HEALTH ANALYSIS ─")))
b.WriteString("\n")
// Health Score
@@ -38,11 +39,11 @@ func RenderSystemHealthView(healthLoading bool, healthMetrics *HealthMetrics, he
} else if healthScore.Score < 80 {
healthStatus = " [Fair]"
}
- b.WriteString(fmt.Sprintf("│ Health Score: %s%s%s \n\n", prog.ViewAs(float64(healthScore.Score)/100.0), scoreStr, healthStatus))
+ b.WriteString(fmt.Sprintf(i18n.T("│ Health Score: %s%s%s \n\n"), prog.ViewAs(float64(healthScore.Score)/100.0), scoreStr, healthStatus))
// Detected Issues
if len(healthScore.Issues) > 0 {
- b.WriteString(lipgloss.NewStyle().Foreground(vars.ErrorColor).Render("⚠️ DETECTED ISSUES"))
+ b.WriteString(lipgloss.NewStyle().Foreground(vars.ErrorColor).Render(i18n.T("⚠️ DETECTED ISSUES")))
b.WriteString("\n")
for _, issue := range healthScore.Issues {
b.WriteString(fmt.Sprintf("- %s \n", issue))
@@ -52,7 +53,7 @@ func RenderSystemHealthView(healthLoading bool, healthMetrics *HealthMetrics, he
// Recommendations
if len(healthScore.Recommendations) > 0 {
- b.WriteString(lipgloss.NewStyle().Foreground(vars.SuccessColor).Render("💡 RECOMMENDATIONS"))
+ b.WriteString(lipgloss.NewStyle().Foreground(vars.SuccessColor).Render(i18n.T("💡 RECOMMENDATIONS")))
b.WriteString("\n")
for _, rec := range healthScore.Recommendations {
b.WriteString(fmt.Sprintf("- %s \n", rec))
@@ -62,7 +63,7 @@ func RenderSystemHealthView(healthLoading bool, healthMetrics *HealthMetrics, he
// Checks Performed
if len(healthScore.ChecksPerformed) > 0 {
- b.WriteString(lipgloss.NewStyle().Bold(true).Render("CHECKS PERFORMED"))
+ b.WriteString(lipgloss.NewStyle().Bold(true).Render(i18n.T("CHECKS PERFORMED")))
b.WriteString("\n")
for _, check := range healthScore.ChecksPerformed {
b.WriteString(fmt.Sprintf("- %s \n", check))
diff --git a/system/security/SystemRestart.go b/system/security/SystemRestart.go
index 58168c0..3be95bf 100644
--- a/system/security/SystemRestart.go
+++ b/system/security/SystemRestart.go
@@ -2,6 +2,7 @@ package security
import (
"fmt"
+ "github.com/Server-Pulse/server-pulse/i18n"
"os"
"os/exec"
"regexp"
@@ -18,7 +19,7 @@ func (sm *SecurityManager) checkSystemRestart() SecurityCheck {
for _, check := range checks {
if required, reason := check(); required {
return SecurityCheck{
- Name: "System Restart",
+ Name: i18n.T("System Restart"),
Status: "Required",
Details: reason,
}
@@ -26,9 +27,9 @@ func (sm *SecurityManager) checkSystemRestart() SecurityCheck {
}
return SecurityCheck{
- Name: "System Restart",
+ Name: i18n.T("System Restart"),
Status: "Not Required",
- Details: "No pending system restart required",
+ Details: i18n.T("No pending system restart required"),
}
}
@@ -36,9 +37,9 @@ func checkRebootRequired() (bool, string) {
if _, err := os.Stat("/var/run/reboot-required"); err == nil {
if content, err := os.ReadFile("/var/run/reboot-required.pkgs"); err == nil {
packages := strings.TrimSpace(string(content))
- return true, fmt.Sprintf("Reboot required due to package updates: %s", packages)
+ return true, fmt.Sprintf(i18n.T("Reboot required due to package updates: %s"), packages)
}
- return true, "Reboot required flag detected"
+ return true, i18n.T("Reboot required flag detected")
}
return false, ""
}
@@ -65,7 +66,7 @@ func checkKernelUpdate() (bool, string) {
for _, match := range matches {
if len(match) > 1 && match[1] != currentVersion {
- return true, fmt.Sprintf("Newer kernel available: %s (current: %s)", match[1], currentVersion)
+ return true, fmt.Sprintf(i18n.T("Newer kernel available: %s (current: %s)"), match[1], currentVersion)
}
}
@@ -84,7 +85,7 @@ func checkSystemdRestart() (bool, string) {
}
}
if failedServices > 0 {
- return true, fmt.Sprintf("%d failed services detected - restart may be needed", failedServices)
+ return true, fmt.Sprintf(i18n.T("%d failed services detected - restart may be needed"), failedServices)
}
}
diff --git a/system/security/SystemUpdates.go b/system/security/SystemUpdates.go
index 1224376..a695b4f 100644
--- a/system/security/SystemUpdates.go
+++ b/system/security/SystemUpdates.go
@@ -2,6 +2,7 @@ package security
import (
"fmt"
+ "github.com/Server-Pulse/server-pulse/i18n"
"os/exec"
"strings"
)
@@ -20,9 +21,9 @@ func (sm *SecurityManager) checkSystemUpdates() SecurityCheck {
}
return SecurityCheck{
- Name: "System Updates",
+ Name: i18n.T("System Updates"),
Status: "Unknown",
- Details: "Could not determine update status - package manager not detected",
+ Details: i18n.T("Could not determine update status - package manager not detected"),
}
}
@@ -104,15 +105,15 @@ func (sm *SecurityManager) handleYumDnfResult(mgr struct {
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 100 {
count, _ := mgr.parseFunc(string(output))
return SecurityCheck{
- Name: "System Updates",
+ Name: i18n.T("System Updates"),
Status: "Updates Available",
- Details: fmt.Sprintf("%d updates available via %s", count, mgr.name),
+ Details: fmt.Sprintf(i18n.T("%d updates available via %s"), count, mgr.name),
}
} else if err == nil {
return SecurityCheck{
- Name: "System Updates",
+ Name: i18n.T("System Updates"),
Status: "Up to date",
- Details: fmt.Sprintf("All packages are up to date (%s)", mgr.name),
+ Details: fmt.Sprintf(i18n.T("All packages are up to date (%s)"), mgr.name),
}
}
@@ -129,14 +130,14 @@ func (sm *SecurityManager) handleAptResult(mgr struct {
count, _ := mgr.parseFunc(string(output))
if count > 0 {
return SecurityCheck{
- Name: "System Updates",
+ Name: i18n.T("System Updates"),
Status: "Updates Available",
- Details: fmt.Sprintf("%d updates available via %s", count, mgr.name),
+ Details: fmt.Sprintf(i18n.T("%d updates available via %s"), count, mgr.name),
}
}
return SecurityCheck{
- Name: "System Updates",
+ Name: i18n.T("System Updates"),
Status: "Up to date",
- Details: fmt.Sprintf("All packages are up to date (%s)", mgr.name),
+ Details: fmt.Sprintf(i18n.T("All packages are up to date (%s)"), mgr.name),
}
}
diff --git a/system/security/autoBane.go b/system/security/autoBane.go
index ab42d72..a96f204 100644
--- a/system/security/autoBane.go
+++ b/system/security/autoBane.go
@@ -2,6 +2,7 @@ package security
import (
"fmt"
+ "github.com/Server-Pulse/server-pulse/i18n"
"os/exec"
"strings"
@@ -42,9 +43,9 @@ func (sm *SecurityManager) checkAutoBan() SecurityCheck {
}
return SecurityCheck{
- Name: "Auto Ban",
+ Name: i18n.T("Auto Ban"),
Status: "Disabled",
- Details: "No intrusion prevention service detected (fail2ban, crowdsec, ossec, suricata, snort, denyhosts, sshguard)",
+ Details: i18n.T("No intrusion prevention service detected (fail2ban, crowdsec, ossec, suricata, snort, denyhosts, sshguard)"),
}
}
@@ -62,7 +63,7 @@ func (sm *SecurityManager) checkSystemdServices() SecurityCheck {
if status == "active" {
details := service.detailsFunc()
return SecurityCheck{
- Name: "Auto Ban",
+ Name: i18n.T("Auto Ban"),
Status: "Enabled",
Details: details,
}
@@ -73,9 +74,9 @@ func (sm *SecurityManager) checkSystemdServices() SecurityCheck {
cmd := exec.Command("systemctl", "is-active", "ossec-hids")
if output, err := cmd.Output(); err == nil && strings.TrimSpace(string(output)) == "active" {
return SecurityCheck{
- Name: "Auto Ban",
+ Name: i18n.T("Auto Ban"),
Status: "Enabled",
- Details: "ossec-hids is active with real-time monitoring",
+ Details: i18n.T("ossec-hids is active with real-time monitoring"),
}
}
}
@@ -201,9 +202,9 @@ func (sm *SecurityManager) checkRunningProcesses() SecurityCheck {
if len(activeServices) > 0 {
return SecurityCheck{
- Name: "Auto Ban",
+ Name: i18n.T("Auto Ban"),
Status: "Enabled",
- Details: fmt.Sprintf("Active intrusion prevention: %s", strings.Join(activeServices, ", ")),
+ Details: fmt.Sprintf(i18n.T("Active intrusion prevention: %s"), strings.Join(activeServices, ", ")),
}
}
@@ -249,7 +250,7 @@ func (sm *SecurityManager) DisplayAutoBanInfos() tea.Cmd {
return AutoBanMsg(AutoBanInfos{
ServiceType: "None",
Status: "Disabled",
- Details: "No intrusion prevention service detected",
+ Details: i18n.T("No intrusion prevention service detected"),
Jails: []AutoBanJail{},
BannedIPs: []string{},
RawOutput: "",
@@ -276,7 +277,7 @@ func (sm *SecurityManager) getFail2banDetails() AutoBanInfos {
return AutoBanInfos{
ServiceType: "fail2ban",
Status: "Active",
- Details: "fail2ban is running but unable to get details",
+ Details: i18n.T("fail2ban is running but unable to get details"),
Jails: []AutoBanJail{},
BannedIPs: []string{},
}
@@ -333,7 +334,7 @@ func (sm *SecurityManager) getFail2banDetails() AutoBanInfos {
Version: version,
Jails: jails,
BannedIPs: bannedIPs,
- Details: fmt.Sprintf("fail2ban is active with %d configured jails", len(jails)),
+ Details: fmt.Sprintf(i18n.T("fail2ban is active with %d configured jails"), len(jails)),
RawOutput: rawOutput,
}
}
@@ -377,7 +378,7 @@ func parseFailfbanJail(name string, output string) AutoBanJail {
}
if jail.Details == "" {
- jail.Details = fmt.Sprintf("Currently: %d banned, Total: %d banned", jail.CurrentBans, jail.TotalBans)
+ jail.Details = fmt.Sprintf(i18n.T("Currently: %d banned, Total: %d banned"), jail.CurrentBans, jail.TotalBans)
}
return jail
@@ -400,7 +401,7 @@ func (sm *SecurityManager) getCrowdsecDetails() AutoBanInfos {
ServiceType: "CrowdSec",
Status: "Active",
Version: version,
- Details: "CrowdSec is active with behavioral analysis",
+ Details: i18n.T("CrowdSec is active with behavioral analysis"),
Jails: []AutoBanJail{},
BannedIPs: []string{},
RawOutput: string(output),
@@ -412,10 +413,10 @@ func (sm *SecurityManager) getOSSECDetails() AutoBanInfos {
return AutoBanInfos{
ServiceType: "OSSEC",
Status: "Active",
- Details: "OSSEC HIDS is active with real-time monitoring",
+ Details: i18n.T("OSSEC HIDS is active with real-time monitoring"),
Jails: []AutoBanJail{},
BannedIPs: []string{},
- RawOutput: "OSSEC is a host-based intrusion detection system",
+ RawOutput: i18n.T("OSSEC is a host-based intrusion detection system"),
}
}
@@ -424,10 +425,10 @@ func (sm *SecurityManager) getDenyhostsDetails() AutoBanInfos {
return AutoBanInfos{
ServiceType: "DenyHosts",
Status: "Active",
- Details: "DenyHosts is enabled and active",
+ Details: i18n.T("DenyHosts is enabled and active"),
Jails: []AutoBanJail{},
BannedIPs: []string{},
- RawOutput: "DenyHosts monitors SSH login attempts",
+ RawOutput: i18n.T("DenyHosts monitors SSH login attempts"),
}
}
@@ -436,10 +437,10 @@ func (sm *SecurityManager) getSSHGuardDetails() AutoBanInfos {
return AutoBanInfos{
ServiceType: "SSHGuard",
Status: "Active",
- Details: "SSHGuard is enabled and active",
+ Details: i18n.T("SSHGuard is enabled and active"),
Jails: []AutoBanJail{},
BannedIPs: []string{},
- RawOutput: "SSHGuard protects SSH and other services",
+ RawOutput: i18n.T("SSHGuard protects SSH and other services"),
}
}
@@ -461,7 +462,7 @@ func (sm *SecurityManager) getSuricataDetails() AutoBanInfos {
ServiceType: "Suricata",
Status: "Active",
Version: version,
- Details: "Suricata IDS/IPS is active with network intrusion detection",
+ Details: i18n.T("Suricata IDS/IPS is active with network intrusion detection"),
Jails: []AutoBanJail{},
BannedIPs: []string{},
RawOutput: string(output),
@@ -485,7 +486,7 @@ func (sm *SecurityManager) getSnortDetails() AutoBanInfos {
ServiceType: "Snort",
Status: "Active",
Version: version,
- Details: "Snort IDS/IPS is active with network intrusion detection",
+ Details: i18n.T("Snort IDS/IPS is active with network intrusion detection"),
Jails: []AutoBanJail{},
BannedIPs: []string{},
RawOutput: string(output),
diff --git a/system/security/firewall.go b/system/security/firewall.go
index b1f0e64..68c9f36 100644
--- a/system/security/firewall.go
+++ b/system/security/firewall.go
@@ -2,6 +2,7 @@ package security
import (
"fmt"
+ "github.com/Server-Pulse/server-pulse/i18n"
"os/exec"
"strings"
@@ -96,15 +97,15 @@ func (sm *SecurityManager) analyzeFirewallOutput(firewallName string, output []b
func (sm *SecurityManager) analyzeUFWStatus(outputStr string) *SecurityCheck {
if strings.Contains(outputStr, "status: active") {
return &SecurityCheck{
- Name: "Firewall Status",
+ Name: i18n.T("Firewall Status"),
Status: "Active",
- Details: "UFW is active and properly configured",
+ Details: i18n.T("UFW is active and properly configured"),
}
} else if strings.Contains(outputStr, "status: inactive") {
return &SecurityCheck{
- Name: "Firewall Status",
+ Name: i18n.T("Firewall Status"),
Status: "Inactive",
- Details: "UFW is installed but inactive",
+ Details: i18n.T("UFW is installed but inactive"),
}
}
return nil
@@ -113,9 +114,9 @@ func (sm *SecurityManager) analyzeUFWStatus(outputStr string) *SecurityCheck {
func (sm *SecurityManager) analyzeFirewalldStatus(outputStr string) *SecurityCheck {
if strings.Contains(outputStr, "running") {
return &SecurityCheck{
- Name: "Firewall Status",
+ Name: i18n.T("Firewall Status"),
Status: "Active",
- Details: "firewalld is active and properly configured",
+ Details: i18n.T("firewalld is active and properly configured"),
}
}
return nil
@@ -124,9 +125,9 @@ func (sm *SecurityManager) analyzeFirewalldStatus(outputStr string) *SecurityChe
func (sm *SecurityManager) analyzeIptablesStatus(output []byte) *SecurityCheck {
if len(strings.Split(string(output), "\n")) > 10 {
return &SecurityCheck{
- Name: "Firewall Status",
+ Name: i18n.T("Firewall Status"),
Status: "Active",
- Details: "iptables rules are configured",
+ Details: i18n.T("iptables rules are configured"),
}
}
return nil
@@ -135,9 +136,9 @@ func (sm *SecurityManager) analyzeIptablesStatus(output []byte) *SecurityCheck {
func (sm *SecurityManager) analyzeNftablesStatus(output []byte) *SecurityCheck {
if len(strings.Split(string(output), "\n")) > 10 {
return &SecurityCheck{
- Name: "Firewall Status",
+ Name: i18n.T("Firewall Status"),
Status: "Active",
- Details: "nftables rules are configured",
+ Details: i18n.T("nftables rules are configured"),
}
}
return nil
@@ -145,9 +146,9 @@ func (sm *SecurityManager) analyzeNftablesStatus(output []byte) *SecurityCheck {
func (sm *SecurityManager) createInactiveFirewallCheck() SecurityCheck {
return SecurityCheck{
- Name: "Firewall Status",
+ Name: i18n.T("Firewall Status"),
Status: "Inactive",
- Details: "No active firewall detected (UFW, firewalld, or iptables)",
+ Details: i18n.T("No active firewall detected (UFW, firewalld, or iptables)"),
}
}
@@ -186,7 +187,7 @@ func (sm *SecurityManager) DisplayFirewallInfos() tea.Cmd {
FirewallType: "None",
Status: "Inactive",
Rules: []FirewallRule{},
- Details: "No active firewall detected",
+ Details: i18n.T("No active firewall detected"),
RawOutput: "",
})
}
@@ -234,12 +235,12 @@ func (sm *SecurityManager) getRawFirewallOutput(firewallType string) string {
cmd = exec.Command("nft", "list", "ruleset")
}
default:
- return "No firewall detected"
+ return i18n.T("No firewall detected")
}
output, err := cmd.Output()
if err != nil {
- return fmt.Sprintf("Error retrieving raw output: %v", err)
+ return fmt.Sprintf(i18n.T("Error retrieving raw output: %v"), err)
}
return string(output)
diff --git a/system/security/passwordPolicy.go b/system/security/passwordPolicy.go
index d90526a..d187076 100644
--- a/system/security/passwordPolicy.go
+++ b/system/security/passwordPolicy.go
@@ -2,6 +2,7 @@ package security
import (
"fmt"
+ "github.com/Server-Pulse/server-pulse/i18n"
"os"
"regexp"
"strings"
@@ -24,19 +25,19 @@ func (sm *SecurityManager) checkPasswordPolicy() SecurityCheck {
if strings.Contains(contentStr, "pam_pwquality") ||
strings.Contains(contentStr, "pam_cracklib") {
hasPolicy = true
- details = append(details, fmt.Sprintf("Password policy found in %s", file))
+ details = append(details, fmt.Sprintf(i18n.T("Password policy found in %s"), file))
if strings.Contains(contentStr, "minlen") {
re := regexp.MustCompile(`minlen=(\d+)`)
if matches := re.FindStringSubmatch(contentStr); len(matches) > 1 {
- details = append(details, fmt.Sprintf("Minimum length: %s", matches[1]))
+ details = append(details, fmt.Sprintf(i18n.T("Minimum length: %s"), matches[1]))
}
}
if strings.Contains(contentStr, "minclass") {
re := regexp.MustCompile(`minclass=(\d+)`)
if matches := re.FindStringSubmatch(contentStr); len(matches) > 1 {
- details = append(details, fmt.Sprintf("Minimum character classes: %s", matches[1]))
+ details = append(details, fmt.Sprintf(i18n.T("Minimum character classes: %s"), matches[1]))
}
}
}
@@ -45,15 +46,15 @@ func (sm *SecurityManager) checkPasswordPolicy() SecurityCheck {
if hasPolicy {
return SecurityCheck{
- Name: "Password Policy",
+ Name: i18n.T("Password Policy"),
Status: "Enabled",
Details: strings.Join(details, ", "),
}
}
return SecurityCheck{
- Name: "Password Policy",
+ Name: i18n.T("Password Policy"),
Status: "Disabled",
- Details: "No password complexity policy found",
+ Details: i18n.T("No password complexity policy found"),
}
}
diff --git a/system/security/ports.go b/system/security/ports.go
index 1498ff9..82cd504 100644
--- a/system/security/ports.go
+++ b/system/security/ports.go
@@ -2,6 +2,7 @@ package security
import (
"fmt"
+ "github.com/Server-Pulse/server-pulse/i18n"
"os/exec"
"strconv"
"strings"
@@ -89,17 +90,17 @@ func (sm *SecurityManager) checkOpenPorts() SecurityCheck {
openPorts, err := o.GetOpenedPorts(sm)
if err != nil {
return SecurityCheck{
- Name: "Open Ports",
+ Name: i18n.T("Open Ports"),
Status: "Error",
- Details: fmt.Sprintf("Failed to get open ports: %v", err),
+ Details: fmt.Sprintf(i18n.T("Failed to get open ports: %v"), err),
}
}
if len(openPorts) == 0 {
return SecurityCheck{
- Name: "Open Ports",
+ Name: i18n.T("Open Ports"),
Status: "Secure",
- Details: "No open ports detected",
+ Details: i18n.T("No open ports detected"),
}
}
@@ -127,21 +128,21 @@ func (sm *SecurityManager) checkOpenPorts() SecurityCheck {
func analyzePortRisk(port int) (RiskLevel, string) {
switch port {
case 22:
- return Warning, "Port 22 (SSH) is open. Change default ssh port for more security."
+ return Warning, i18n.T("Port 22 (SSH) is open. Change default ssh port for more security.")
case 20, 21:
- return Warning, "Port 20/21 (FTP) is open. FTP is insecure, consider using SFTP or FTPS."
+ return Warning, i18n.T("Port 20/21 (FTP) is open. FTP is insecure, consider using SFTP or FTPS.")
case 23:
- return HighRisk, "Port 23 (Telnet) is open. Telnet is insecure and should be closed."
+ return HighRisk, i18n.T("Port 23 (Telnet) is open. Telnet is insecure and should be closed.")
case 161, 162:
- return Warning, "Port 161/162 (SNMP) is open. SNMP can expose sensitive information."
+ return Warning, i18n.T("Port 161/162 (SNMP) is open. SNMP can expose sensitive information.")
case 137, 138, 139:
- return HighRisk, "Port 137/138/139 (NetBIOS), you can be attacked by null sessions."
+ return HighRisk, i18n.T("Port 137/138/139 (NetBIOS), you can be attacked by null sessions.")
case 445:
- return HighRisk, "Port 445 (SMB) is open. SMB has had many vulnerabilities."
+ return HighRisk, i18n.T("Port 445 (SMB) is open. SMB has had many vulnerabilities.")
case 3389:
- return HighRisk, "Port 3389 (RDP) is open. RDP is often targeted by attackers."
+ return HighRisk, i18n.T("Port 3389 (RDP) is open. RDP is often targeted by attackers.")
case 3306, 5432, 6379, 27017:
- return HighRisk, "Database port exposed. Databases should not be accessible from internet."
+ return HighRisk, i18n.T("Database port exposed. Databases should not be accessible from internet.")
default:
return Secure, ""
}
@@ -151,29 +152,29 @@ func buildSecurityCheckResult(maxRisk RiskLevel, allPorts map[int]bool, riskPort
switch maxRisk {
case HighRisk:
return SecurityCheck{
- Name: "Open Ports",
+ Name: i18n.T("Open Ports"),
Status: "High Risk",
- Details: fmt.Sprintf("Critical ports detected (%s): %s",
+ Details: fmt.Sprintf(i18n.T("Critical ports detected (%s): %s"),
strings.Join(riskPorts, ", "), strings.Join(findings, " | ")),
}
case Warning:
return SecurityCheck{
- Name: "Open Ports",
+ Name: i18n.T("Open Ports"),
Status: "Warning",
Details: strings.Join(findings, " | "),
}
default:
return SecurityCheck{
- Name: "Open Ports",
+ Name: i18n.T("Open Ports"),
Status: "Secure",
- Details: fmt.Sprintf("%d ports open: %s", len(allPorts), ShowOpenedPorts(allPorts)),
+ Details: fmt.Sprintf(i18n.T("%d ports open: %s"), len(allPorts), ShowOpenedPorts(allPorts)),
}
}
}
func ShowOpenedPorts(ports map[int]bool) string {
if len(ports) == 0 {
- return "No open ports detected"
+ return i18n.T("No open ports detected")
}
var portList []string
diff --git a/system/security/sshPassword.go b/system/security/sshPassword.go
index 8fdf8ed..7303dc2 100644
--- a/system/security/sshPassword.go
+++ b/system/security/sshPassword.go
@@ -2,6 +2,7 @@ package security
import (
"fmt"
+ "github.com/Server-Pulse/server-pulse/i18n"
"os/exec"
"strings"
)
@@ -11,9 +12,9 @@ func (sm *SecurityManager) checkSSHPasswordAuthentication() SecurityCheck {
output, err := cmd.Output()
if err != nil {
return SecurityCheck{
- Name: "SSH Password Authentication",
+ Name: i18n.T("SSH Password Authentication"),
Status: "Error",
- Details: fmt.Sprintf("Failed to check SSH config: %v", err),
+ Details: fmt.Sprintf(i18n.T("Failed to check SSH config: %v"), err),
}
}
@@ -36,15 +37,15 @@ func (sm *SecurityManager) checkSSHPasswordAuthentication() SecurityCheck {
if passwordAuth == "yes" {
return SecurityCheck{
- Name: "SSH Password Authentication",
+ Name: i18n.T("SSH Password Authentication"),
Status: "Enabled",
- Details: "SSH password authentication is enabled - consider disabling for better security",
+ Details: i18n.T("SSH password authentication is enabled - consider disabling for better security"),
}
}
return SecurityCheck{
- Name: "SSH Password Authentication",
+ Name: i18n.T("SSH Password Authentication"),
Status: "Disabled",
- Details: "SSH password authentication is disabled",
+ Details: i18n.T("SSH password authentication is disabled"),
}
}
diff --git a/system/security/sshRoot.go b/system/security/sshRoot.go
index 1f32130..20a3784 100644
--- a/system/security/sshRoot.go
+++ b/system/security/sshRoot.go
@@ -2,6 +2,7 @@ package security
import (
"fmt"
+ "github.com/Server-Pulse/server-pulse/i18n"
"os/exec"
"strings"
@@ -72,9 +73,9 @@ func (sm *SecurityManager) checkSSHRootLogin() SecurityCheck {
config, err := s.GetActiveConfig(sm)
if err != nil {
return SecurityCheck{
- Name: "SSH Root Login",
+ Name: i18n.T("SSH Root Login"),
Status: "Error",
- Details: fmt.Sprintf("Failed to get SSH config: %v", err),
+ Details: fmt.Sprintf(i18n.T("Failed to get SSH config: %v"), err),
}
}
@@ -85,23 +86,23 @@ func (sm *SecurityManager) checkSSHRootLogin() SecurityCheck {
}
status := "Disabled"
- details := "SSH root login is disabled."
+ details := i18n.T("SSH root login is disabled.")
switch strings.ToLower(permitRootLogin) {
case "yes":
status = "Enabled (with password)"
- details = "Root login with a password is permitted. This is a high security risk."
+ details = i18n.T("Root login with a password is permitted. This is a high security risk.")
case "without-password", "prohibit-password":
status = "Enabled (key-only)"
- details = "Root can login, but only with key-based authentication."
+ details = i18n.T("Root can login, but only with key-based authentication.")
case "forced-commands-only":
status = "Enabled (commands-only)"
- details = "Root can login via public key, but only to run specific commands."
+ details = i18n.T("Root can login via public key, but only to run specific commands.")
case "no":
}
return SecurityCheck{
- Name: "SSH Root Login",
+ Name: i18n.T("SSH Root Login"),
Status: status,
Details: details,
}
diff --git a/system/security/ssl.go b/system/security/ssl.go
index 862eba8..41cabf3 100644
--- a/system/security/ssl.go
+++ b/system/security/ssl.go
@@ -4,6 +4,7 @@ import (
"crypto/tls"
"crypto/x509"
"fmt"
+ "github.com/Server-Pulse/server-pulse/i18n"
"time"
tea "github.com/charmbracelet/bubbletea"
@@ -36,9 +37,9 @@ type CertificateDisplayMsg CertificateInfos
func (sm *SecurityManager) checkSSLCertificate(domaine string) SecurityCheck {
if domaine == "" {
return SecurityCheck{
- Name: "SSL Certificate",
+ Name: i18n.T("SSL Certificate"),
Status: "Error",
- Details: "Domain name is required",
+ Details: i18n.T("Domain name is required"),
}
}
@@ -49,9 +50,9 @@ func (sm *SecurityManager) checkSSLCertificate(domaine string) SecurityCheck {
})
if err != nil {
return SecurityCheck{
- Name: "SSL Certificate",
+ Name: i18n.T("SSL Certificate"),
Status: "Error",
- Details: fmt.Sprintf("Failed to connect to %v: %v", domaine, err),
+ Details: fmt.Sprintf(i18n.T("Failed to connect to %v: %v"), domaine, err),
}
}
defer conn.Close()
@@ -61,9 +62,9 @@ func (sm *SecurityManager) checkSSLCertificate(domaine string) SecurityCheck {
if len(state.PeerCertificates) == 0 {
return SecurityCheck{
- Name: "SSL Certificate",
+ Name: i18n.T("SSL Certificate"),
Status: "Invalid",
- Details: "No SSL certificate found",
+ Details: i18n.T("No SSL certificate found"),
}
}
@@ -73,26 +74,26 @@ func (sm *SecurityManager) checkSSLCertificate(domaine string) SecurityCheck {
if cert.NotAfter.Before(now) {
return SecurityCheck{
- Name: "SSL Certificate",
+ Name: i18n.T("SSL Certificate"),
Status: "Invalid",
- Details: "Certificate has expired",
+ Details: i18n.T("Certificate has expired"),
}
}
if cert.NotAfter.Before(now.Add(30 * 24 * time.Hour)) {
daysUntilExpiration := cert.NotAfter.Sub(now).Hours() / 24
return SecurityCheck{
- Name: "SSL Certificate",
+ Name: i18n.T("SSL Certificate"),
Status: "Warning",
- Details: fmt.Sprintf("Certificate expires soon in %.0f days", daysUntilExpiration),
+ Details: fmt.Sprintf(i18n.T("Certificate expires soon in %.0f days"), daysUntilExpiration),
}
}
daysUntilExpiration := time.Until(cert.NotAfter).Hours() / 24
return SecurityCheck{
- Name: "SSL Certificate",
+ Name: i18n.T("SSL Certificate"),
Status: "Valid",
- Details: fmt.Sprintf("Certificate expires in %.0f days", daysUntilExpiration),
+ Details: fmt.Sprintf(i18n.T("Certificate expires in %.0f days"), daysUntilExpiration),
}
}
@@ -104,7 +105,7 @@ func DisplayCertificateInfo(cert *x509.Certificate, hostname string) Certificate
otherNames := cert.DNSNames
if len(otherNames) == 0 {
- otherNames = []string{"No alternative names"}
+ otherNames = []string{i18n.T("No alternative names")}
}
return CertificateInfos{
Subject: cert.Subject.CommonName,
diff --git a/utils/test/utils_test.go b/utils/test/utils_test.go
index c311343..80ce8d7 100644
--- a/utils/test/utils_test.go
+++ b/utils/test/utils_test.go
@@ -146,11 +146,11 @@ func TestFormatUptime(t *testing.T) {
input uint64
expected string
}{
- {"Minutes only", 300, "5 minutes"},
- {"Hours and minutes", 3660, "1 hours, 1 minutes"},
- {"Days and hours", 90000, "1 days, 1 hours"},
- {"Multiple days", 172800, "2 days, 0 hours"},
- {"Zero seconds", 0, "0 minutes"},
+ {"Minutes only", 300, "5 分钟"},
+ {"Hours and minutes", 3660, "1 小时 1 分钟"},
+ {"Days and hours", 90000, "1 天 1 小时"},
+ {"Multiple days", 172800, "2 天 0 小时"},
+ {"Zero seconds", 0, "0 分钟"},
}
for _, tt := range tests {
@@ -264,12 +264,12 @@ func TestFormatOperationMessage(t *testing.T) {
err error
expected string
}{
- {"Success restart", "restart", true, nil, "✅ Container restarted successfully"},
- {"Failure restart", "restart", false, nil, "❌ Container restarted failed"},
- {"Failure with error", "start", false, fmt.Errorf("permission denied"), "❌ Container started failed: permission denied"},
- {"Unknown operation success", "unknown", true, nil, "✅ Operation 'unknown' successfully"},
- {"Unknown operation failure", "unknown", false, nil, "❌ Operation 'unknown' failed"},
- {"Empty operation", "", true, nil, "✅ Operation '' successfully"},
+ {"Success restart", "restart", true, nil, "✅ 容器已重启成功"},
+ {"Failure restart", "restart", false, nil, "❌ 容器已重启失败"},
+ {"Failure with error", "start", false, fmt.Errorf("permission denied"), "❌ 容器已启动失败: permission denied"},
+ {"Unknown operation success", "unknown", true, nil, "✅ 操作 'unknown'成功"},
+ {"Unknown operation failure", "unknown", false, nil, "❌ 操作 'unknown'失败"},
+ {"Empty operation", "", true, nil, "✅ 操作 ''成功"},
}
for _, tt := range tests {
diff --git a/utils/utils.go b/utils/utils.go
index ec016a9..9c7372b 100644
--- a/utils/utils.go
+++ b/utils/utils.go
@@ -4,6 +4,7 @@ import (
"bufio"
"encoding/binary"
"fmt"
+ "github.com/Server-Pulse/server-pulse/i18n"
"net"
"os"
"os/exec"
@@ -81,11 +82,11 @@ func FormatUptime(seconds uint64) string {
minutes := (seconds % (60 * 60)) / 60
if days > 0 {
- return fmt.Sprintf("%d days, %d hours", days, hours)
+ return fmt.Sprintf(i18n.T("%d days, %d hours"), days, hours)
} else if hours > 0 {
- return fmt.Sprintf("%d hours, %d minutes", hours, minutes)
+ return fmt.Sprintf(i18n.T("%d hours, %d minutes"), hours, minutes)
} else {
- return fmt.Sprintf("%d minutes", minutes)
+ return fmt.Sprintf(i18n.T("%d minutes"), minutes)
}
}
@@ -138,7 +139,7 @@ func CheckDockerPermissions() (bool, string) {
// Step 1: Check if the 'docker' command exists in the user's PATH (cross-platform)
_, err := exec.LookPath("docker")
if err != nil {
- return false, "The 'docker' command was not found. Please ensure Docker is installed."
+ return false, i18n.T("The 'docker' command was not found. Please ensure Docker is installed.")
}
// Step 2: Platform-specific permission checks
@@ -146,12 +147,12 @@ func CheckDockerPermissions() (bool, string) {
case "linux": // Linux
currentUser, err := user.Current()
if err != nil {
- return false, fmt.Sprintf("Error getting the current user: %v", err)
+ return false, fmt.Sprintf(i18n.T("Error getting the current user: %v"), err)
}
groups, err := currentUser.GroupIds()
if err != nil {
- return false, fmt.Sprintf("Error getting user's groups: %v", err)
+ return false, fmt.Sprintf(i18n.T("Error getting user's groups: %v"), err)
}
for _, groupID := range groups {
@@ -160,43 +161,43 @@ func CheckDockerPermissions() (bool, string) {
continue
}
if group.Name == "docker" || group.Name == "root" {
- return true, "The user has permissions to run Docker."
+ return true, i18n.T("The user has permissions to run Docker.")
}
}
- return false, "The user is not in the 'docker' group. To add them, run 'sudo usermod -aG docker ' and then log out and log back in."
+ return false, i18n.T("The user is not in the 'docker' group. To add them, run 'sudo usermod -aG docker ' and then log out and log back in.")
default:
- return false, fmt.Sprintf("Unsupported operating system: %s", runtime.GOOS)
+ return false, fmt.Sprintf(i18n.T("Unsupported operating system: %s"), runtime.GOOS)
}
}
func FormatOperationMessage(operation string, success bool, err error) string {
operationLabels := map[string]string{
- "restart": "Container restarted",
- "start": "Container started",
- "stop": "Container stopped",
- "pause": "Container paused",
- "unpause": "Container resumed",
- "delete": "Container deleted",
- "toggle_start": "Container state changed",
- "toggle_pause": "Container pause state changed",
- "exec": "Shell opened",
- "logs": "Logs loaded",
+ "restart": i18n.T("Container restarted"),
+ "start": i18n.T("Container started"),
+ "stop": i18n.T("Container stopped"),
+ "pause": i18n.T("Container paused"),
+ "unpause": i18n.T("Container resumed"),
+ "delete": i18n.T("Container deleted"),
+ "toggle_start": i18n.T("Container state changed"),
+ "toggle_pause": i18n.T("Container pause state changed"),
+ "exec": i18n.T("Shell opened"),
+ "logs": i18n.T("Logs loaded"),
}
label, exists := operationLabels[operation]
if !exists {
- label = fmt.Sprintf("Operation '%s'", operation)
+ label = fmt.Sprintf(i18n.T("Operation '%s'"), operation)
}
if success {
- return fmt.Sprintf("✅ %s successfully", label)
+ return fmt.Sprintf(i18n.T("✅ %s successfully"), label)
} else {
if err != nil {
- return fmt.Sprintf("❌ %s failed: %v", label, err)
+ return fmt.Sprintf(i18n.T("❌ %s failed: %v"), label, err)
}
- return fmt.Sprintf("❌ %s failed", label)
+ return fmt.Sprintf(i18n.T("❌ %s failed"), label)
}
}
diff --git a/widgets/auth/styles.go b/widgets/auth/styles.go
index e8bd5d1..8318675 100644
--- a/widgets/auth/styles.go
+++ b/widgets/auth/styles.go
@@ -1,6 +1,7 @@
package auth
import (
+ "github.com/Server-Pulse/server-pulse/i18n"
"github.com/charmbracelet/lipgloss"
)
@@ -33,33 +34,24 @@ var (
Foreground(lipgloss.Color("214")) // Orange
)
-const (
- AuthRequiredMessage = "🔐 Admin Authentication Required"
-
- AuthSuccessMessage = "✅ Authentication Successful"
-
- AuthFailedMessage = "❌ Authentication Failed"
-
- AuthInProgressMessage = "⏳ Authenticating..."
-
- AuthPromptMessage = "Enter password for admin access:"
-
- AuthInstructions = "Press Enter to authenticate, Esc to cancel"
-
- AuthRetryMessage = "Press 'a' to retry authentication"
-
- AdminAccessGranted = "✅ Admin access granted"
-
- AdminAccessRequired = "🔒 Some checks require admin privileges. Press 'a' to authenticate"
-
- SudoNotAvailable = "Sudo is not available. Please run as root."
-
- InvalidPasswordMessage = "Invalid password or insufficient privileges"
-
- AuthenticationFailedGeneric = "Authentication failed"
-
- LockedCheckIndicator = "🔒"
-)
+const LockedCheckIndicator = "🔒"
+
+func AuthRequiredMessage() string { return i18n.T("🔐 Admin Authentication Required") }
+func AuthSuccessMessage() string { return i18n.T("✅ Authentication Successful") }
+func AuthFailedMessage() string { return i18n.T("❌ Authentication Failed") }
+func AuthInProgressMessage() string { return i18n.T("⏳ Authenticating...") }
+func AuthPromptMessage() string { return i18n.T("Enter password for admin access:") }
+func AuthInstructions() string { return i18n.T("Press Enter to authenticate, Esc to cancel") }
+func AuthRetryMessage() string { return i18n.T("Press 'a' to retry authentication") }
+func AdminAccessGranted() string { return i18n.T("✅ Admin access granted") }
+func AdminAccessRequired() string {
+ return i18n.T("🔒 Some checks require admin privileges. Press 'a' to authenticate")
+}
+func SudoNotAvailable() string { return i18n.T("Sudo is not available. Please run as root.") }
+func InvalidPasswordMessage() string {
+ return i18n.T("Invalid password or insufficient privileges")
+}
+func AuthenticationFailedGeneric() string { return i18n.T("Authentication failed") }
func GetAuthMessage(state int, customMessage string) string {
switch state {
@@ -67,16 +59,16 @@ func GetAuthMessage(state int, customMessage string) string {
if customMessage != "" {
return customMessage
}
- return AuthRequiredMessage
+ return AuthRequiredMessage()
case 2: // AuthInProgress
- return AuthInProgressMessage
+ return AuthInProgressMessage()
case 3: // AuthSuccess
- return AuthSuccessMessage
+ return AuthSuccessMessage()
case 4: // AuthFailed
if customMessage != "" {
return customMessage
}
- return AuthFailedMessage
+ return AuthFailedMessage()
default:
return ""
}
diff --git a/widgets/handles-common.go b/widgets/handles-common.go
index a092826..8db4235 100644
--- a/widgets/handles-common.go
+++ b/widgets/handles-common.go
@@ -2,6 +2,7 @@ package widgets
import (
"fmt"
+ "github.com/Server-Pulse/server-pulse/i18n"
"math"
"strings"
"time"
@@ -107,7 +108,7 @@ func (m *Model) setContainerLogs(logs string) {
func (m *Model) updateLogsViewport() {
if len(m.Monitor.ContainerLogsPagination.Lines) == 0 {
- m.LogsViewport.SetContent("No logs available")
+ m.LogsViewport.SetContent(i18n.T("No logs available"))
return
}
@@ -178,7 +179,7 @@ func calculateConnectivityContentLines(m Model) int {
tempContent := strings.Builder{}
if len(m.Network.PingResults) > 0 {
- tempContent.WriteString("Ping Results\n")
+ tempContent.WriteString(i18n.T("Ping Results\n"))
for _, result := range m.Network.PingResults {
statusIcon := "❌"
if result.Success {
@@ -186,9 +187,9 @@ func calculateConnectivityContentLines(m Model) int {
}
tempContent.WriteString(fmt.Sprintf("%s %s - ", statusIcon, result.Target))
if result.Success {
- tempContent.WriteString(fmt.Sprintf("Latency: %v, Packet Loss: %.1f%%", result.Latency, result.PacketLoss))
+ tempContent.WriteString(fmt.Sprintf(i18n.T("Latency: %v, Packet Loss: %.1f%%"), result.Latency, result.PacketLoss))
} else {
- tempContent.WriteString(fmt.Sprintf("Error: %s", result.Error))
+ tempContent.WriteString(i18n.T("Error: ") + result.Error)
}
tempContent.WriteString("\n")
}
@@ -197,9 +198,9 @@ func calculateConnectivityContentLines(m Model) int {
for _, tracerouteResult := range m.Network.TracerouteResults {
if tracerouteResult.Target != "" {
- tempContent.WriteString(fmt.Sprintf("Traceroute to %s\n", tracerouteResult.Target))
+ tempContent.WriteString(fmt.Sprintf(i18n.T("Traceroute to %s\n"), tracerouteResult.Target))
if tracerouteResult.Error != "" {
- tempContent.WriteString("Error: " + tracerouteResult.Error + "\n")
+ tempContent.WriteString(i18n.T("Error: ") + tracerouteResult.Error + "\n")
} else if len(tracerouteResult.Hops) > 0 {
for _, hop := range tracerouteResult.Hops {
hopLine := fmt.Sprintf("%2d. ", hop.HopNumber)
@@ -217,7 +218,7 @@ func calculateConnectivityContentLines(m Model) int {
tempContent.WriteString(hopLine + "\n")
}
} else {
- tempContent.WriteString("No route found\n")
+ tempContent.WriteString(i18n.T("No route found\n"))
}
tempContent.WriteString("\n")
}
@@ -225,9 +226,9 @@ func calculateConnectivityContentLines(m Model) int {
// Speed test results
for _, result := range m.Network.SpeedTestResults {
- tempContent.WriteString("Speed Test Results\n")
+ tempContent.WriteString(i18n.T("Speed Test Results\n"))
tempContent.WriteString(fmt.Sprintf("Server: %s\n", result.Server))
- tempContent.WriteString(fmt.Sprintf("Test Duration: %v\n\n", result.TestDuration))
+ tempContent.WriteString(fmt.Sprintf(i18n.T("Test Duration: %v\n\n"), result.TestDuration))
if result.PingResult != nil {
tempContent.WriteString("Latency (Ping):\n")
diff --git a/widgets/handles.go b/widgets/handles.go
index 6b14ceb..27bf1d7 100644
--- a/widgets/handles.go
+++ b/widgets/handles.go
@@ -2,6 +2,7 @@ package widgets
import (
"fmt"
+ "github.com/Server-Pulse/server-pulse/i18n"
"os"
"path/filepath"
"slices"
@@ -124,7 +125,7 @@ func (m Model) handleContainerRelatedMsgs(msg tea.Msg) (tea.Model, tea.Cmd) {
if container.ID == m.Monitor.SelectedContainer.ID {
if strings.ToLower(container.Status) != "up" && m.Monitor.ContainerLogsStreaming {
m.cleanupLogsStream()
- m.LastOperationMsg = fmt.Sprintf("Container stopped, streaming disabled (status: %s)", container.Status)
+ m.LastOperationMsg = fmt.Sprintf(i18n.T("Container stopped, streaming disabled (status: %s)"), container.Status)
}
break
}
@@ -139,10 +140,10 @@ func (m Model) handleContainerRelatedMsgs(msg tea.Msg) (tea.Model, tea.Cmd) {
m.Monitor.ContainerLogsLoading = false
if logsMsg.Error != nil {
if strings.Contains(logsMsg.Error.Error(), "streaming unavailable") {
- m.Monitor.ContainerLogs = fmt.Sprintf("Streaming not available: %v\nShowing static logs instead...", logsMsg.Error)
+ m.Monitor.ContainerLogs = fmt.Sprintf(i18n.T("Streaming not available: %v\nShowing static logs instead..."), logsMsg.Error)
return m, m.Monitor.App.GetContainerLogsCmd(m.Monitor.SelectedContainer.ID)
} else {
- m.Monitor.ContainerLogs = fmt.Sprintf("Error loading logs: %v", logsMsg.Error)
+ m.Monitor.ContainerLogs = fmt.Sprintf(i18n.T("Error loading logs: %v"), logsMsg.Error)
}
m.Monitor.ContainerLogsPagination.Lines = []string{m.Monitor.ContainerLogs}
m.Monitor.ContainerLogsPagination.TotalPages = 1
@@ -230,6 +231,15 @@ func (m Model) handleKeyMsg(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
return m.handleSearchKeys(msg)
}
+ // Global language toggle (Shift+L / Ctrl+L). Plain "l" remains navigation.
+ switch msg.String() {
+ case "L", "ctrl+l":
+ i18n.Toggle()
+ m.applyLocale()
+ m.LastOperationMsg = fmt.Sprintf(i18n.T("Language: %s"), i18n.Label())
+ return m, nil
+ }
+
switch m.Ui.State {
case model.StateHome:
return m.handleHomeKeys(msg)
@@ -550,19 +560,19 @@ func (m Model) handleNetworkKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
case "enter":
if m.Diagnostic.Password.Value() != "" {
m.Network.AuthState = model.AuthInProgress
- m.Network.AuthMessage = "Authenticating..."
+ m.Network.AuthMessage = i18n.T("Authenticating...")
// Try to authenticate
err := m.setRoot()
if err != nil {
m.Network.AuthState = model.AuthFailed
- m.Network.AuthMessage = fmt.Sprintf("Authentication failed: %v", err)
+ m.Network.AuthMessage = fmt.Sprintf(i18n.T("Authentication failed: %v"), err)
if !m.SudoAvailable {
m.Network.AuthMessage += "\nSudo is not available. Please run as root."
}
m.Diagnostic.Password.Reset()
} else {
m.Network.AuthState = model.AuthSuccess
- m.Network.AuthMessage = "Authentication successful!"
+ m.Network.AuthMessage = i18n.T("Authentication successful!")
m.AsRoot = true
m.Network.AuthTimer = 2
@@ -889,13 +899,13 @@ func (m Model) handleConnectivityInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
// Clear password input and reset echo mode
m.Network.TracerouteInput.SetValue("")
m.Network.TracerouteInput.EchoMode = textinput.EchoNormal
- m.Network.TracerouteInput.Placeholder = "Enter target (IP or domain)..."
+ m.Network.TracerouteInput.Placeholder = i18n.T("Enter target (IP or domain)...")
m.Network.ConnectivityMode = model.ConnectivityModeNone
m.Network.TracerouteInstallTarget = ""
// Start installation
m.OperationInProgress = true
- m.LastOperationMsg = "Installing traceroute..."
+ m.LastOperationMsg = i18n.T("Installing traceroute...")
m.resetSpinner()
return m, tea.Batch(
network.InstallTraceroute(target, password),
@@ -908,7 +918,7 @@ func (m Model) handleConnectivityInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
m.Network.PingInput.SetValue("")
m.Network.TracerouteInput.SetValue("")
m.Network.TracerouteInput.EchoMode = textinput.EchoNormal
- m.Network.TracerouteInput.Placeholder = "Enter target (IP or domain)..."
+ m.Network.TracerouteInput.Placeholder = i18n.T("Enter target (IP or domain)...")
m.Network.TracerouteInstallTarget = ""
m.Network.PingLoading = false
m.Network.TracerouteLoading = false
@@ -1050,19 +1060,19 @@ func (m Model) handleDiagnosticsKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
case "enter":
if m.Diagnostic.Password.Value() != "" {
m.Diagnostic.AuthState = model.AuthInProgress
- m.Diagnostic.AuthMessage = "Authenticating..."
+ m.Diagnostic.AuthMessage = i18n.T("Authenticating...")
// Try to authenticate
err := m.setRoot()
if err != nil {
m.Diagnostic.AuthState = model.AuthFailed
- m.Diagnostic.AuthMessage = fmt.Sprintf("Authentication failed: %v", err)
+ m.Diagnostic.AuthMessage = fmt.Sprintf(i18n.T("Authentication failed: %v"), err)
if !m.SudoAvailable {
m.Diagnostic.AuthMessage += "\nSudo is not available. Please run as root."
}
m.Diagnostic.Password.Reset()
} else {
m.Diagnostic.AuthState = model.AuthSuccess
- m.Diagnostic.AuthMessage = "Authentication successful!"
+ m.Diagnostic.AuthMessage = i18n.T("Authentication successful!")
m.AsRoot = true
m.Diagnostic.AuthTimer = 2
@@ -1467,7 +1477,7 @@ func (m Model) handleDiagnosticsKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
case "a":
if m.Diagnostic.SelectedItem == model.DiagnosticSecurityChecks && !m.AsRoot && !m.CanRunSudo {
m.Diagnostic.AuthState = model.AuthRequired
- m.Diagnostic.AuthMessage = "Enter password for admin access:"
+ m.Diagnostic.AuthMessage = i18n.T("Enter password for admin access:")
m.Diagnostic.Password.Focus()
m.Diagnostic.Password.SetValue("")
}
@@ -1550,7 +1560,7 @@ func (m Model) handleDiagnosticsKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
// Check if admin privileges are required for this diagnostic
if !m.canAccessDiagnostic(checkName) {
m.Diagnostic.AuthState = model.AuthRequired
- m.Diagnostic.AuthMessage = fmt.Sprintf("Admin privileges required for: %s\nEnter password:", checkName)
+ m.Diagnostic.AuthMessage = fmt.Sprintf(i18n.T("Admin privileges required for: %s\nEnter password:"), checkName)
m.Diagnostic.Password.Focus()
m.Diagnostic.Password.SetValue("")
return m, nil
@@ -1558,15 +1568,15 @@ func (m Model) handleDiagnosticsKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
// Execute the diagnostic check
switch checkName {
- case "SSL Certificate":
+ case i18n.T("SSL Certificate"):
return m, m.Diagnostic.SecurityManager.RunCertificateDisplay()
- case "SSH Root Login":
+ case i18n.T("SSH Root Login"):
return m, m.Diagnostic.SecurityManager.DisplaySSHRootInfos()
- case "Open Ports":
+ case i18n.T("Open Ports"):
return m, m.Diagnostic.SecurityManager.DisplayOpenedPortsInfos()
- case "Firewall Status":
+ case i18n.T("Firewall Status"):
return m, m.Diagnostic.SecurityManager.DisplayFirewallInfos()
- case "Auto Ban":
+ case i18n.T("Auto Ban"):
return m, m.Diagnostic.SecurityManager.DisplayAutoBanInfos()
}
}
@@ -1598,7 +1608,7 @@ func (m Model) handleSecurityCheckMsgs(msg tea.Msg) (tea.Model, tea.Cmd) {
}
if hasAdminChecks && m.Diagnostic.AuthState == model.AuthNotRequired {
- m.Diagnostic.AuthMessage = "Some checks require admin privileges. Press 'a' to authenticate."
+ m.Diagnostic.AuthMessage = i18n.T("Some checks require admin privileges. Press 'a' to authenticate.")
}
}
@@ -1945,21 +1955,21 @@ func (m Model) handleContainerMenuKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
case "r":
m.Monitor.ContainerMenuState = model.ContainerMenuState(0) // ContainerMenuHidden
m.ConfirmationVisible = true
- m.ConfirmationMessage = fmt.Sprintf("Restart container '%s'?\nThis will stop and start the container.", m.Monitor.SelectedContainer.Name)
+ m.ConfirmationMessage = fmt.Sprintf(i18n.T("Restart container '%s'?\nThis will stop and start the container."), m.Monitor.SelectedContainer.Name)
m.ConfirmationAction = "restart"
m.ConfirmationData = m.Monitor.SelectedContainer.ID
return m, nil
case "d":
m.Monitor.ContainerMenuState = model.ContainerMenuState(0) // ContainerMenuHidden
m.ConfirmationVisible = true
- m.ConfirmationMessage = fmt.Sprintf("Delete container '%s'?\nThis action cannot be undone.", m.Monitor.SelectedContainer.Name)
+ m.ConfirmationMessage = fmt.Sprintf(i18n.T("Delete container '%s'?\nThis action cannot be undone."), m.Monitor.SelectedContainer.Name)
m.ConfirmationAction = "delete"
m.ConfirmationData = m.Monitor.SelectedContainer.ID
return m, nil
case "x":
m.Monitor.ContainerMenuState = model.ContainerMenuState(0) // ContainerMenuHidden
m.ConfirmationVisible = true
- m.ConfirmationMessage = fmt.Sprintf("Force remove container '%s'?\nThis action cannot be undone.", m.Monitor.SelectedContainer.Name)
+ m.ConfirmationMessage = fmt.Sprintf(i18n.T("Force remove container '%s'?\nThis action cannot be undone."), m.Monitor.SelectedContainer.Name)
m.ConfirmationAction = "remove"
m.ConfirmationData = m.Monitor.SelectedContainer.ID
return m, nil
@@ -1978,7 +1988,7 @@ func (m Model) handleContainerMenuKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
return m, tea.Quit
case "c":
m.Monitor.ContainerMenuState = model.ContainerMenuState(0) // ContainerMenuHidden
- m.LastOperationMsg = "Commit functionality not yet implemented"
+ m.LastOperationMsg = i18n.T("Commit functionality not yet implemented")
return m, nil
case "esc", "b":
m.Monitor.ContainerMenuState = model.ContainerMenuState(0) // ContainerMenuHidden
@@ -2010,21 +2020,21 @@ func (m Model) executeContainerMenuAction() (tea.Model, tea.Cmd) {
case "restart":
m.Monitor.ContainerMenuState = model.ContainerMenuState(0) // ContainerMenuHidden
m.ConfirmationVisible = true
- m.ConfirmationMessage = fmt.Sprintf("Restart container '%s'?\nThis will stop and start the container.", m.Monitor.SelectedContainer.Name)
+ m.ConfirmationMessage = fmt.Sprintf(i18n.T("Restart container '%s'?\nThis will stop and start the container."), m.Monitor.SelectedContainer.Name)
m.ConfirmationAction = "restart"
m.ConfirmationData = m.Monitor.SelectedContainer.ID
return m, nil
case "delete":
m.Monitor.ContainerMenuState = model.ContainerMenuState(0) // ContainerMenuHidden
m.ConfirmationVisible = true
- m.ConfirmationMessage = fmt.Sprintf("Delete container '%s'?\nThis action cannot be undone.", m.Monitor.SelectedContainer.Name)
+ m.ConfirmationMessage = fmt.Sprintf(i18n.T("Delete container '%s'?\nThis action cannot be undone."), m.Monitor.SelectedContainer.Name)
m.ConfirmationAction = "delete"
m.ConfirmationData = m.Monitor.SelectedContainer.ID
return m, nil
case "remove":
m.Monitor.ContainerMenuState = model.ContainerMenuState(0) // ContainerMenuHidden
m.ConfirmationVisible = true
- m.ConfirmationMessage = fmt.Sprintf("Force remove container '%s'?\nThis action cannot be undone.", m.Monitor.SelectedContainer.Name)
+ m.ConfirmationMessage = fmt.Sprintf(i18n.T("Force remove container '%s'?\nThis action cannot be undone."), m.Monitor.SelectedContainer.Name)
m.ConfirmationAction = "remove"
m.ConfirmationData = m.Monitor.SelectedContainer.ID
return m, nil
@@ -2043,7 +2053,7 @@ func (m Model) executeContainerMenuAction() (tea.Model, tea.Cmd) {
return m, tea.Quit
case "commit":
m.Monitor.ContainerMenuState = model.ContainerMenuState(0) // ContainerMenuHidden
- m.LastOperationMsg = "Commit functionality not yet implemented"
+ m.LastOperationMsg = i18n.T("Commit functionality not yet implemented")
return m, nil
}
return m, nil
@@ -2088,7 +2098,7 @@ func (m Model) handleConfirmationKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
m.Network.TracerouteInstallTarget = target
m.Network.ConnectivityMode = model.ConnectivityModeInstallPassword
m.Network.TracerouteInput.SetValue("")
- m.Network.TracerouteInput.Placeholder = "Enter sudo password..."
+ m.Network.TracerouteInput.Placeholder = i18n.T("Enter sudo password...")
m.Network.TracerouteInput.EchoMode = textinput.EchoPassword
m.Network.TracerouteInput.Focus()
return m, nil
@@ -2343,24 +2353,24 @@ func (m Model) handleNetworkMsgs(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
case network.SpeedTestErrorMsg:
m.Network.SpeedTestLoading = false
- m.LastOperationMsg = "Speed test failed: " + msg.Error
+ m.LastOperationMsg = i18n.T("Speed test failed: ") + msg.Error
return m, clearOperationMessage()
case network.SpeedTestProgressMsg:
// Handle progress updates if needed
- m.LastOperationMsg = fmt.Sprintf("Speed test: %s (%.0f%%)", msg.Message, msg.Progress*100)
+ m.LastOperationMsg = fmt.Sprintf(i18n.T("Speed test: %s (%.0f%%)"), msg.Message, msg.Progress*100)
return m, nil
case network.TracerouteInstallPromptMsg:
// Traceroute is not installed, show confirmation dialog
m.Network.TracerouteLoading = false
m.ConfirmationVisible = true
- m.ConfirmationMessage = "Traceroute is not installed. Would you like to install it?"
+ m.ConfirmationMessage = i18n.T("Traceroute is not installed. Would you like to install it?")
m.ConfirmationAction = "install_traceroute"
m.ConfirmationData = msg.Target
return m, nil
case network.TracerouteInstallResultMsg:
m.OperationInProgress = false
if msg.Success {
- m.LastOperationMsg = "Traceroute installed successfully. Running traceroute..."
+ m.LastOperationMsg = i18n.T("Traceroute installed successfully. Running traceroute...")
m.Network.TracerouteLoading = true
m.resetSpinner()
// Now run traceroute with the original target
@@ -2375,12 +2385,12 @@ func (m Model) handleNetworkMsgs(msg tea.Msg) (tea.Model, tea.Cmd) {
m.Network.TracerouteInstallTarget = msg.Target
m.Network.ConnectivityMode = model.ConnectivityModeInstallPassword
m.Network.TracerouteInput.SetValue("")
- m.Network.TracerouteInput.Placeholder = "Enter sudo password..."
+ m.Network.TracerouteInput.Placeholder = i18n.T("Enter sudo password...")
m.Network.TracerouteInput.EchoMode = textinput.EchoPassword
m.Network.TracerouteInput.Focus()
return m, clearOperationMessage()
}
- m.LastOperationMsg = "Installation failed: " + msg.Error
+ m.LastOperationMsg = i18n.T("Installation failed: ") + msg.Error
return m, clearOperationMessage()
}
}
diff --git a/widgets/help.go b/widgets/help.go
index b8459d0..53a9387 100644
--- a/widgets/help.go
+++ b/widgets/help.go
@@ -1,6 +1,7 @@
package widgets
import (
+ "github.com/Server-Pulse/server-pulse/i18n"
model "github.com/Server-Pulse/server-pulse/widgets/model"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key"
@@ -15,19 +16,27 @@ type KeyMap interface {
// BaseKeyMap contains keys that are available in most states
type BaseKeyMap struct {
- Help key.Binding
- Quit key.Binding
- Back key.Binding
+ Help key.Binding
+ Quit key.Binding
+ Back key.Binding
+ Language key.Binding
}
func (k BaseKeyMap) ShortHelp() []key.Binding {
- return []key.Binding{k.Help, k.Quit}
+ keys := []key.Binding{k.Help}
+ if len(k.Language.Keys()) > 0 {
+ keys = append(keys, k.Language)
+ }
+ keys = append(keys, k.Quit)
+ return keys
}
func (k BaseKeyMap) FullHelp() [][]key.Binding {
- return [][]key.Binding{
- {k.Help, k.Quit, k.Back},
+ row := []key.Binding{k.Help, k.Quit, k.Back}
+ if len(k.Language.Keys()) > 0 {
+ row = []key.Binding{k.Help, k.Language, k.Quit, k.Back}
}
+ return [][]key.Binding{row}
}
// HomeKeyMap defines keybindings for the home state
@@ -42,14 +51,14 @@ type HomeKeyMap struct {
}
func (k HomeKeyMap) ShortHelp() []key.Binding {
- return []key.Binding{k.Select, k.Navigate, k.Help, k.Quit}
+ return []key.Binding{k.Select, k.Navigate, k.Help, k.Language, k.Quit}
}
func (k HomeKeyMap) FullHelp() [][]key.Binding {
return [][]key.Binding{
{k.Select, k.Navigate},
{k.Quick1, k.Quick2, k.Quick3, k.Quick4},
- {k.Help, k.Quit},
+ {k.Help, k.Language, k.Quit},
}
}
@@ -64,7 +73,7 @@ type MonitorKeyMap struct {
}
func (k MonitorKeyMap) ShortHelp() []key.Binding {
- return []key.Binding{k.Select, k.Navigate, k.Help, k.Back, k.Quit}
+ return []key.Binding{k.Select, k.Navigate, k.Help, k.Language, k.Back, k.Quit}
}
func (k MonitorKeyMap) FullHelp() [][]key.Binding {
@@ -82,7 +91,7 @@ type SystemKeyMap struct {
}
func (k SystemKeyMap) ShortHelp() []key.Binding {
- return []key.Binding{k.Scroll, k.Help, k.Back, k.Quit}
+ return []key.Binding{k.Scroll, k.Help, k.Language, k.Back, k.Quit}
}
func (k SystemKeyMap) FullHelp() [][]key.Binding {
@@ -103,7 +112,7 @@ type ProcessKeyMap struct {
}
func (k ProcessKeyMap) ShortHelp() []key.Binding {
- return []key.Binding{k.Navigate, k.Search, k.Help, k.Back, k.Quit}
+ return []key.Binding{k.Navigate, k.Search, k.Help, k.Language, k.Back, k.Quit}
}
func (k ProcessKeyMap) FullHelp() [][]key.Binding {
@@ -123,7 +132,7 @@ type ContainersKeyMap struct {
}
func (k ContainersKeyMap) ShortHelp() []key.Binding {
- return []key.Binding{k.Navigate, k.Select, k.Search, k.Help, k.Back, k.Quit}
+ return []key.Binding{k.Navigate, k.Select, k.Search, k.Help, k.Language, k.Back, k.Quit}
}
func (k ContainersKeyMap) FullHelp() [][]key.Binding {
@@ -140,7 +149,7 @@ type ContainerKeyMap struct {
}
func (k ContainerKeyMap) ShortHelp() []key.Binding {
- return []key.Binding{k.SwitchTab, k.Help, k.Back, k.Quit}
+ return []key.Binding{k.SwitchTab, k.Help, k.Language, k.Back, k.Quit}
}
func (k ContainerKeyMap) FullHelp() [][]key.Binding {
@@ -160,7 +169,7 @@ type ContainerLogsKeyMap struct {
}
func (k ContainerLogsKeyMap) ShortHelp() []key.Binding {
- return []key.Binding{k.Scroll, k.Refresh, k.Help, k.Back, k.Quit}
+ return []key.Binding{k.Scroll, k.Refresh, k.Help, k.Language, k.Back, k.Quit}
}
func (k ContainerLogsKeyMap) FullHelp() [][]key.Binding {
@@ -183,7 +192,7 @@ type NetworkKeyMap struct {
}
func (k NetworkKeyMap) ShortHelp() []key.Binding {
- return []key.Binding{k.SwitchTab, k.Navigate, k.Help, k.Back, k.Quit}
+ return []key.Binding{k.SwitchTab, k.Navigate, k.Help, k.Language, k.Back, k.Quit}
}
func (k NetworkKeyMap) FullHelp() [][]key.Binding {
@@ -212,7 +221,7 @@ type DiagnosticsKeyMap struct {
}
func (k DiagnosticsKeyMap) ShortHelp() []key.Binding {
- return []key.Binding{k.Navigate, k.Search, k.SwitchTab, k.Help, k.Back, k.Quit}
+ return []key.Binding{k.Navigate, k.Search, k.SwitchTab, k.Help, k.Language, k.Back, k.Quit}
}
func (k DiagnosticsKeyMap) FullHelp() [][]key.Binding {
@@ -256,7 +265,7 @@ type ContainerMenuKeyMap struct {
}
func (k ContainerMenuKeyMap) ShortHelp() []key.Binding {
- return []key.Binding{k.Navigate, k.Select, k.Help, k.Back, k.Quit}
+ return []key.Binding{k.Navigate, k.Select, k.Help, k.Language, k.Back, k.Quit}
}
func (k ContainerMenuKeyMap) FullHelp() [][]key.Binding {
@@ -280,7 +289,7 @@ type ReportingKeyMap struct {
}
func (k ReportingKeyMap) ShortHelp() []key.Binding {
- return []key.Binding{k.Help, k.Back, k.Quit}
+ return []key.Binding{k.Help, k.Language, k.Back, k.Quit}
}
func (k ReportingKeyMap) FullHelp() [][]key.Binding {
@@ -321,15 +330,19 @@ func (hs *HelpSystem) GetKeyMapForState(state model.AppState, diagnosticSelected
baseKeys := BaseKeyMap{
Help: key.NewBinding(
key.WithKeys("?"),
- key.WithHelp("?", "toggle help"),
+ key.WithHelp("?", i18n.T("toggle help")),
),
Quit: key.NewBinding(
key.WithKeys("q", "esc", "ctrl+c"),
- key.WithHelp("q", "quit"),
+ key.WithHelp("q", i18n.T("quit")),
),
Back: key.NewBinding(
key.WithKeys("b"),
- key.WithHelp("b", "back"),
+ key.WithHelp("b", i18n.T("back")),
+ ),
+ Language: key.NewBinding(
+ key.WithKeys("L", "ctrl+l"),
+ key.WithHelp("L", i18n.T("switch language")),
),
}
@@ -339,27 +352,27 @@ func (hs *HelpSystem) GetKeyMapForState(state model.AppState, diagnosticSelected
BaseKeyMap: baseKeys,
Select: key.NewBinding(
key.WithKeys("enter"),
- key.WithHelp("enter", "select"),
+ key.WithHelp("enter", i18n.T("select")),
),
Navigate: key.NewBinding(
key.WithKeys("tab", "left", "right"),
- key.WithHelp("tab/←→", "navigate"),
+ key.WithHelp("tab/←→", i18n.T("navigate")),
),
Quick1: key.NewBinding(
key.WithKeys("1"),
- key.WithHelp("1", "monitor"),
+ key.WithHelp("1", i18n.T("monitor")),
),
Quick2: key.NewBinding(
key.WithKeys("2"),
- key.WithHelp("2", "diagnostics"),
+ key.WithHelp("2", i18n.T("diagnostics")),
),
Quick3: key.NewBinding(
key.WithKeys("3"),
- key.WithHelp("3", "network"),
+ key.WithHelp("3", i18n.T("network")),
),
Quick4: key.NewBinding(
key.WithKeys("4"),
- key.WithHelp("4", "reporting"),
+ key.WithHelp("4", i18n.T("reporting")),
),
}
@@ -368,23 +381,23 @@ func (hs *HelpSystem) GetKeyMapForState(state model.AppState, diagnosticSelected
BaseKeyMap: baseKeys,
Select: key.NewBinding(
key.WithKeys("enter"),
- key.WithHelp("enter", "select"),
+ key.WithHelp("enter", i18n.T("select")),
),
Navigate: key.NewBinding(
key.WithKeys("tab", "left", "right"),
- key.WithHelp("tab/←→", "navigate"),
+ key.WithHelp("tab/←→", i18n.T("navigate")),
),
Quick1: key.NewBinding(
key.WithKeys("1"),
- key.WithHelp("1", "system"),
+ key.WithHelp("1", i18n.T("system")),
),
Quick2: key.NewBinding(
key.WithKeys("2"),
- key.WithHelp("2", "process"),
+ key.WithHelp("2", i18n.T("process")),
),
Quick3: key.NewBinding(
key.WithKeys("3"),
- key.WithHelp("3", "containers"),
+ key.WithHelp("3", i18n.T("containers")),
),
}
@@ -393,7 +406,7 @@ func (hs *HelpSystem) GetKeyMapForState(state model.AppState, diagnosticSelected
BaseKeyMap: baseKeys,
Scroll: key.NewBinding(
key.WithKeys("up", "down"),
- key.WithHelp("↑↓", "scroll"),
+ key.WithHelp("↑↓", i18n.T("scroll")),
),
}
@@ -402,23 +415,23 @@ func (hs *HelpSystem) GetKeyMapForState(state model.AppState, diagnosticSelected
BaseKeyMap: baseKeys,
Navigate: key.NewBinding(
key.WithKeys("up", "down"),
- key.WithHelp("↑↓", "navigate"),
+ key.WithHelp("↑↓", i18n.T("navigate")),
),
Search: key.NewBinding(
key.WithKeys("/"),
- key.WithHelp("/", "search"),
+ key.WithHelp("/", i18n.T("search")),
),
Kill: key.NewBinding(
key.WithKeys("k"),
- key.WithHelp("k", "kill"),
+ key.WithHelp("k", i18n.T("kill")),
),
SortMem: key.NewBinding(
key.WithKeys("m"),
- key.WithHelp("m", "sort by mem"),
+ key.WithHelp("m", i18n.T("sort by mem")),
),
SortCPU: key.NewBinding(
key.WithKeys("s"),
- key.WithHelp("s", "sort by cpu"),
+ key.WithHelp("s", i18n.T("sort by cpu")),
),
}
@@ -427,15 +440,15 @@ func (hs *HelpSystem) GetKeyMapForState(state model.AppState, diagnosticSelected
BaseKeyMap: baseKeys,
Navigate: key.NewBinding(
key.WithKeys("up", "down"),
- key.WithHelp("↑↓", "navigate"),
+ key.WithHelp("↑↓", i18n.T("navigate")),
),
Select: key.NewBinding(
key.WithKeys("enter"),
- key.WithHelp("enter", "menu"),
+ key.WithHelp("enter", i18n.T("menu")),
),
Search: key.NewBinding(
key.WithKeys("/"),
- key.WithHelp("/", "search"),
+ key.WithHelp("/", i18n.T("search")),
),
}
@@ -444,7 +457,7 @@ func (hs *HelpSystem) GetKeyMapForState(state model.AppState, diagnosticSelected
BaseKeyMap: baseKeys,
SwitchTab: key.NewBinding(
key.WithKeys("tab", "left", "right"),
- key.WithHelp("tab/←→", "switch tabs"),
+ key.WithHelp("tab/←→", i18n.T("switch tabs")),
),
}
@@ -453,19 +466,19 @@ func (hs *HelpSystem) GetKeyMapForState(state model.AppState, diagnosticSelected
BaseKeyMap: baseKeys,
Scroll: key.NewBinding(
key.WithKeys("up", "down"),
- key.WithHelp("↑↓", "scroll"),
+ key.WithHelp("↑↓", i18n.T("scroll")),
),
Refresh: key.NewBinding(
key.WithKeys("r"),
- key.WithHelp("r", "refresh"),
+ key.WithHelp("r", i18n.T("refresh")),
),
HomeEnd: key.NewBinding(
key.WithKeys("home", "end"),
- key.WithHelp("home/end", "navigate"),
+ key.WithHelp("home/end", i18n.T("navigate")),
),
Stream: key.NewBinding(
key.WithKeys("s"),
- key.WithHelp("s", "toggle stream"),
+ key.WithHelp("s", i18n.T("toggle stream")),
),
}
@@ -474,27 +487,27 @@ func (hs *HelpSystem) GetKeyMapForState(state model.AppState, diagnosticSelected
BaseKeyMap: baseKeys,
SwitchTab: key.NewBinding(
key.WithKeys("tab", "left", "right"),
- key.WithHelp("tab/←→", "switch tabs"),
+ key.WithHelp("tab/←→", i18n.T("switch tabs")),
),
Ping: key.NewBinding(
key.WithKeys("p"),
- key.WithHelp("p", "ping"),
+ key.WithHelp("p", i18n.T("ping")),
),
Trace: key.NewBinding(
key.WithKeys("t"),
- key.WithHelp("t", "trace route"),
+ key.WithHelp("t", i18n.T("trace route")),
),
Clear: key.NewBinding(
key.WithKeys("c"),
- key.WithHelp("c", "clear"),
+ key.WithHelp("c", i18n.T("clear")),
),
Switch: key.NewBinding(
key.WithKeys("space"),
- key.WithHelp("space", "switch view"),
+ key.WithHelp("space", i18n.T("switch view")),
),
Navigate: key.NewBinding(
key.WithKeys("up", "down"),
- key.WithHelp("↑↓", "navigate tables/pages"),
+ key.WithHelp("↑↓", i18n.T("navigate tables/pages")),
),
}
@@ -505,31 +518,31 @@ func (hs *HelpSystem) GetKeyMapForState(state model.AppState, diagnosticSelected
BaseKeyMap: baseKeys,
SwitchTab: key.NewBinding(
key.WithKeys("left", "right"),
- key.WithHelp("←→", "switch tabs"),
+ key.WithHelp("←→", i18n.T("switch tabs")),
),
Reload: key.NewBinding(
key.WithKeys("enter", "r"),
- key.WithHelp("enter/r", "reload"),
+ key.WithHelp("enter/r", i18n.T("reload")),
),
Search: key.NewBinding(
key.WithKeys("/"),
- key.WithHelp("/", "search"),
+ key.WithHelp("/", i18n.T("search")),
),
Navigate: key.NewBinding(
key.WithKeys("up", "down"),
- key.WithHelp("↑↓", "navigate"),
+ key.WithHelp("↑↓", i18n.T("navigate")),
),
Details: key.NewBinding(
key.WithKeys("enter"),
- key.WithHelp("enter", "details"),
+ key.WithHelp("enter", i18n.T("details")),
),
Security: key.NewBinding(
key.WithKeys("1"),
- key.WithHelp("1", "security"),
+ key.WithHelp("1", i18n.T("security")),
),
Performance: key.NewBinding(
key.WithKeys("2"),
- key.WithHelp("2", "performance"),
+ key.WithHelp("2", i18n.T("performance")),
),
Logs: key.NewBinding(
key.WithKeys("3"),
@@ -541,23 +554,23 @@ func (hs *HelpSystem) GetKeyMapForState(state model.AppState, diagnosticSelected
BaseKeyMap: baseKeys,
SwitchTab: key.NewBinding(
key.WithKeys("left", "right"),
- key.WithHelp("←→", "switch tabs"),
+ key.WithHelp("←→", i18n.T("switch tabs")),
),
Navigate: key.NewBinding(
key.WithKeys("up", "down"),
- key.WithHelp("↑↓", "navigate"),
+ key.WithHelp("↑↓", i18n.T("navigate")),
),
Details: key.NewBinding(
key.WithKeys("enter"),
- key.WithHelp("enter", "details"),
+ key.WithHelp("enter", i18n.T("details")),
),
Security: key.NewBinding(
key.WithKeys("1"),
- key.WithHelp("1", "security"),
+ key.WithHelp("1", i18n.T("security")),
),
Performance: key.NewBinding(
key.WithKeys("2"),
- key.WithHelp("2", "performance"),
+ key.WithHelp("2", i18n.T("performance")),
),
Logs: key.NewBinding(
key.WithKeys("3"),
@@ -569,47 +582,47 @@ func (hs *HelpSystem) GetKeyMapForState(state model.AppState, diagnosticSelected
BaseKeyMap: baseKeys,
TimeRange: key.NewBinding(
key.WithKeys("shift+left", "shift+right"),
- key.WithHelp("shift+←→", "time range"),
+ key.WithHelp("shift+←→", i18n.T("time range")),
),
Level: key.NewBinding(
key.WithKeys("shift+left", "shift+right"),
- key.WithHelp("shift+←→", "level"),
+ key.WithHelp("shift+←→", i18n.T("level")),
),
SwitchFilter: key.NewBinding(
key.WithKeys("space"),
- key.WithHelp("space", "switch filter"),
+ key.WithHelp("space", i18n.T("switch filter")),
),
SwitchTab: key.NewBinding(
key.WithKeys("left", "right"),
- key.WithHelp("←→", "switch tabs"),
+ key.WithHelp("←→", i18n.T("switch tabs")),
),
Reload: key.NewBinding(
key.WithKeys("enter", "r"),
- key.WithHelp("enter/r", "reload"),
+ key.WithHelp("enter/r", i18n.T("reload")),
),
Search: key.NewBinding(
key.WithKeys("/"),
- key.WithHelp("/", "search"),
+ key.WithHelp("/", i18n.T("search")),
),
Service: key.NewBinding(
key.WithKeys("s"),
- key.WithHelp("s", "service"),
+ key.WithHelp("s", i18n.T("service")),
),
Navigate: key.NewBinding(
key.WithKeys("up", "down"),
- key.WithHelp("↑↓", "navigate"),
+ key.WithHelp("↑↓", i18n.T("navigate")),
),
Details: key.NewBinding(
key.WithKeys("d"),
- key.WithHelp("d", "details"),
+ key.WithHelp("d", i18n.T("details")),
),
Security: key.NewBinding(
key.WithKeys("1"),
- key.WithHelp("1", "security"),
+ key.WithHelp("1", i18n.T("security")),
),
Performance: key.NewBinding(
key.WithKeys("2"),
- key.WithHelp("2", "performance"),
+ key.WithHelp("2", i18n.T("performance")),
),
Logs: key.NewBinding(
key.WithKeys("3"),
@@ -621,23 +634,23 @@ func (hs *HelpSystem) GetKeyMapForState(state model.AppState, diagnosticSelected
BaseKeyMap: baseKeys,
SwitchTab: key.NewBinding(
key.WithKeys("left", "right"),
- key.WithHelp("←→", "switch tabs"),
+ key.WithHelp("←→", i18n.T("switch tabs")),
),
Navigate: key.NewBinding(
key.WithKeys("up", "down"),
- key.WithHelp("↑↓", "navigate"),
+ key.WithHelp("↑↓", i18n.T("navigate")),
),
Details: key.NewBinding(
key.WithKeys("enter"),
- key.WithHelp("enter", "details"),
+ key.WithHelp("enter", i18n.T("details")),
),
Security: key.NewBinding(
key.WithKeys("1"),
- key.WithHelp("1", "security"),
+ key.WithHelp("1", i18n.T("security")),
),
Performance: key.NewBinding(
key.WithKeys("2"),
- key.WithHelp("2", "performance"),
+ key.WithHelp("2", i18n.T("performance")),
),
Logs: key.NewBinding(
key.WithKeys("3"),
@@ -651,23 +664,23 @@ func (hs *HelpSystem) GetKeyMapForState(state model.AppState, diagnosticSelected
BaseKeyMap: baseKeys,
Generate: key.NewBinding(
key.WithKeys("g"),
- key.WithHelp("g", "generate report"),
+ key.WithHelp("g", i18n.T("generate report")),
),
Save: key.NewBinding(
key.WithKeys("s"),
- key.WithHelp("s", "save report"),
+ key.WithHelp("s", i18n.T("save report")),
),
Load: key.NewBinding(
key.WithKeys("l"),
- key.WithHelp("l", "load reports"),
+ key.WithHelp("l", i18n.T("load reports")),
),
Navigate: key.NewBinding(
key.WithKeys("up", "down", "k", "j"),
- key.WithHelp("↑↓", "navigate"),
+ key.WithHelp("↑↓", i18n.T("navigate")),
),
Select: key.NewBinding(
key.WithKeys("enter"),
- key.WithHelp("enter", "select"),
+ key.WithHelp("enter", i18n.T("select")),
),
Delete: key.NewBinding(
key.WithKeys("d"),
@@ -722,15 +735,15 @@ func (hs *HelpSystem) GetKeyMapForStateWithModel(state model.AppState, diagnosti
baseKeys := BaseKeyMap{
Help: key.NewBinding(
key.WithKeys("?"),
- key.WithHelp("?", "toggle help"),
+ key.WithHelp("?", i18n.T("toggle help")),
),
Quit: key.NewBinding(
key.WithKeys("q", "ctrl+c"),
- key.WithHelp("q", "quit"),
+ key.WithHelp("q", i18n.T("quit")),
),
Back: key.NewBinding(
key.WithKeys("b", "esc"),
- key.WithHelp("b/esc", "back"),
+ key.WithHelp("b/esc", i18n.T("back")),
),
}
@@ -738,7 +751,7 @@ func (hs *HelpSystem) GetKeyMapForStateWithModel(state model.AppState, diagnosti
BaseKeyMap: baseKeys,
SwitchTab: key.NewBinding(
key.WithKeys("left", "right", "h", "l"),
- key.WithHelp("←→", "switch CPU sub-tabs"),
+ key.WithHelp("←→", i18n.T("switch CPU sub-tabs")),
),
Navigate: key.NewBinding(
key.WithKeys(""),
@@ -759,15 +772,15 @@ func (hs *HelpSystem) GetKeyMapForStateWithModel(state model.AppState, diagnosti
baseKeys := BaseKeyMap{
Help: key.NewBinding(
key.WithKeys("?"),
- key.WithHelp("?", "toggle help"),
+ key.WithHelp("?", i18n.T("toggle help")),
),
Quit: key.NewBinding(
key.WithKeys("q", "ctrl+c"),
- key.WithHelp("q", "quit"),
+ key.WithHelp("q", i18n.T("quit")),
),
Back: key.NewBinding(
key.WithKeys("b", "esc"),
- key.WithHelp("b/esc", "back"),
+ key.WithHelp("b/esc", i18n.T("back")),
),
}
@@ -775,7 +788,7 @@ func (hs *HelpSystem) GetKeyMapForStateWithModel(state model.AppState, diagnosti
BaseKeyMap: baseKeys,
SwitchTab: key.NewBinding(
key.WithKeys("left", "right", "h", "l"),
- key.WithHelp("←→", "switch Memory sub-tabs"),
+ key.WithHelp("←→", i18n.T("switch Memory sub-tabs")),
),
Navigate: key.NewBinding(
key.WithKeys(""),
@@ -797,32 +810,32 @@ func (hs *HelpSystem) GetKeyMapForStateWithModel(state model.AppState, diagnosti
baseKeys := BaseKeyMap{
Help: key.NewBinding(
key.WithKeys("?"),
- key.WithHelp("?", "toggle help"),
+ key.WithHelp("?", i18n.T("toggle help")),
),
Quit: key.NewBinding(
key.WithKeys("q", "ctrl+c"),
- key.WithHelp("q", "quit"),
+ key.WithHelp("q", i18n.T("quit")),
),
Back: key.NewBinding(
key.WithKeys("b", "esc"),
- key.WithHelp("b/esc", "back"),
+ key.WithHelp("b/esc", i18n.T("back")),
),
}
- enterHelp := "activate tab"
+ enterHelp := i18n.T("activate tab")
// Show special help when on CPU or Memory tab
switch m.Diagnostic.Performance.SelectedItem {
case model.CPU:
- enterHelp = "enter CPU sub-tabs"
+ enterHelp = i18n.T("enter CPU sub-tabs")
case model.Memory:
- enterHelp = "enter Memory sub-tabs"
+ enterHelp = i18n.T("enter Memory sub-tabs")
}
return DiagnosticsKeyMap{
BaseKeyMap: baseKeys,
SwitchTab: key.NewBinding(
key.WithKeys("left", "right", "h", "l"),
- key.WithHelp("←→", "switch performance tabs"),
+ key.WithHelp("←→", i18n.T("switch performance tabs")),
),
Navigate: key.NewBinding(
key.WithKeys(""),
@@ -834,7 +847,7 @@ func (hs *HelpSystem) GetKeyMapForStateWithModel(state model.AppState, diagnosti
),
Reload: key.NewBinding(
key.WithKeys("r"),
- key.WithHelp("r", "reload data"),
+ key.WithHelp("r", i18n.T("reload data")),
),
}
}
diff --git a/widgets/init.go b/widgets/init.go
index 6ba9145..a6cca42 100644
--- a/widgets/init.go
+++ b/widgets/init.go
@@ -2,6 +2,7 @@ package widgets
import (
"fmt"
+ "github.com/Server-Pulse/server-pulse/i18n"
"time"
"github.com/Server-Pulse/server-pulse/system/app"
@@ -39,16 +40,16 @@ func InitialModelWithManager(apk *app.DockerManager) Model {
var err error
containers, err = apk.RefreshContainers()
if err != nil {
- fmt.Printf("Erreur lors du chargement des conteneurs: %v\n", err)
+ fmt.Printf(i18n.T("Erreur lors du chargement des conteneurs: %v\n"), err)
}
}
columns := []table.Column{
{Title: "PID", Width: 8},
- {Title: "User", Width: 12},
+ {Title: i18n.T("User"), Width: 12},
{Title: "CPU%", Width: 8},
- {Title: "Mem%", Width: 8},
- {Title: "Command", Width: 30},
+ {Title: i18n.T("Mem%"), Width: 8},
+ {Title: i18n.T("Command"), Width: 30},
}
t := table.New(
table.WithColumns(columns),
@@ -61,12 +62,12 @@ func InitialModelWithManager(apk *app.DockerManager) Model {
// containers table
ctColumns := []table.Column{
{Title: "ID", Width: 12},
- {Title: "Image", Width: 12},
- {Title: "Name", Width: 16},
- {Title: "Status", Width: 12},
- {Title: "Health", Width: 12},
- {Title: "Project", Width: 20},
- {Title: "Ports", Width: 20},
+ {Title: i18n.T("Image"), Width: 12},
+ {Title: i18n.T("Name"), Width: 16},
+ {Title: i18n.T("Status"), Width: 12},
+ {Title: i18n.T("Health"), Width: 12},
+ {Title: i18n.T("Project"), Width: 20},
+ {Title: i18n.T("Ports"), Width: 20},
}
ct := table.New(
table.WithColumns(ctColumns),
@@ -79,11 +80,11 @@ func InitialModelWithManager(apk *app.DockerManager) Model {
// Network table
networkColumns := []table.Column{
- {Title: "Interface", Width: 12},
- {Title: "Status", Width: 8},
- {Title: "IP Addresses", Width: 25},
- {Title: "RX", Width: 12},
- {Title: "TX", Width: 12},
+ {Title: i18n.T("Interface"), Width: 12},
+ {Title: i18n.T("Status"), Width: 8},
+ {Title: i18n.T("IP Addresses"), Width: 25},
+ {Title: i18n.T("RX"), Width: 12},
+ {Title: i18n.T("TX"), Width: 12},
}
networkTable := table.New(
table.WithColumns(networkColumns),
@@ -100,13 +101,13 @@ func InitialModelWithManager(apk *app.DockerManager) Model {
networkTable.SetStyles(networkStyle)
connectionsColumns := []table.Column{
- {Title: "Proto", Width: 6},
- {Title: "Recv-Q", Width: 8},
- {Title: "Send-Q", Width: 8},
- {Title: "Local Address", Width: 22},
- {Title: "Foreign Address", Width: 20},
- {Title: "State", Width: 12},
- {Title: "PID/Program", Width: 18},
+ {Title: i18n.T("Proto"), Width: 6},
+ {Title: i18n.T("Recv-Q"), Width: 8},
+ {Title: i18n.T("Send-Q"), Width: 8},
+ {Title: i18n.T("Local Address"), Width: 22},
+ {Title: i18n.T("Foreign Address"), Width: 20},
+ {Title: i18n.T("State"), Width: 12},
+ {Title: i18n.T("PID/Program"), Width: 18},
}
connectionsTable := table.New(
table.WithColumns(connectionsColumns),
@@ -116,11 +117,11 @@ func InitialModelWithManager(apk *app.DockerManager) Model {
connectionsTable.SetStyles(networkStyle)
routesColumns := []table.Column{
- {Title: "Destination", Width: 18},
- {Title: "Gateway", Width: 12},
- {Title: "Genmask", Width: 16},
- {Title: "Flags", Width: 8},
- {Title: "Iface", Width: 16},
+ {Title: i18n.T("Destination"), Width: 18},
+ {Title: i18n.T("Gateway"), Width: 12},
+ {Title: i18n.T("Genmask"), Width: 16},
+ {Title: i18n.T("Flags"), Width: 8},
+ {Title: i18n.T("Iface"), Width: 16},
}
routesTable := table.New(
table.WithColumns(routesColumns),
@@ -140,9 +141,9 @@ func InitialModelWithManager(apk *app.DockerManager) Model {
dnsTable.SetStyles(networkStyle)
diagnosticColumns := []table.Column{
- {Title: "Security Check", Width: 30},
- {Title: "Performances", Width: 12},
- {Title: "Logs", Width: 40},
+ {Title: i18n.T("Security Check"), Width: 30},
+ {Title: i18n.T("Performances"), Width: 12},
+ {Title: i18n.T("Logs"), Width: 40},
}
diagnosticTable := table.New(
@@ -152,13 +153,13 @@ func InitialModelWithManager(apk *app.DockerManager) Model {
diagnosticTable.SetStyles(networkStyle)
searchInput := textinput.New()
- searchInput.Placeholder = "Search a process..."
+ searchInput.Placeholder = i18n.T("Search a process...")
searchInput.Prompt = "/"
searchInput.CharLimit = 50
searchInput.Width = 30
passwordInput := textinput.New()
- passwordInput.Placeholder = "Enter root password..."
+ passwordInput.Placeholder = i18n.T("Enter root password...")
passwordInput.EchoMode = textinput.EchoPassword
passwordInput.EchoCharacter = '•'
passwordInput.CharLimit = 50
@@ -181,9 +182,9 @@ func InitialModelWithManager(apk *app.DockerManager) Model {
securityManager.SudoPassword = "" // No password initially
securityColumns := []table.Column{
- {Title: "Name", Width: 20},
- {Title: "Status", Width: 15},
- {Title: "Details", Width: 40},
+ {Title: i18n.T("Name"), Width: 20},
+ {Title: i18n.T("Status"), Width: 15},
+ {Title: i18n.T("Details"), Width: 40},
}
securityTable := table.New(
table.WithColumns(securityColumns),
@@ -202,9 +203,9 @@ func InitialModelWithManager(apk *app.DockerManager) Model {
securityTable.SetStyles(tableStyle)
portsColumns := []table.Column{
- {Title: "Port", Width: 10},
- {Title: "Service", Width: 20},
- {Title: "Protocol", Width: 10},
+ {Title: i18n.T("Port"), Width: 10},
+ {Title: i18n.T("Service"), Width: 20},
+ {Title: i18n.T("Protocol"), Width: 10},
{Title: "PID", Width: 10},
}
@@ -217,7 +218,7 @@ func InitialModelWithManager(apk *app.DockerManager) Model {
spinnerModel := getRandomSpinner()
firewallColumns := []table.Column{
- {Title: "Firewall Rule", Width: 100},
+ {Title: i18n.T("Firewall Rule"), Width: 100},
}
firewallTable := table.New(
@@ -228,7 +229,7 @@ func InitialModelWithManager(apk *app.DockerManager) Model {
firewallTable.SetStyles(tableStyle)
autoBanColumns := []table.Column{
- {Title: "Jail/Service Details", Width: 100},
+ {Title: i18n.T("Jail/Service Details"), Width: 100},
}
autoBanTable := table.New(
@@ -240,10 +241,10 @@ func InitialModelWithManager(apk *app.DockerManager) Model {
// Logs table
logsColumns := []table.Column{
- {Title: "Time", Width: 20},
- {Title: "Level", Width: 8},
- {Title: "Service", Width: 20},
- {Title: "Message", Width: 70},
+ {Title: i18n.T("Time"), Width: 20},
+ {Title: i18n.T("Level"), Width: 8},
+ {Title: i18n.T("Service"), Width: 20},
+ {Title: i18n.T("Message"), Width: 70},
}
logsTable := table.New(
@@ -256,17 +257,17 @@ func InitialModelWithManager(apk *app.DockerManager) Model {
// Log filter inputs
logSearchInput := textinput.New()
- logSearchInput.Placeholder = "Search logs..."
+ logSearchInput.Placeholder = i18n.T("Search logs...")
logSearchInput.CharLimit = 100
logSearchInput.Width = 30
logServiceInput := textinput.New()
- logServiceInput.Placeholder = "Filter by service..."
+ logServiceInput.Placeholder = i18n.T("Filter by service...")
logServiceInput.CharLimit = 50
logServiceInput.Width = 25
logTimeRangeInput := textinput.New()
- logTimeRangeInput.Placeholder = "e.g., '2 hours ago', '2025-01-08', '3 days ago'"
+ logTimeRangeInput.Placeholder = i18n.T("e.g., '2 hours ago', '2025-01-08', '3 days ago'")
logTimeRangeInput.CharLimit = 50
logTimeRangeInput.Width = 50
@@ -290,18 +291,18 @@ func InitialModelWithManager(apk *app.DockerManager) Model {
ConnectionsTable: connectionsTable,
RoutesTable: routesTable,
DNSTable: dnsTable,
- Nav: v.NetworkNav,
+ Nav: v.LocalizedNetworkNav(),
SelectedItem: model.NetworkTabInterface,
PingInput: func() textinput.Model {
ti := textinput.New()
- ti.Placeholder = "Enter host or IP to ping (e.g., 8.8.8.8)"
+ ti.Placeholder = i18n.T("Enter host or IP to ping (e.g., 8.8.8.8)")
ti.CharLimit = 100
ti.Width = 40
return ti
}(),
TracerouteInput: func() textinput.Model {
ti := textinput.New()
- ti.Placeholder = "Enter host or IP to trace (e.g., google.com)"
+ ti.Placeholder = i18n.T("Enter host or IP to trace (e.g., google.com)")
ti.CharLimit = 100
ti.Width = 40
return ti
@@ -315,11 +316,11 @@ func InitialModelWithManager(apk *app.DockerManager) Model {
},
Diagnostic: model.DiagnosticModel{
DiagnosticTable: diagnosticTable,
- Nav: v.DiagnosticNav,
+ Nav: v.LocalizedDiagnosticNav(),
SelectedItem: model.DiagnosticSecurityChecks,
Performance: model.PerformanceModel{
SelectedItem: model.SystemHealth,
- Nav: []string{"System Health", "I/O", "CPU", "Memory"},
+ Nav: v.LocalizedPerformanceNav(),
SubTabNavigationActive: false,
CPUSelectedTab: model.CPUTabStateBreakdown,
CPUSubTabActive: false,
@@ -343,7 +344,7 @@ func InitialModelWithManager(apk *app.DockerManager) Model {
CustomTimeInputMode: false,
DomainInput: func() textinput.Model {
ti := textinput.New()
- ti.Placeholder = "Enter domain name (e.g., google.com)"
+ ti.Placeholder = i18n.T("Enter domain name (e.g., google.com)")
ti.CharLimit = 100
ti.Width = 40
return ti
@@ -368,9 +369,9 @@ func InitialModelWithManager(apk *app.DockerManager) Model {
App: apk,
ContainerMenuState: v.ContainerMenuHidden,
SelectedContainer: nil,
- ContainerMenuItems: v.ContainerMenuItems,
+ ContainerMenuItems: v.LocalizedContainerMenuItems(),
ContainerViewState: v.ContainerViewNone,
- ContainerTabs: v.ContainerTabs,
+ ContainerTabs: v.LocalizedContainerTabs(),
ContainerLogsPagination: model.ContainerLogsPagination{
PageSize: 100,
CurrentPage: 1,
@@ -405,7 +406,7 @@ func InitialModelWithManager(apk *app.DockerManager) Model {
},
Ui: model.UIModel{
State: model.StateHome,
- Tabs: v.Menu,
+ Tabs: v.LocalizedMenu(),
SelectedTab: 0,
ActiveView: -1,
SearchInput: searchInput,
diff --git a/widgets/locale.go b/widgets/locale.go
new file mode 100644
index 0000000..ff6fbe3
--- /dev/null
+++ b/widgets/locale.go
@@ -0,0 +1,88 @@
+package widgets
+
+import (
+ "github.com/Server-Pulse/server-pulse/i18n"
+ v "github.com/Server-Pulse/server-pulse/widgets/vars"
+ "github.com/charmbracelet/bubbles/table"
+)
+
+// applyLocale refreshes all in-memory UI labels after a language switch.
+func (m *Model) applyLocale() {
+ m.Ui.Tabs = v.LocalizedMenu()
+ m.Network.Nav = v.LocalizedNetworkNav()
+ m.Diagnostic.Nav = v.LocalizedDiagnosticNav()
+ m.Diagnostic.Performance.Nav = v.LocalizedPerformanceNav()
+ m.Monitor.ContainerTabs = v.LocalizedContainerTabs()
+ m.Monitor.ContainerMenuItems = v.LocalizedContainerMenuItems()
+
+ m.Monitor.ProcessTable.SetColumns([]table.Column{
+ {Title: "PID", Width: 8},
+ {Title: i18n.T("User"), Width: 12},
+ {Title: "CPU%", Width: 8},
+ {Title: i18n.T("Mem%"), Width: 8},
+ {Title: i18n.T("Command"), Width: 30},
+ })
+ m.Monitor.Container.SetColumns([]table.Column{
+ {Title: "ID", Width: 12},
+ {Title: i18n.T("Image"), Width: 12},
+ {Title: i18n.T("Name"), Width: 16},
+ {Title: i18n.T("Status"), Width: 12},
+ {Title: i18n.T("Health"), Width: 12},
+ {Title: i18n.T("Project"), Width: 20},
+ {Title: i18n.T("Ports"), Width: 20},
+ })
+ m.Network.NetworkTable.SetColumns([]table.Column{
+ {Title: i18n.T("Interface"), Width: 12},
+ {Title: i18n.T("Status"), Width: 8},
+ {Title: i18n.T("IP Addresses"), Width: 25},
+ {Title: i18n.T("RX"), Width: 12},
+ {Title: i18n.T("TX"), Width: 12},
+ })
+ m.Network.ConnectionsTable.SetColumns([]table.Column{
+ {Title: i18n.T("Proto"), Width: 6},
+ {Title: i18n.T("Recv-Q"), Width: 8},
+ {Title: i18n.T("Send-Q"), Width: 8},
+ {Title: i18n.T("Local Address"), Width: 22},
+ {Title: i18n.T("Foreign Address"), Width: 20},
+ {Title: i18n.T("State"), Width: 12},
+ {Title: i18n.T("PID/Program"), Width: 18},
+ })
+ m.Network.RoutesTable.SetColumns([]table.Column{
+ {Title: i18n.T("Destination"), Width: 18},
+ {Title: i18n.T("Gateway"), Width: 12},
+ {Title: i18n.T("Genmask"), Width: 16},
+ {Title: i18n.T("Flags"), Width: 8},
+ {Title: i18n.T("Iface"), Width: 16},
+ })
+ m.Diagnostic.SecurityTable.SetColumns([]table.Column{
+ {Title: i18n.T("Name"), Width: 20},
+ {Title: i18n.T("Status"), Width: 15},
+ {Title: i18n.T("Details"), Width: 40},
+ })
+ m.Diagnostic.PortsTable.SetColumns([]table.Column{
+ {Title: i18n.T("Port"), Width: 10},
+ {Title: i18n.T("Service"), Width: 20},
+ {Title: i18n.T("Protocol"), Width: 10},
+ {Title: "PID", Width: 10},
+ })
+ m.Diagnostic.FirewallTable.SetColumns([]table.Column{
+ {Title: i18n.T("Firewall Rule"), Width: 100},
+ })
+ m.Diagnostic.AutoBanTable.SetColumns([]table.Column{
+ {Title: i18n.T("Jail/Service Details"), Width: 100},
+ })
+ m.Diagnostic.LogsTable.SetColumns([]table.Column{
+ {Title: i18n.T("Time"), Width: 20},
+ {Title: i18n.T("Level"), Width: 8},
+ {Title: i18n.T("Service"), Width: 20},
+ {Title: i18n.T("Message"), Width: 70},
+ })
+
+ m.Ui.SearchInput.Placeholder = i18n.T("Search a process...")
+ m.Diagnostic.Password.Placeholder = i18n.T("Enter root password...")
+ m.Diagnostic.LogSearchInput.Placeholder = i18n.T("Search logs...")
+ m.Diagnostic.LogServiceInput.Placeholder = i18n.T("Filter by service...")
+ m.Network.PingInput.Placeholder = i18n.T("Enter host or IP to ping (e.g., 8.8.8.8)")
+ m.Network.TracerouteInput.Placeholder = i18n.T("Enter host or IP to trace (e.g., google.com)")
+ m.Diagnostic.DomainInput.Placeholder = i18n.T("Enter domain name (e.g., google.com)")
+}
diff --git a/widgets/model/performance.go b/widgets/model/performance.go
index 90d8df3..47ebd85 100644
--- a/widgets/model/performance.go
+++ b/widgets/model/performance.go
@@ -1,6 +1,9 @@
package model
-import "time"
+import (
+ "github.com/Server-Pulse/server-pulse/i18n"
+ "time"
+)
// HealthMetrics holds the metrics for the system health check.
type HealthMetrics struct {
@@ -31,7 +34,7 @@ const (
)
func (pt PerformanceTab) String() string {
- return []string{"System Health", "I/O", "CPU", "Memory"}[pt]
+ return []string{i18n.T("System Health"), "I/O", "CPU", i18n.T("Memory")}[pt]
}
type CPUTab int
@@ -43,7 +46,7 @@ const (
)
func (ct CPUTab) String() string {
- return []string{"CPU State Breakdown", "Per-Core Performance", "System Activity Metrics"}[ct]
+ return []string{i18n.T("CPU State Breakdown"), i18n.T("Per-Core Performance"), i18n.T("System Activity Metrics")}[ct]
}
type MemoryTab int
@@ -56,7 +59,7 @@ const (
)
func (mt MemoryTab) String() string {
- return []string{"Overview", "Usage Breakdown", "Swap Analysis", "System Memory"}[mt]
+ return []string{i18n.T("Overview"), i18n.T("Usage Breakdown"), i18n.T("Swap Analysis"), i18n.T("System Memory")}[mt]
}
type PerformanceModel struct {
diff --git a/widgets/model/reporting.go b/widgets/model/reporting.go
index 8f6fa57..fdf51e2 100644
--- a/widgets/model/reporting.go
+++ b/widgets/model/reporting.go
@@ -2,6 +2,7 @@ package model
import (
"fmt"
+ "github.com/Server-Pulse/server-pulse/i18n"
"os"
"path/filepath"
"sort"
@@ -426,19 +427,19 @@ func (rm *ReportModel) getSecurityRecommendations(securityChecks []security.Secu
for _, check := range securityChecks {
if strings.Contains(check.Status, "Warning") {
switch check.Name {
- case "SSH Password Authentication":
- recommendations = append(recommendations, "SSH password authentication is enabled. Consider using SSH keys only for better security.")
- case "System Updates":
- recommendations = append(recommendations, "System updates are available. Run 'sudo apt update && sudo apt upgrade' to apply security patches.")
- case "System Restart Required":
- recommendations = append(recommendations, "System restart required for kernel updates. Schedule a maintenance window to reboot the system.")
+ case i18n.T("SSH Password Authentication"):
+ recommendations = append(recommendations, i18n.T("SSH password authentication is enabled. Consider using SSH keys only for better security."))
+ case i18n.T("System Updates"):
+ recommendations = append(recommendations, i18n.T("System updates are available. Run 'sudo apt update && sudo apt upgrade' to apply security patches."))
+ case i18n.T("System Restart"):
+ recommendations = append(recommendations, i18n.T("System restart required for kernel updates. Schedule a maintenance window to reboot the system."))
}
} else if strings.Contains(check.Status, "Failed") {
switch check.Name {
- case "Firewall Status":
- recommendations = append(recommendations, "Firewall is not active. Enable it with 'sudo ufw enable'.")
- case "Open Ports":
- recommendations = append(recommendations, "Risky ports are open. Close unnecessary ports to reduce attack surface.")
+ case i18n.T("Firewall Status"):
+ recommendations = append(recommendations, i18n.T("Firewall is not active. Enable it with 'sudo ufw enable'."))
+ case i18n.T("Open Ports"):
+ recommendations = append(recommendations, i18n.T("Risky ports are open. Close unnecessary ports to reduce attack surface."))
}
}
}
@@ -454,7 +455,7 @@ func (rm *ReportModel) getPerformanceRecommendations(m MonitorModel) []string {
if len(m.Processes) > 0 {
topProcess := m.Processes[0]
recommendations = append(recommendations,
- fmt.Sprintf("Memory usage very high (%.1f%%). Process '%s' is consuming %.1f%% memory. Consider optimizing or allocating more resources.",
+ fmt.Sprintf(i18n.T("Memory usage very high (%.1f%%). Process '%s' is consuming %.1f%% memory. Consider optimizing or allocating more resources."),
m.Memory.Usage, topProcess.Command, topProcess.Mem))
} else {
recommendations = append(recommendations,
@@ -495,7 +496,7 @@ func (rm *ReportModel) getDockerRecommendations(m MonitorModel) []string {
for _, container := range containers {
if container.Health == "unhealthy" {
recommendations = append(recommendations,
- fmt.Sprintf("Container '%s' is unhealthy. Check its logs with 'docker logs %s'.",
+ fmt.Sprintf(i18n.T("Container '%s' is unhealthy. Check its logs with 'docker logs %s'."),
container.Name, container.ID[:12]))
}
}
@@ -511,7 +512,7 @@ func (rm *ReportModel) SaveReport() (string, error) {
filename := fmt.Sprintf("report_%s_%s.md",
time.Now().Format("2006-01-02_150405"),
- "system") // In practice, you might want to get the actual hostname
+ i18n.T("system")) // In practice, you might want to get the actual hostname
filepath := filepath.Join(rm.ReportDirectory, filename)
err := os.WriteFile(filepath, []byte(rm.CurrentReport), 0644)
@@ -569,9 +570,9 @@ func formatUptime(uptime uint64) string {
if days > 0 {
return fmt.Sprintf("%d days, %d hours, %d minutes", days, hours, minutes)
} else if hours > 0 {
- return fmt.Sprintf("%d hours, %d minutes", hours, minutes)
+ return fmt.Sprintf(i18n.T("%d hours, %d minutes"), hours, minutes)
} else {
- return fmt.Sprintf("%d minutes", minutes)
+ return fmt.Sprintf(i18n.T("%d minutes"), minutes)
}
}
diff --git a/widgets/render-body.go b/widgets/render-body.go
index ad8fd75..8265f00 100644
--- a/widgets/render-body.go
+++ b/widgets/render-body.go
@@ -2,6 +2,7 @@ package widgets
import (
"fmt"
+ "github.com/Server-Pulse/server-pulse/i18n"
"github.com/Server-Pulse/server-pulse/widgets/model"
)
@@ -45,7 +46,7 @@ func (m Model) renderMainContent() string {
case model.StateSystemHealth, model.StateInputOutput, model.StateCPU, model.StateMemory, model.StateQuickTests:
currentView = m.renderPerformanceAnalysis()
default:
- currentView = fmt.Sprintf("Unknown state: %v", m.Ui.State)
+ currentView = fmt.Sprintf(i18n.T("Unknown state: %v"), m.Ui.State)
}
m.Ui.Viewport.SetContent(currentView)
m.Ui.Viewport.SetYOffset(m.Ui.Viewport.YOffset)
diff --git a/widgets/render-charts.go b/widgets/render-charts.go
index 35e3f10..dc08b3e 100644
--- a/widgets/render-charts.go
+++ b/widgets/render-charts.go
@@ -2,6 +2,7 @@ package widgets
import (
"fmt"
+ "github.com/Server-Pulse/server-pulse/i18n"
"math"
model "github.com/Server-Pulse/server-pulse/widgets/model"
@@ -82,23 +83,23 @@ func (m Model) renderNetworkRXChart(width, height int) string {
// Get container-specific history if available
var points []float64
var maxValue float64
- caption := "Network RX"
+ caption := i18n.T("Network RX")
if m.Monitor.SelectedContainer != nil {
if containerHistory, exists := m.Monitor.ContainerHistories[m.Monitor.SelectedContainer.ID]; exists {
points = extractValues(containerHistory.NetworkRxHistory.Points)
- points, maxValue, caption = getOptimalNetworkScale(points, "RX")
+ points, maxValue, caption = getOptimalNetworkScale(points, i18n.T("RX"))
}
}
// Fallback to global history if no container-specific data
if len(points) < 1 {
points = extractValues(m.Monitor.NetworkRxHistory.Points)
- points, maxValue, caption = getOptimalNetworkScale(points, "RX")
+ points, maxValue, caption = getOptimalNetworkScale(points, i18n.T("RX"))
}
if len(points) < 1 {
- return renderEmptyChart("Network RX", height)
+ return renderEmptyChart(i18n.T("Network RX"), height)
}
// Ensure minimum scale for visibility
@@ -123,23 +124,23 @@ func (m Model) renderNetworkTXChart(width, height int) string {
// Get container-specific history if available
var points []float64
var maxValue float64
- caption := "Network TX"
+ caption := i18n.T("Network TX")
if m.Monitor.SelectedContainer != nil {
if containerHistory, exists := m.Monitor.ContainerHistories[m.Monitor.SelectedContainer.ID]; exists {
points = extractValues(containerHistory.NetworkTxHistory.Points)
- points, maxValue, caption = getOptimalNetworkScale(points, "TX")
+ points, maxValue, caption = getOptimalNetworkScale(points, i18n.T("TX"))
}
}
// Fallback to global history if no container-specific data
if len(points) < 1 {
points = extractValues(m.Monitor.NetworkTxHistory.Points)
- points, maxValue, caption = getOptimalNetworkScale(points, "TX")
+ points, maxValue, caption = getOptimalNetworkScale(points, i18n.T("TX"))
}
if len(points) < 1 {
- return renderEmptyChart("Network TX", height)
+ return renderEmptyChart(i18n.T("Network TX"), height)
}
// Ensure minimum scale for visibility
@@ -175,18 +176,18 @@ func renderEmptyChart(title string, height int) string {
builder += "\n"
}
- builder += "Collecting data..."
+ builder += i18n.T("Collecting data...")
return builder
}
// getOptimalNetworkScale determines the best scale and unit for network data
func getOptimalNetworkScale(points []float64, direction string) ([]float64, float64, string) {
if len(points) == 0 {
- return points, 10.0, "Network " + direction + " (MB/s)"
+ return points, 10.0, i18n.T("Network ") + direction + " (MB/s)"
}
maxValue := getMaxValueFromFloat64(points)
- caption := "Network " + direction
+ caption := i18n.T("Network ") + direction
// Determine the best unit and scale based on the maximum value
if maxValue < 0.001 { // Less than 1 KB/s in MB units
@@ -234,11 +235,11 @@ func getMaxValueFromFloat64(values []float64) float64 {
// getOptimalCPUScale determines the best scale for CPU usage data
func getOptimalCPUScale(points []float64) ([]float64, float64, string) {
if len(points) == 0 {
- return points, 10.0, "CPU Usage (%)"
+ return points, 10.0, i18n.T("CPU Usage (%)")
}
maxValue := getMaxValueFromFloat64(points)
- caption := "CPU Usage (%)"
+ caption := i18n.T("CPU Usage (%)")
// Dynamic scaling for low CPU usage
if maxValue < 1.0 { // Less than 1%
@@ -273,11 +274,11 @@ func getOptimalCPUScale(points []float64) ([]float64, float64, string) {
// getOptimalMemoryScale determines the best scale for memory usage data
func getOptimalMemoryScale(points []float64) ([]float64, float64, string) {
if len(points) == 0 {
- return points, 10.0, "Memory Usage (%)"
+ return points, 10.0, i18n.T("Memory Usage (%)")
}
maxValue := getMaxValueFromFloat64(points)
- caption := "Memory Usage (%)"
+ caption := i18n.T("Memory Usage (%)")
// Dynamic scaling for low memory usage
if maxValue < 1.0 { // Less than 1%
diff --git a/widgets/render-common.go b/widgets/render-common.go
index 56a4c37..2c2c9bf 100644
--- a/widgets/render-common.go
+++ b/widgets/render-common.go
@@ -2,6 +2,7 @@ package widgets
import (
"fmt"
+ "github.com/Server-Pulse/server-pulse/i18n"
v "github.com/Server-Pulse/server-pulse/widgets/vars"
"strings"
@@ -25,9 +26,9 @@ func (m Model) renderConfirmationDialog() string {
doc.WriteString(v.MetricLabelStyle.Render(m.ConfirmationMessage))
doc.WriteString("\n\n")
- doc.WriteString(lipgloss.NewStyle().Bold(true).Render("Are you sure?"))
+ doc.WriteString(lipgloss.NewStyle().Bold(true).Render(i18n.T("Are you sure?")))
doc.WriteString("\n")
- doc.WriteString("Press 'y' to confirm or 'n' to cancel")
+ doc.WriteString(i18n.T("Press 'y' to confirm or 'n' to cancel"))
confirmationStyle := lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
diff --git a/widgets/render-container.go b/widgets/render-container.go
index 7ed5229..667e0e3 100644
--- a/widgets/render-container.go
+++ b/widgets/render-container.go
@@ -2,6 +2,7 @@ package widgets
import (
"fmt"
+ "github.com/Server-Pulse/server-pulse/i18n"
"strings"
"github.com/Server-Pulse/server-pulse/utils"
@@ -21,8 +22,8 @@ func (m Model) renderContainerMenu() string {
doc := strings.Builder{}
doc.WriteString("CONTAINER MENU\n")
- doc.WriteString(fmt.Sprintf("Container: %s\n", m.Monitor.SelectedContainer.Name))
- doc.WriteString(fmt.Sprintf("Status: %s\n", m.Monitor.SelectedContainer.Status))
+ doc.WriteString(fmt.Sprintf(i18n.T("Container: %s\n"), m.Monitor.SelectedContainer.Name))
+ doc.WriteString(fmt.Sprintf(i18n.T("Status: %s\n"), m.Monitor.SelectedContainer.Status))
doc.WriteString("\n")
for i, item := range m.Monitor.ContainerMenuItems {
@@ -42,7 +43,7 @@ func (m Model) renderContainerMenu() string {
func (m Model) renderContainerSingleView() string {
if m.Monitor.SelectedContainer == nil {
- return v.CardStyle.Render("No container selected.")
+ return v.CardStyle.Render(i18n.T("No container selected."))
}
style := lipgloss.NewStyle().Padding(0, 2).
Foreground(v.WhiteColor).
@@ -67,7 +68,7 @@ func (m Model) renderContainerSingleView() string {
case model.ContainerTabEnv:
content = m.renderContainerEnv()
default:
- content = "Not implemented"
+ content = i18n.T("Not implemented")
}
return lipgloss.JoinVertical(lipgloss.Left, tabsHeader, content)
@@ -83,11 +84,11 @@ func (m Model) renderContainerGeneral() string {
containerName = m.Monitor.SelectedContainer.Name
}
- doc.WriteString(lipgloss.NewStyle().Bold(true).Underline(true).MarginBottom(1).Render(fmt.Sprintf("Container: %s", containerName)))
+ doc.WriteString(lipgloss.NewStyle().Bold(true).Underline(true).MarginBottom(1).Render(fmt.Sprintf(i18n.T("Container: %s"), containerName)))
doc.WriteString("\n\n")
if m.Monitor.ContainerDetails != nil {
- info := fmt.Sprintf("ID: %s\nName: %s\nImage: %s\nStatus: %s\nProject: %s\nCreated: %s\nUptime: %s\nHealth: %s\nIP Address: %s\nGateway: %s",
+ info := fmt.Sprintf(i18n.T("ID: %s\nName: %s\nImage: %s\nStatus: %s\nProject: %s\nCreated: %s\nUptime: %s\nHealth: %s\nIP Address: %s\nGateway: %s"),
m.Monitor.ContainerDetails.ID,
m.Monitor.ContainerDetails.Name,
m.Monitor.ContainerDetails.Image,
@@ -103,7 +104,7 @@ func (m Model) renderContainerGeneral() string {
if len(m.Monitor.ContainerDetails.Ports) > 0 {
doc.WriteString("\n\n")
- doc.WriteString(lipgloss.NewStyle().Bold(true).Render("Ports:"))
+ doc.WriteString(lipgloss.NewStyle().Bold(true).Render(i18n.T("Ports:")))
doc.WriteString("\n")
for _, port := range m.Monitor.ContainerDetails.Ports {
portInfo := fmt.Sprintf(" %s:%d → %d/%s", port.HostIP, port.PublicPort, port.PrivatePort, port.Type)
@@ -114,18 +115,18 @@ func (m Model) renderContainerGeneral() string {
if m.Monitor.ContainerDetails.Command != "" {
doc.WriteString("\n")
- doc.WriteString(lipgloss.NewStyle().Bold(true).Render("Command:"))
+ doc.WriteString(lipgloss.NewStyle().Bold(true).Render(i18n.T("Command:")))
doc.WriteString("\n")
doc.WriteString(v.MetricValueStyle.Render(" " + m.Monitor.ContainerDetails.Command))
}
if len(m.Monitor.ContainerDetails.Environment) > 0 {
doc.WriteString("\n\n")
- doc.WriteString(lipgloss.NewStyle().Bold(true).Render("Environment:"))
- doc.WriteString(fmt.Sprintf(" %d variables", len(m.Monitor.ContainerDetails.Environment)))
+ doc.WriteString(lipgloss.NewStyle().Bold(true).Render(i18n.T("Environment:")))
+ doc.WriteString(fmt.Sprintf(i18n.T(" %d variables"), len(m.Monitor.ContainerDetails.Environment)))
}
} else {
- info := "Loading container details..."
+ info := i18n.T("Loading container details...")
doc.WriteString(v.MetricLabelStyle.Render(info))
}
@@ -136,22 +137,22 @@ func (m Model) renderContainerCPU() string {
doc := strings.Builder{}
- doc.WriteString(lipgloss.NewStyle().Bold(true).Underline(true).MarginBottom(1).Render("CPU Usage"))
+ doc.WriteString(lipgloss.NewStyle().Bold(true).Underline(true).MarginBottom(1).Render(i18n.T("CPU Usage")))
doc.WriteString("\n")
if m.Monitor.ContainerDetails != nil {
cpuPercent := m.Monitor.ContainerDetails.Stats.CPUPercent
- doc.WriteString(fmt.Sprintf("Usage: %.1f%%\n", cpuPercent))
+ doc.WriteString(fmt.Sprintf(i18n.T("Usage: %.1f%%\n"), cpuPercent))
// Display progress bar with dynamic color
doc.WriteString(renderProgressBar(cpuPercent) + "\n\n")
- doc.WriteString(lipgloss.NewStyle().Bold(true).Render("Usage History:"))
+ doc.WriteString(lipgloss.NewStyle().Bold(true).Render(i18n.T("Usage History:")))
doc.WriteString("\n")
chart := m.renderCPUChart(50, 10)
doc.WriteString(chart)
} else {
- doc.WriteString(v.MetricLabelStyle.Render("Loading CPU metrics..."))
+ doc.WriteString(v.MetricLabelStyle.Render(i18n.T("Loading CPU metrics...")))
}
return v.CardStyle.Render(doc.String())
@@ -161,7 +162,7 @@ func (m Model) renderContainerMemory() string {
doc := strings.Builder{}
- doc.WriteString(lipgloss.NewStyle().Bold(true).Underline(true).MarginBottom(1).Render("Memory Usage"))
+ doc.WriteString(lipgloss.NewStyle().Bold(true).Underline(true).MarginBottom(1).Render(i18n.T("Memory Usage")))
doc.WriteString("\n")
if m.Monitor.ContainerDetails != nil {
@@ -169,18 +170,18 @@ func (m Model) renderContainerMemory() string {
memUsage := m.Monitor.ContainerDetails.Stats.MemoryUsage
memLimit := m.Monitor.ContainerDetails.Stats.MemoryLimit
- doc.WriteString(fmt.Sprintf("Usage: %.1f%% | Used: %s | Limit: %s | Available: %s\n\n", memPercent,
+ doc.WriteString(fmt.Sprintf(i18n.T("Usage: %.1f%% | Used: %s | Limit: %s | Available: %s\n\n"), memPercent,
utils.FormatBytes(memUsage), utils.FormatBytes(memLimit), utils.FormatBytes(memLimit-memUsage)))
// Display progress bar with dynamic color
doc.WriteString(renderProgressBar(memPercent) + "\n\n")
- doc.WriteString(lipgloss.NewStyle().Bold(true).Render("Usage History:"))
+ doc.WriteString(lipgloss.NewStyle().Bold(true).Render(i18n.T("Usage History:")))
doc.WriteString("\n")
chart := m.renderMemoryChart(50, 10)
doc.WriteString(chart)
} else {
- doc.WriteString(v.MetricLabelStyle.Render("Loading memory metrics..."))
+ doc.WriteString(v.MetricLabelStyle.Render(i18n.T("Loading memory metrics...")))
}
return v.CardStyle.Render(doc.String())
@@ -189,15 +190,15 @@ func (m Model) renderContainerMemory() string {
func (m Model) renderContainerNetwork() string {
doc := strings.Builder{}
- doc.WriteString(lipgloss.NewStyle().Bold(true).Underline(true).MarginBottom(1).Render("Network Usage"))
+ doc.WriteString(lipgloss.NewStyle().Bold(true).Underline(true).MarginBottom(1).Render(i18n.T("Network Usage")))
doc.WriteString("\n")
if m.Monitor.ContainerDetails != nil {
rxBytes := m.Monitor.ContainerDetails.Stats.NetworkRx
txBytes := m.Monitor.ContainerDetails.Stats.NetworkTx
- doc.WriteString(fmt.Sprintf("RX Total: %s\n", utils.FormatBytes(rxBytes)))
- doc.WriteString(fmt.Sprintf("TX Total: %s\n\n", utils.FormatBytes(txBytes)))
+ doc.WriteString(fmt.Sprintf(i18n.T("RX Total: %s\n"), utils.FormatBytes(rxBytes)))
+ doc.WriteString(fmt.Sprintf(i18n.T("TX Total: %s\n\n"), utils.FormatBytes(txBytes)))
rxChart := m.renderNetworkRXChart(50, 6)
doc.WriteString(rxChart)
@@ -206,12 +207,12 @@ func (m Model) renderContainerNetwork() string {
doc.WriteString(txChart)
if m.Monitor.ContainerDetails.IPAddress != "" {
- doc.WriteString("\n\n" + lipgloss.NewStyle().Bold(true).Render("Network Interfaces:"))
+ doc.WriteString("\n\n" + lipgloss.NewStyle().Bold(true).Render(i18n.T("Network Interfaces:")))
doc.WriteString("\n")
doc.WriteString(v.MetricValueStyle.Render(" " + m.Monitor.ContainerDetails.IPAddress))
}
} else {
- doc.WriteString(v.MetricLabelStyle.Render("Loading network metrics..."))
+ doc.WriteString(v.MetricLabelStyle.Render(i18n.T("Loading network metrics...")))
}
return v.CardStyle.Render(doc.String())
@@ -221,15 +222,15 @@ func (m Model) renderContainerNetwork() string {
func (m Model) renderContainerDisk() string {
doc := strings.Builder{}
- doc.WriteString(lipgloss.NewStyle().Bold(true).Underline(true).MarginBottom(1).Render("Disk I/O"))
+ doc.WriteString(lipgloss.NewStyle().Bold(true).Underline(true).MarginBottom(1).Render(i18n.T("Disk I/O")))
doc.WriteString("\n\n")
if m.Monitor.ContainerDetails != nil {
readBytes := m.Monitor.ContainerDetails.Stats.BlockRead
writeBytes := m.Monitor.ContainerDetails.Stats.BlockWrite
- doc.WriteString(fmt.Sprintf("Read: %s\n", utils.FormatBytes(readBytes)))
- doc.WriteString(fmt.Sprintf("Write: %s\n\n", utils.FormatBytes(writeBytes)))
+ doc.WriteString(fmt.Sprintf(i18n.T("Read: %s\n"), utils.FormatBytes(readBytes)))
+ doc.WriteString(fmt.Sprintf(i18n.T("Write: %s\n\n"), utils.FormatBytes(writeBytes)))
totalIO := readBytes + writeBytes
if totalIO > 0 {
@@ -246,11 +247,11 @@ func (m Model) renderContainerDisk() string {
doc.WriteString(fmt.Sprintf("WRITE [%s] %.1f%%\n", writeBar, writePercent))
}
- doc.WriteString("\n" + lipgloss.NewStyle().Bold(true).Render("I/O History:"))
+ doc.WriteString("\n" + lipgloss.NewStyle().Bold(true).Render(i18n.T("I/O History:")))
doc.WriteString("\n")
- doc.WriteString("Disk I/O history charts coming soon...")
+ doc.WriteString(i18n.T("Disk I/O history charts coming soon..."))
} else {
- doc.WriteString(v.MetricLabelStyle.Render("Loading disk metrics..."))
+ doc.WriteString(v.MetricLabelStyle.Render(i18n.T("Loading disk metrics...")))
}
return v.CardStyle.Render(doc.String())
@@ -261,7 +262,7 @@ func (m Model) renderContainerEnv() string {
doc := strings.Builder{}
- doc.WriteString(lipgloss.NewStyle().Bold(true).Underline(true).MarginBottom(1).Render("Environment Variables"))
+ doc.WriteString(lipgloss.NewStyle().Bold(true).Underline(true).MarginBottom(1).Render(i18n.T("Environment Variables")))
doc.WriteString("\n\n")
if m.Monitor.ContainerDetails != nil && len(m.Monitor.ContainerDetails.Environment) > 0 {
@@ -279,9 +280,9 @@ func (m Model) renderContainerEnv() string {
}
}
} else if m.Monitor.ContainerDetails != nil {
- doc.WriteString(v.MetricLabelStyle.Render("No environment variables found"))
+ doc.WriteString(v.MetricLabelStyle.Render(i18n.T("No environment variables found")))
} else {
- doc.WriteString(v.MetricLabelStyle.Render("Loading environment variables..."))
+ doc.WriteString(v.MetricLabelStyle.Render(i18n.T("Loading environment variables...")))
}
return v.CardStyle.Render(doc.String())
@@ -289,11 +290,11 @@ func (m Model) renderContainerEnv() string {
func (m Model) renderContainerLogs() string {
if m.Monitor.SelectedContainer == nil {
- return v.CardStyle.Render("No container selected")
+ return v.CardStyle.Render(i18n.T("No container selected"))
}
doc := strings.Builder{}
- status := "Loading..."
+ status := i18n.T("Loading...")
if m.Monitor.ContainerLogsStreaming {
status = "🟢 Live"
} else if !m.Monitor.ContainerLogsLoading {
@@ -314,7 +315,7 @@ func (m Model) renderContainerLogs() string {
m.Monitor.ContainerLogsPagination.TotalPages)))
if m.Monitor.ContainerLogsLoading {
- doc.WriteString("\n\n" + v.MetricLabelStyle.Render("Loading logs..."))
+ doc.WriteString("\n\n" + v.MetricLabelStyle.Render(i18n.T("Loading logs...")))
} else if len(m.Monitor.ContainerLogsPagination.Lines) > 0 {
logStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("250")).
@@ -326,7 +327,7 @@ func (m Model) renderContainerLogs() string {
m.LogsViewport.Style = logStyle
doc.WriteString("\n" + m.LogsViewport.View())
} else {
- doc.WriteString("\n\n" + v.MetricLabelStyle.Render("No logs available"))
+ doc.WriteString("\n\n" + v.MetricLabelStyle.Render(i18n.T("No logs available")))
}
return v.CardStyle.Render(doc.String())
diff --git a/widgets/render-diagnostic.go b/widgets/render-diagnostic.go
index b4cc141..56dba7e 100644
--- a/widgets/render-diagnostic.go
+++ b/widgets/render-diagnostic.go
@@ -2,6 +2,7 @@ package widgets
import (
"fmt"
+ "github.com/Server-Pulse/server-pulse/i18n"
"strings"
"github.com/Server-Pulse/server-pulse/system/performance"
@@ -14,23 +15,23 @@ import (
func (m Model) renderLogEntryDetails() string {
if m.Diagnostic.SelectedLogEntry == nil {
- return vars.CardStyle.Render("No log entry selected.")
+ return vars.CardStyle.Render(i18n.T("No log entry selected."))
}
entry := m.Diagnostic.SelectedLogEntry
var builder strings.Builder
// Title
- title := lipgloss.NewStyle().Bold(true).Underline(true).MarginBottom(1).Render("Log Entry Details")
+ title := lipgloss.NewStyle().Bold(true).Underline(true).MarginBottom(1).Render(i18n.T("Log Entry Details"))
builder.WriteString(title)
builder.WriteString("\n\n")
// Details
- builder.WriteString(fmt.Sprintf("%s: %s\n", vars.MetricLabelStyle.Render("Time"), entry.Timestamp))
- builder.WriteString(fmt.Sprintf("%s: %s\n", vars.MetricLabelStyle.Render("Level"), entry.Level))
- builder.WriteString(fmt.Sprintf("%s: %s\n", vars.MetricLabelStyle.Render("Service"), entry.Service))
+ builder.WriteString(fmt.Sprintf("%s: %s\n", vars.MetricLabelStyle.Render(i18n.T("Time")), entry.Timestamp))
+ builder.WriteString(fmt.Sprintf("%s: %s\n", vars.MetricLabelStyle.Render(i18n.T("Level")), entry.Level))
+ builder.WriteString(fmt.Sprintf("%s: %s\n", vars.MetricLabelStyle.Render(i18n.T("Service")), entry.Service))
builder.WriteString("\n")
- builder.WriteString(vars.MetricLabelStyle.Render("Message:"))
+ builder.WriteString(vars.MetricLabelStyle.Render(i18n.T("Message:")))
builder.WriteString("\n\n")
// Message with word wrapping in a bordered box
@@ -42,7 +43,7 @@ func (m Model) renderLogEntryDetails() string {
builder.WriteString(messageStyle.Render(entry.Message))
builder.WriteString("\n\n")
- builder.WriteString(lipgloss.NewStyle().Faint(true).Render("Press 'b', 'esc', or 'enter' to go back."))
+ builder.WriteString(lipgloss.NewStyle().Faint(true).Render(i18n.T("Press 'b', 'esc', or 'enter' to go back.")))
return vars.CardStyle.Render(builder.String())
}
@@ -68,7 +69,7 @@ func (m Model) renderDiagnosticSecurity() string {
doc := strings.Builder{}
// Title
- doc.WriteString(lipgloss.NewStyle().Bold(true).Underline(true).MarginBottom(1).Render("Security Checks"))
+ doc.WriteString(lipgloss.NewStyle().Bold(true).Underline(true).MarginBottom(1).Render(i18n.T("Security Checks")))
doc.WriteString("\n\n")
// Authentication section
@@ -82,7 +83,7 @@ func (m Model) renderDiagnosticSecurity() string {
doc.WriteString("\n")
doc.WriteString(m.Diagnostic.Password.View())
doc.WriteString("\n\n")
- doc.WriteString(auth.AuthInfoStyle.Render(auth.AuthInstructions))
+ doc.WriteString(auth.AuthInfoStyle.Render(auth.AuthInstructions()))
} else {
doc.WriteString(auth.AuthInProgressStyle.Render("⏳ " + m.Diagnostic.AuthMessage))
}
@@ -98,7 +99,7 @@ func (m Model) renderDiagnosticSecurity() string {
doc.WriteString("\n\n")
doc.WriteString(m.Diagnostic.AuthMessage)
doc.WriteString("\n\n")
- doc.WriteString(auth.AuthInfoStyle.Render(auth.AuthRetryMessage))
+ doc.WriteString(auth.AuthInfoStyle.Render(auth.AuthRetryMessage()))
doc.WriteString("\n\n")
}
@@ -111,28 +112,28 @@ func (m Model) renderDiagnosticSecurity() string {
}
if len(m.Diagnostic.SecurityChecks) == 0 {
- doc.WriteString("Loading security checks...")
+ doc.WriteString(i18n.T("Loading security checks..."))
doc.WriteString("\n\n")
return vars.CardStyle.Render(doc.String())
}
// Domain input section
if m.Diagnostic.DomainInputMode {
- doc.WriteString(lipgloss.NewStyle().Bold(true).Render("Enter Domain Name for SSL Check"))
+ doc.WriteString(lipgloss.NewStyle().Bold(true).Render(i18n.T("Enter Domain Name for SSL Check")))
doc.WriteString("\n")
doc.WriteString(m.Diagnostic.DomainInput.View())
doc.WriteString("\n\n")
- doc.WriteString(lipgloss.NewStyle().Faint(true).Render("Press Enter to check SSL, Esc to cancel"))
+ doc.WriteString(lipgloss.NewStyle().Faint(true).Render(i18n.T("Press Enter to check SSL, Esc to cancel")))
doc.WriteString("\n\n")
} else {
// Show current domain or prompt to enter one
currentDomain := m.Diagnostic.DomainInput.Value()
if currentDomain == "" {
- doc.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("244")).Render("Press 'd' to enter domain name for SSL check"))
+ doc.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("244")).Render(i18n.T("Press 'd' to enter domain name for SSL check")))
} else {
- doc.WriteString(lipgloss.NewStyle().Bold(true).Render("Current Domain: ") + currentDomain)
+ doc.WriteString(lipgloss.NewStyle().Bold(true).Render(i18n.T("Current Domain: ")) + currentDomain)
doc.WriteString("\n")
- doc.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("244")).Render("Press 'd' to change domain, 'r' to refresh checks"))
+ doc.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("244")).Render(i18n.T("Press 'd' to change domain, 'r' to refresh checks")))
}
doc.WriteString("\n\n")
}
@@ -167,7 +168,7 @@ func (m Model) renderDiagnosticSecurity() string {
// Footer with authentication info
doc.WriteString("\n\n")
if !m.AsRoot && !m.CanRunSudo {
- doc.WriteString(auth.AccessIndicatorStyle.Render(auth.AdminAccessRequired))
+ doc.WriteString(auth.AccessIndicatorStyle.Render(auth.AdminAccessRequired()))
}
doc.WriteString("\n\n")
@@ -177,28 +178,28 @@ func (m Model) renderDiagnosticSecurity() string {
func (m Model) renderCertificateDetails() string {
if m.Diagnostic.CertificateInfo == nil {
- return vars.CardStyle.Render("No certificate information available")
+ return vars.CardStyle.Render(i18n.T("No certificate information available"))
}
cert := m.Diagnostic.CertificateInfo
doc := strings.Builder{}
// Title
- doc.WriteString(lipgloss.NewStyle().Bold(true).Underline(true).MarginBottom(1).Render("SSL Certificate Details"))
+ doc.WriteString(lipgloss.NewStyle().Bold(true).Underline(true).MarginBottom(1).Render(i18n.T("SSL Certificate Details")))
doc.WriteString("\n\n")
// Certificate information
- doc.WriteString(vars.MetricLabelStyle.Render("Subject: ") + cert.Subject + "\n")
- doc.WriteString(vars.MetricLabelStyle.Render("Issuer: ") + cert.Issuer + "\n")
- doc.WriteString(vars.MetricLabelStyle.Render("Serial Number: ") + cert.SerialNumber + "\n")
- doc.WriteString(vars.MetricLabelStyle.Render("Version: ") + fmt.Sprintf("%d", cert.Version) + "\n")
+ doc.WriteString(vars.MetricLabelStyle.Render(i18n.T("Subject: ")) + cert.Subject + "\n")
+ doc.WriteString(vars.MetricLabelStyle.Render(i18n.T("Issuer: ")) + cert.Issuer + "\n")
+ doc.WriteString(vars.MetricLabelStyle.Render(i18n.T("Serial Number: ")) + cert.SerialNumber + "\n")
+ doc.WriteString(vars.MetricLabelStyle.Render(i18n.T("Version: ")) + fmt.Sprintf("%d", cert.Version) + "\n")
doc.WriteString("\n")
// Validity period
- doc.WriteString(lipgloss.NewStyle().Bold(true).Render("Validity Period"))
+ doc.WriteString(lipgloss.NewStyle().Bold(true).Render(i18n.T("Validity Period")))
doc.WriteString("\n")
- doc.WriteString(vars.MetricLabelStyle.Render("Valid From: ") + cert.ValidityPeriodFrom + "\n")
- doc.WriteString(vars.MetricLabelStyle.Render("Valid To: ") + cert.ValidityPeriodTo + "\n")
+ doc.WriteString(vars.MetricLabelStyle.Render(i18n.T("Valid From: ")) + cert.ValidityPeriodFrom + "\n")
+ doc.WriteString(vars.MetricLabelStyle.Render(i18n.T("Valid To: ")) + cert.ValidityPeriodTo + "\n")
// Days until expiry with color coding
expiryText := ""
@@ -209,14 +210,14 @@ func (m Model) renderCertificateDetails() string {
} else {
expiryText = lipgloss.NewStyle().Foreground(lipgloss.Color("46")).Render("✓ " + fmt.Sprintf("%d", cert.DaysUntilExpiry) + " days")
}
- doc.WriteString(vars.MetricLabelStyle.Render("Days Until Expiry: ") + expiryText + "\n")
+ doc.WriteString(vars.MetricLabelStyle.Render(i18n.T("Days Until Expiry: ")) + expiryText + "\n")
doc.WriteString("\n")
// Security information
- doc.WriteString(lipgloss.NewStyle().Bold(true).Render("Security Information"))
+ doc.WriteString(lipgloss.NewStyle().Bold(true).Render(i18n.T("Security Information")))
doc.WriteString("\n")
- doc.WriteString(vars.MetricLabelStyle.Render("Algorithm: ") + cert.Algorithm + "\n")
- doc.WriteString(vars.MetricLabelStyle.Render("Signature Algorithm: ") + cert.SignatureAlgorithm + "\n")
+ doc.WriteString(vars.MetricLabelStyle.Render(i18n.T("Algorithm: ")) + cert.Algorithm + "\n")
+ doc.WriteString(vars.MetricLabelStyle.Render(i18n.T("Signature Algorithm: ")) + cert.SignatureAlgorithm + "\n")
hostnameStatus := ""
if cert.HostnameVerified {
@@ -224,12 +225,12 @@ func (m Model) renderCertificateDetails() string {
} else {
hostnameStatus = lipgloss.NewStyle().Foreground(lipgloss.Color("196")).Render("✗ Not Verified")
}
- doc.WriteString(vars.MetricLabelStyle.Render("Hostname Verified: ") + hostnameStatus + "\n")
+ doc.WriteString(vars.MetricLabelStyle.Render(i18n.T("Hostname Verified: ")) + hostnameStatus + "\n")
doc.WriteString("\n")
// Alternative names
if len(cert.AlternativeNames) > 0 {
- doc.WriteString(lipgloss.NewStyle().Bold(true).Render("Alternative Names"))
+ doc.WriteString(lipgloss.NewStyle().Bold(true).Render(i18n.T("Alternative Names")))
doc.WriteString("\n")
for _, name := range cert.AlternativeNames {
doc.WriteString("• " + name + "\n")
@@ -242,31 +243,31 @@ func (m Model) renderCertificateDetails() string {
func (m Model) renderSSHRootDetails() string {
if m.Diagnostic.SSHRootInfo == nil {
- return vars.CardStyle.Render("No SSH root login information available")
+ return vars.CardStyle.Render(i18n.T("No SSH root login information available"))
}
sshInfo := m.Diagnostic.SSHRootInfo
doc := strings.Builder{}
// Title
- doc.WriteString(lipgloss.NewStyle().Bold(true).Underline(true).MarginBottom(1).Render("SSH Root Login Details"))
+ doc.WriteString(lipgloss.NewStyle().Bold(true).Underline(true).MarginBottom(1).Render(i18n.T("SSH Root Login Details")))
doc.WriteString("\n\n")
- doc.WriteString(vars.MetricLabelStyle.Render("Status: ") + sshInfo.Status + "\n")
- doc.WriteString(vars.MetricLabelStyle.Render("Details: ") + sshInfo.Details + "\n")
+ doc.WriteString(vars.MetricLabelStyle.Render(i18n.T("Status: ")) + sshInfo.Status + "\n")
+ doc.WriteString(vars.MetricLabelStyle.Render(i18n.T("Details: ")) + sshInfo.Details + "\n")
return vars.CardStyle.Render(doc.String())
}
func (m Model) renderOpenedPortsDetails() string {
if m.Diagnostic.OpenedPortsInfo == nil {
- return vars.CardStyle.Render("No opened ports information available")
+ return vars.CardStyle.Render(i18n.T("No opened ports information available"))
}
doc := strings.Builder{}
// Title
- doc.WriteString(lipgloss.NewStyle().Bold(true).Underline(true).MarginBottom(1).Render("Opened Ports Details"))
+ doc.WriteString(lipgloss.NewStyle().Bold(true).Underline(true).MarginBottom(1).Render(i18n.T("Opened Ports Details")))
doc.WriteString("\n\n")
doc.WriteString(m.Diagnostic.PortsTable.View())
@@ -277,13 +278,13 @@ func (m Model) renderOpenedPortsDetails() string {
func (m Model) renderFirewallDetails() string {
if m.Diagnostic.FirewallInfo == nil {
- return vars.CardStyle.Render("No firewall information available")
+ return vars.CardStyle.Render(i18n.T("No firewall information available"))
}
doc := strings.Builder{}
// Title
- title := fmt.Sprintf("Firewall Details - %s", m.Diagnostic.FirewallInfo.FirewallType)
+ title := fmt.Sprintf(i18n.T("Firewall Details - %s"), m.Diagnostic.FirewallInfo.FirewallType)
doc.WriteString(lipgloss.NewStyle().Bold(true).Underline(true).MarginBottom(1).Render(title))
doc.WriteString("\n\n")
@@ -295,29 +296,29 @@ func (m Model) renderFirewallDetails() string {
statusStyle = statusStyle.Foreground(lipgloss.Color("196")) // Red
}
- doc.WriteString(vars.MetricLabelStyle.Render("Status: "))
+ doc.WriteString(vars.MetricLabelStyle.Render(i18n.T("Status: ")))
doc.WriteString(statusStyle.Render(m.Diagnostic.FirewallInfo.Status))
doc.WriteString("\n")
- doc.WriteString(vars.MetricLabelStyle.Render("Details: "))
+ doc.WriteString(vars.MetricLabelStyle.Render(i18n.T("Details: ")))
doc.WriteString(m.Diagnostic.FirewallInfo.Details)
doc.WriteString("\n\n")
// Rules summary
if len(m.Diagnostic.FirewallInfo.Rules) > 0 {
- doc.WriteString(lipgloss.NewStyle().Bold(true).Render(fmt.Sprintf("Firewall Rules (%d total)", len(m.Diagnostic.FirewallInfo.Rules))))
+ doc.WriteString(lipgloss.NewStyle().Bold(true).Render(fmt.Sprintf(i18n.T("Firewall Rules (%d total)"), len(m.Diagnostic.FirewallInfo.Rules))))
doc.WriteString("\n\n")
doc.WriteString(m.Diagnostic.FirewallTable.View())
doc.WriteString("\n\n")
} else {
- doc.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("244")).Render("No firewall rules configured"))
+ doc.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("244")).Render(i18n.T("No firewall rules configured")))
doc.WriteString("\n\n")
}
// Raw output section (collapsible view)
if m.Diagnostic.FirewallInfo.RawOutput != "" {
- doc.WriteString(lipgloss.NewStyle().Bold(true).Render("Complete Firewall Configuration:"))
+ doc.WriteString(lipgloss.NewStyle().Bold(true).Render(i18n.T("Complete Firewall Configuration:")))
doc.WriteString("\n")
- doc.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("244")).Render("(Full raw output from firewall command)"))
+ doc.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("244")).Render(i18n.T("(Full raw output from firewall command)")))
doc.WriteString("\n\n")
// Show raw output in a bordered box
@@ -336,13 +337,13 @@ func (m Model) renderFirewallDetails() string {
func (m Model) renderAutoBanDetails() string {
if m.Diagnostic.AutoBanInfo == nil {
- return vars.CardStyle.Render("No auto-ban information available")
+ return vars.CardStyle.Render(i18n.T("No auto-ban information available"))
}
doc := strings.Builder{}
// Title
- title := fmt.Sprintf("Auto Ban Details - %s", m.Diagnostic.AutoBanInfo.ServiceType)
+ title := fmt.Sprintf(i18n.T("Auto Ban Details - %s"), m.Diagnostic.AutoBanInfo.ServiceType)
doc.WriteString(lipgloss.NewStyle().Bold(true).Underline(true).MarginBottom(1).Render(title))
doc.WriteString("\n\n")
@@ -354,36 +355,36 @@ func (m Model) renderAutoBanDetails() string {
statusStyle = statusStyle.Foreground(lipgloss.Color("196")) // Red
}
- doc.WriteString(vars.MetricLabelStyle.Render("Status: "))
+ doc.WriteString(vars.MetricLabelStyle.Render(i18n.T("Status: ")))
doc.WriteString(statusStyle.Render(m.Diagnostic.AutoBanInfo.Status))
doc.WriteString("\n")
if m.Diagnostic.AutoBanInfo.Version != "" {
- doc.WriteString(vars.MetricLabelStyle.Render("Version: "))
+ doc.WriteString(vars.MetricLabelStyle.Render(i18n.T("Version: ")))
doc.WriteString(m.Diagnostic.AutoBanInfo.Version)
doc.WriteString("\n")
}
- doc.WriteString(vars.MetricLabelStyle.Render("Details: "))
+ doc.WriteString(vars.MetricLabelStyle.Render(i18n.T("Details: ")))
doc.WriteString(m.Diagnostic.AutoBanInfo.Details)
doc.WriteString("\n\n")
// Jails/Services table
if len(m.Diagnostic.AutoBanInfo.Jails) > 0 {
- doc.WriteString(lipgloss.NewStyle().Bold(true).Render(fmt.Sprintf("Jails/Services (%d total)", len(m.Diagnostic.AutoBanInfo.Jails))))
+ doc.WriteString(lipgloss.NewStyle().Bold(true).Render(fmt.Sprintf(i18n.T("Jails/Services (%d total)"), len(m.Diagnostic.AutoBanInfo.Jails))))
doc.WriteString("\n\n")
doc.WriteString(m.Diagnostic.AutoBanTable.View())
doc.WriteString("\n\n")
} else {
- doc.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("244")).Render("No jails/services configured"))
+ doc.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("244")).Render(i18n.T("No jails/services configured")))
doc.WriteString("\n\n")
}
// Raw output section
if m.Diagnostic.AutoBanInfo.RawOutput != "" {
- doc.WriteString(lipgloss.NewStyle().Bold(true).Render("Complete Configuration:"))
+ doc.WriteString(lipgloss.NewStyle().Bold(true).Render(i18n.T("Complete Configuration:")))
doc.WriteString("\n")
- doc.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("244")).Render("(Full raw output from service)"))
+ doc.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("244")).Render(i18n.T("(Full raw output from service)")))
doc.WriteString("\n\n")
rawOutputStyle := lipgloss.NewStyle().
@@ -404,7 +405,7 @@ func (m Model) renderDiagnosticLogs() string {
// Custom time input mode - show popup
if m.Diagnostic.CustomTimeInputMode {
- doc.WriteString(lipgloss.NewStyle().Bold(true).Render("Enter Custom Time Range"))
+ doc.WriteString(lipgloss.NewStyle().Bold(true).Render(i18n.T("Enter Custom Time Range")))
doc.WriteString("\n")
doc.WriteString(m.Diagnostic.LogTimeRangeInput.View())
doc.WriteString("\n\n")
@@ -418,9 +419,9 @@ func (m Model) renderDiagnosticLogs() string {
doc.WriteString("\n\n")
}
- doc.WriteString(lipgloss.NewStyle().Faint(true).Render("Examples: '2 hours ago', '2025-01-08 14:30:00', '3 days ago'"))
+ doc.WriteString(lipgloss.NewStyle().Faint(true).Render(i18n.T("Examples: '2 hours ago', '2025-01-08 14:30:00', '3 days ago'")))
doc.WriteString("\n")
- doc.WriteString(lipgloss.NewStyle().Faint(true).Render("Press Enter to apply, ESC to cancel"))
+ doc.WriteString(lipgloss.NewStyle().Faint(true).Render(i18n.T("Press Enter to apply, ESC to cancel")))
return vars.CardStyle.Render(doc.String())
}
@@ -435,8 +436,8 @@ func (m Model) renderDiagnosticLogs() string {
// Filter selection indicator
filterIndicatorStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("214"))
- timeIndicator := "Time"
- levelIndicator := "Level"
+ timeIndicator := i18n.T("Time")
+ levelIndicator := i18n.T("Level")
if m.Diagnostic.LogFilterSelected == 0 {
timeIndicator = filterIndicatorStyle.Render("▶ Time")
} else {
@@ -444,7 +445,7 @@ func (m Model) renderDiagnosticLogs() string {
}
// Time range options
- timeRanges := []string{"All", "5m", "1h", "24h", "7d", "Custom"}
+ timeRanges := i18n.Ts("All", "5m", "1h", "24h", "7d", "Custom")
timeRangeStr := timeIndicator + ": "
for i, tr := range timeRanges {
if i == m.Diagnostic.LogTimeSelected {
@@ -457,7 +458,7 @@ func (m Model) renderDiagnosticLogs() string {
filterDoc.WriteString("\n")
// Log level options
- levels := []string{"All", "Error", "Warn", "Info", "Debug"}
+ levels := i18n.Ts("All", "Error", "Warn", "Info", "Debug")
levelStr := levelIndicator + ": "
for i, level := range levels {
if i == m.Diagnostic.LogLevelSelected {
@@ -470,21 +471,21 @@ func (m Model) renderDiagnosticLogs() string {
filterDoc.WriteString("\n")
// Filter inputs
- filterDoc.WriteString("Search: " + m.Diagnostic.LogSearchInput.View() + " ")
- filterDoc.WriteString("Service: " + m.Diagnostic.LogServiceInput.View())
+ filterDoc.WriteString(i18n.T("Search: ") + m.Diagnostic.LogSearchInput.View() + " ")
+ filterDoc.WriteString(i18n.T("Service: ") + m.Diagnostic.LogServiceInput.View())
doc.WriteString(filterStyle.Render(filterDoc.String()))
doc.WriteString("\n\n")
// Log entries table or loading message
if m.Diagnostic.LogsInfo == nil {
- doc.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("244")).Render("Press 'r' to load logs or Enter to apply filters"))
+ doc.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("244")).Render(i18n.T("Press 'r' to load logs or Enter to apply filters")))
doc.WriteString("\n")
} else if m.Diagnostic.LogsInfo.ErrorMsg != "" {
- doc.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("196")).Render("Error: " + m.Diagnostic.LogsInfo.ErrorMsg))
+ doc.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("196")).Render(i18n.T("Error: ") + m.Diagnostic.LogsInfo.ErrorMsg))
doc.WriteString("\n")
} else if len(m.Diagnostic.LogsInfo.Entries) == 0 {
- doc.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("244")).Render("No log entries found matching filters"))
+ doc.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("244")).Render(i18n.T("No log entries found matching filters")))
doc.WriteString("\n")
} else {
// Display logs table
@@ -493,7 +494,7 @@ func (m Model) renderDiagnosticLogs() string {
// Summary
summaryStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("244"))
- summary := fmt.Sprintf("Showing %d entries", m.Diagnostic.LogsInfo.TotalCount)
+ summary := fmt.Sprintf(i18n.T("Showing %d entries"), m.Diagnostic.LogsInfo.TotalCount)
if m.Diagnostic.LogsInfo.HasMore {
summary += " (more available - increase limit or refine filters)"
}
@@ -557,15 +558,15 @@ func (m Model) renderPerformanceAnalysis() string {
func (m Model) renderCPUPerformance() string {
if m.Diagnostic.Performance.CPULoading {
- return vars.CardStyle.Render("⏳ Loading CPU Performance Metrics...")
+ return vars.CardStyle.Render(i18n.T("⏳ Loading CPU Performance Metrics..."))
}
if m.Diagnostic.Performance.CPUMetrics == nil {
- return vars.CardStyle.Render("Press 'r' to load CPU metrics")
+ return vars.CardStyle.Render(i18n.T("Press 'r' to load CPU metrics"))
}
// CPU sub-tabs navigation
- cpuTabs := []string{"CPU State Breakdown", "Per-Core Performance", "System Activity Metrics"}
+ cpuTabs := []string{i18n.T("CPU State Breakdown"), i18n.T("Per-Core Performance"), i18n.T("System Activity Metrics")}
activeTabStyle := lipgloss.NewStyle().Padding(0, 2).
Foreground(lipgloss.Color("240")).
@@ -609,11 +610,11 @@ func (m Model) renderMemoryPerformance() string {
}
if m.Diagnostic.Performance.MemoryMetrics == nil {
- return vars.CardStyle.Render("Press 'r' to load memory metrics")
+ return vars.CardStyle.Render(i18n.T("Press 'r' to load memory metrics"))
}
// Memory sub-tabs navigation
- memoryTabs := []string{"Overview", "Usage Breakdown", "Swap Analysis", "System Memory"}
+ memoryTabs := []string{i18n.T("Overview"), i18n.T("Usage Breakdown"), i18n.T("Swap Analysis"), i18n.T("System Memory")}
activeTabStyle := lipgloss.NewStyle().Padding(0, 2).
Foreground(lipgloss.Color("240")).
diff --git a/widgets/render-footer.go b/widgets/render-footer.go
index 3175ade..9379bcd 100644
--- a/widgets/render-footer.go
+++ b/widgets/render-footer.go
@@ -3,6 +3,7 @@ package widgets
import (
"strings"
+ "github.com/Server-Pulse/server-pulse/i18n"
"github.com/Server-Pulse/server-pulse/widgets/model"
v "github.com/Server-Pulse/server-pulse/widgets/vars"
"github.com/charmbracelet/lipgloss"
@@ -16,10 +17,11 @@ func (m Model) renderFooter() string {
Background(v.PurpleCollor).
Padding(0, 1).
Bold(true)
- statusLine += statusStyle.Render("⏳ Operation in progress...") + "\n"
+ statusLine += statusStyle.Render(i18n.T("⏳ Operation in progress...")) + "\n"
} else if m.LastOperationMsg != "" {
var statusStyle lipgloss.Style
- if strings.Contains(m.LastOperationMsg, "failed") || strings.Contains(m.LastOperationMsg, "Error") {
+ if strings.Contains(m.LastOperationMsg, "failed") || strings.Contains(m.LastOperationMsg, "Error") ||
+ strings.Contains(m.LastOperationMsg, "失败") || strings.Contains(m.LastOperationMsg, "错误") {
statusStyle = lipgloss.NewStyle().
Foreground(v.WhiteColor).
Background(v.ErrorColor).
diff --git a/widgets/render-header.go b/widgets/render-header.go
index f554422..c3a8bab 100644
--- a/widgets/render-header.go
+++ b/widgets/render-header.go
@@ -2,6 +2,7 @@ package widgets
import (
"fmt"
+ "github.com/Server-Pulse/server-pulse/i18n"
v "github.com/Server-Pulse/server-pulse/widgets/vars"
"strings"
@@ -40,7 +41,7 @@ func (m Model) renderHeader() string {
}
doc := strings.Builder{}
- systemInfo := fmt.Sprintf("Host: %s | OS: %s | Kernel: %s | Uptime: %s", m.Monitor.System.Hostname, m.Monitor.System.OS, m.Monitor.System.Kernel, utils.FormatUptime(m.Monitor.System.Uptime))
+ systemInfo := fmt.Sprintf(i18n.T("Host: %s | OS: %s | Kernel: %s | Uptime: %s"), m.Monitor.System.Hostname, m.Monitor.System.OS, m.Monitor.System.Kernel, utils.FormatUptime(m.Monitor.System.Uptime))
doc.WriteString(v.MetricLabelStyle.Render(systemInfo))
header = append(header, v.CardStyle.MarginBottom(0).Render(doc.String()))
diff --git a/widgets/render-monitor.go b/widgets/render-monitor.go
index 7c72f8d..6c1ab70 100644
--- a/widgets/render-monitor.go
+++ b/widgets/render-monitor.go
@@ -2,6 +2,7 @@ package widgets
import (
"fmt"
+ "github.com/Server-Pulse/server-pulse/i18n"
"strings"
"github.com/Server-Pulse/server-pulse/utils"
@@ -22,31 +23,31 @@ func (m Model) renderMonitor() string {
}
func (m Model) renderContainers() string {
- p := "Search a container..."
+ p := i18n.T("Search a container...")
return m.renderTable(m.Monitor.Container, p)
}
func (m Model) renderProcesses() string {
- p := "Search a process..."
+ p := i18n.T("Search a process...")
return m.renderTable(m.Monitor.ProcessTable, p)
}
func (m Model) renderSystem() string {
doc := strings.Builder{}
- cpuInfo := fmt.Sprintf("CPU: %s %.1f%% | Load: %.2f, %.2f, %.2f", m.Monitor.CpuProgress.View(), m.Monitor.Cpu.Usage, m.Monitor.Cpu.LoadAvg1, m.Monitor.Cpu.LoadAvg5, m.Monitor.Cpu.LoadAvg15)
+ cpuInfo := fmt.Sprintf(i18n.T("CPU: %s %.1f%% | Load: %.2f, %.2f, %.2f"), m.Monitor.CpuProgress.View(), m.Monitor.Cpu.Usage, m.Monitor.Cpu.LoadAvg1, m.Monitor.Cpu.LoadAvg5, m.Monitor.Cpu.LoadAvg15)
doc.WriteString(lipgloss.NewStyle().Bold(true).Render("CPU"))
doc.WriteString("\n")
doc.WriteString(cpuInfo)
doc.WriteString("\n\n")
- memInfo := fmt.Sprintf("RAM: %s %.1f%% | Total: %s | Used: %s | Free: %s", m.Monitor.MemProgress.View(), m.Monitor.Memory.Usage, utils.FormatBytes(m.Monitor.Memory.Total), utils.FormatBytes(m.Monitor.Memory.Used), utils.FormatBytes(m.Monitor.Memory.Free))
- doc.WriteString(lipgloss.NewStyle().Bold(true).Render("Memory"))
+ memInfo := fmt.Sprintf(i18n.T("RAM: %s %.1f%% | Total: %s | Used: %s | Free: %s"), m.Monitor.MemProgress.View(), m.Monitor.Memory.Usage, utils.FormatBytes(m.Monitor.Memory.Total), utils.FormatBytes(m.Monitor.Memory.Used), utils.FormatBytes(m.Monitor.Memory.Free))
+ doc.WriteString(lipgloss.NewStyle().Bold(true).Render(i18n.T("Memory")))
doc.WriteString("\n")
doc.WriteString(memInfo)
doc.WriteString("\n")
- swapInfo := fmt.Sprintf("SWP: %s %.1f%% | Total: %s | Used: %s | Free: %s", m.Monitor.SwapProgress.View(), m.Monitor.Memory.SwapUsage, utils.FormatBytes(m.Monitor.Memory.SwapTotal), utils.FormatBytes(m.Monitor.Memory.SwapUsed), utils.FormatBytes(m.Monitor.Memory.SwapFree))
+ swapInfo := fmt.Sprintf(i18n.T("SWP: %s %.1f%% | Total: %s | Used: %s | Free: %s"), m.Monitor.SwapProgress.View(), m.Monitor.Memory.SwapUsage, utils.FormatBytes(m.Monitor.Memory.SwapTotal), utils.FormatBytes(m.Monitor.Memory.SwapUsed), utils.FormatBytes(m.Monitor.Memory.SwapFree))
doc.WriteString(swapInfo)
doc.WriteString("\n\n")
- doc.WriteString(lipgloss.NewStyle().Bold(true).Render("Disks"))
+ doc.WriteString(lipgloss.NewStyle().Bold(true).Render(i18n.T("Disks")))
doc.WriteString("\n")
for _, disk := range m.Monitor.Disks {
if disk.Total > 0 {
diff --git a/widgets/render-nav.go b/widgets/render-nav.go
index df9a46d..9712735 100644
--- a/widgets/render-nav.go
+++ b/widgets/render-nav.go
@@ -1,6 +1,7 @@
package widgets
import (
+ "github.com/Server-Pulse/server-pulse/i18n"
"strings"
"github.com/Server-Pulse/server-pulse/widgets/model"
@@ -26,7 +27,7 @@ func renderNav(header []string, state model.ContainerTab, styleColor lipgloss.St
}
func (m Model) renderCurrentNav() string {
- if strings.HasPrefix(string(m.Ui.State), "monitor") {
+ if strings.HasPrefix(string(m.Ui.State), i18n.T("monitor")) {
style := lipgloss.NewStyle().Padding(0, 2).
Foreground(v.ClearWhite).
Background(v.PurpleCollor).
diff --git a/widgets/render-network.go b/widgets/render-network.go
index ca44aa3..0154c37 100644
--- a/widgets/render-network.go
+++ b/widgets/render-network.go
@@ -2,6 +2,7 @@ package widgets
import (
"fmt"
+ "github.com/Server-Pulse/server-pulse/i18n"
"strings"
"github.com/Server-Pulse/server-pulse/widgets/auth"
@@ -15,12 +16,12 @@ func (m Model) interfaces() string {
content := strings.Builder{}
statusIcon := "🔴"
- statusText := "Disconnected"
+ statusText := i18n.T("Disconnected")
statusColor := v.ErrorColor
if m.Network.NetworkResource.Connected {
statusIcon = "🟢"
- statusText = "Connected"
+ statusText = i18n.T("Connected")
statusColor = v.SuccessColor
}
@@ -30,7 +31,7 @@ func (m Model) interfaces() string {
content.WriteString(statusLine)
if len(m.Network.NetworkResource.Interfaces) > 0 {
- tableContent := m.renderTable(m.Network.NetworkTable, "No network interfaces")
+ tableContent := m.renderTable(m.Network.NetworkTable, i18n.T("No network interfaces"))
content.WriteString(tableContent)
}
@@ -68,11 +69,11 @@ func (m Model) renderProtocolAnalysis() string {
content.WriteString("\n\n")
content.WriteString(m.Network.AuthMessage)
if m.Network.AuthState == model.AuthRequired {
- content.WriteString(auth.AuthPromptStyle.Render("Enter Password:"))
+ content.WriteString(auth.AuthPromptStyle.Render(i18n.T("Enter Password:")))
content.WriteString("\n")
content.WriteString(m.Diagnostic.Password.View())
content.WriteString("\n\n")
- content.WriteString(auth.AuthInfoStyle.Render(auth.AuthInstructions))
+ content.WriteString(auth.AuthInfoStyle.Render(auth.AuthInstructions()))
} else {
content.WriteString(auth.AuthInProgressStyle.Render("⏳ " + m.Network.AuthMessage))
}
@@ -88,7 +89,7 @@ func (m Model) renderProtocolAnalysis() string {
content.WriteString("\n\n")
content.WriteString(m.Network.AuthMessage)
content.WriteString("\n\n")
- content.WriteString(auth.AuthInfoStyle.Render(auth.AuthRetryMessage))
+ content.WriteString(auth.AuthInfoStyle.Render(auth.AuthRetryMessage()))
}
if m.Network.AuthState == model.AuthSuccess && m.Network.AuthTimer > 0 {
@@ -107,7 +108,7 @@ func (m Model) renderProtocolAnalysis() string {
stats := m.getConnectionStats()
statsText := lipgloss.NewStyle().
Foreground(lipgloss.Color("229")).
- Render(fmt.Sprintf("TCP: %d | UDP: %d | Listening: %d | Established: %d",
+ Render(fmt.Sprintf(i18n.T("TCP: %d | UDP: %d | Listening: %d | Established: %d"),
stats.tcp, stats.udp, stats.listening, stats.established))
content.WriteString(statsText + "\n")
}
@@ -122,10 +123,10 @@ func (m Model) renderProtocolAnalysis() string {
content.WriteString(accessInfo + "\n")
content.WriteString(lipgloss.NewStyle().
Foreground(lipgloss.Color("244")).
- Render("Press 'a' to authenticate for detailed connection information"))
+ Render(i18n.T("Press 'a' to authenticate for detailed connection information")))
}
- tableContent := m.renderTable(m.Network.ConnectionsTable, "No active connections")
+ tableContent := m.renderTable(m.Network.ConnectionsTable, i18n.T("No active connections"))
content.WriteString(tableContent)
return v.CardNetworkStyle.
@@ -141,12 +142,12 @@ func (m Model) renderConnectivityAnalysis() string {
Bold(true).
Foreground(lipgloss.Color("62")).
MarginBottom(1).
- Render("Network Connectivity Tools")
+ Render(i18n.T("Network Connectivity Tools"))
content.WriteString(header + "\n")
instructions := lipgloss.NewStyle().
Foreground(lipgloss.Color("240")).
- Render("Press 'p' to ping, 't' to traceroute, 's' for speed test, 'c' to clear results")
+ Render(i18n.T("Press 'p' to ping, 't' to traceroute, 's' for speed test, 'c' to clear results"))
content.WriteString(instructions + "\n\n")
// Input modes - always visible
@@ -189,7 +190,7 @@ func (m Model) renderConnectivityAnalysis() string {
Bold(true).
Underline(true).
Foreground(lipgloss.Color("39")).
- Render("Ping Results")
+ Render(i18n.T("Ping Results"))
resultsContent.WriteString(pingTitle + "\n")
for _, result := range m.Network.PingResults {
@@ -202,10 +203,10 @@ func (m Model) renderConnectivityAnalysis() string {
statusLine := fmt.Sprintf("%s %s - ", statusIcon, result.Target)
if result.Success {
- statusLine += fmt.Sprintf("Latency: %v, Packet Loss: %.1f%%",
+ statusLine += fmt.Sprintf(i18n.T("Latency: %v, Packet Loss: %.1f%%"),
result.Latency, result.PacketLoss)
} else {
- statusLine += fmt.Sprintf("Error: %s", result.Error)
+ statusLine += i18n.T("Error: ") + result.Error
}
resultsContent.WriteString(lipgloss.NewStyle().Foreground(statusColor).Render(statusLine) + "\n")
@@ -220,11 +221,11 @@ func (m Model) renderConnectivityAnalysis() string {
Bold(true).
Underline(true).
Foreground(lipgloss.Color("208")).
- Render(fmt.Sprintf("Traceroute to %s", tracerouteResult.Target))
+ Render(fmt.Sprintf(i18n.T("Traceroute to %s"), tracerouteResult.Target))
resultsContent.WriteString(tracerouteTitle + "\n")
if tracerouteResult.Error != "" {
- resultsContent.WriteString(lipgloss.NewStyle().Foreground(v.ErrorColor).Render("Error: "+tracerouteResult.Error) + "\n")
+ resultsContent.WriteString(lipgloss.NewStyle().Foreground(v.ErrorColor).Render(i18n.T("Error: ")+tracerouteResult.Error) + "\n")
} else if len(tracerouteResult.Hops) > 0 {
for _, hop := range tracerouteResult.Hops {
hopLine := fmt.Sprintf("%2d. ", hop.HopNumber)
@@ -245,7 +246,7 @@ func (m Model) renderConnectivityAnalysis() string {
resultsContent.WriteString(hopLine + "\n")
}
} else {
- resultsContent.WriteString("No route found\n")
+ resultsContent.WriteString(i18n.T("No route found\n"))
}
resultsContent.WriteString("\n")
}
@@ -261,8 +262,8 @@ func (m Model) renderConnectivityAnalysis() string {
Render("🚀 Speed Test Results")
resultsContent.WriteString(speedTestTitle + "\n")
- resultsContent.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("39")).Render(fmt.Sprintf("Server: %s", result.Server)) + "\n")
- resultsContent.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("39")).Render(fmt.Sprintf("Test Duration: %v", result.TestDuration)) + "\n\n")
+ resultsContent.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("39")).Render(fmt.Sprintf(i18n.T("Server: %s"), result.Server)) + "\n")
+ resultsContent.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("39")).Render(fmt.Sprintf(i18n.T("Test Duration: %v"), result.TestDuration)) + "\n\n")
if result.PingResult != nil {
pingSection := lipgloss.NewStyle().
@@ -313,7 +314,7 @@ func (m Model) renderConnectivityAnalysis() string {
if totalResultsLines > m.Network.ConnectivityPerPage {
paginationInstructions := lipgloss.NewStyle().
Foreground(lipgloss.Color("214")).
- Render("Use '↑' and '↓' to navigate results pages")
+ Render(i18n.T("Use '↑' and '↓' to navigate results pages"))
content.WriteString(paginationInstructions + "\n\n")
}
@@ -328,7 +329,7 @@ func (m Model) renderConnectivityAnalysis() string {
// Add pagination info if needed
if totalPages > 1 {
- paginationInfo := fmt.Sprintf("Page %d/%d (Results %d-%d of %d)",
+ paginationInfo := fmt.Sprintf(i18n.T("Page %d/%d (Results %d-%d of %d)"),
m.Network.ConnectivityPage+1, totalPages, startIdx+1, endIdx, totalResultsLines)
paginationStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("244")).
@@ -379,7 +380,7 @@ func (m Model) renderNetworkConfiguration() string {
if len(m.Network.Routes) > 0 {
content.WriteString(m.Network.RoutesTable.View())
} else {
- content.WriteString("No routing table entries found")
+ content.WriteString(i18n.T("No routing table entries found"))
}
} else {
dnsTitle := lipgloss.NewStyle().
@@ -393,18 +394,18 @@ func (m Model) renderNetworkConfiguration() string {
if len(m.Network.DNS) > 0 {
content.WriteString(m.Network.DNSTable.View())
} else {
- content.WriteString("No DNS servers configured")
+ content.WriteString(i18n.T("No DNS servers configured"))
}
}
footer := lipgloss.NewStyle().
Foreground(lipgloss.Color("244")).
- Render(fmt.Sprintf("\nCurrently viewing: %s | Press SPACE to switch",
+ Render(fmt.Sprintf(i18n.T("\nCurrently viewing: %s | Press SPACE to switch"),
func() string {
if m.Network.RoutesTable.Focused() {
- return "Routing Table"
+ return i18n.T("Routing Table")
}
- return "DNS Servers"
+ return i18n.T("DNS Servers")
}()))
content.WriteString(footer)
diff --git a/widgets/render-reporting.go b/widgets/render-reporting.go
index 348867c..4ea94c6 100644
--- a/widgets/render-reporting.go
+++ b/widgets/render-reporting.go
@@ -2,6 +2,7 @@ package widgets
import (
"fmt"
+ "github.com/Server-Pulse/server-pulse/i18n"
"strings"
model "github.com/Server-Pulse/server-pulse/widgets/model"
@@ -26,7 +27,7 @@ func (m Model) renderReporting() string {
Foreground(lipgloss.Color("241")).
Italic(true)
- content.WriteString(instructions.Render("Press 'g' to generate a new report, 'l' to load saved reports"))
+ content.WriteString(instructions.Render(i18n.T("Press 'g' to generate a new report, 'l' to load saved reports")))
content.WriteString("\n\n")
// Report status
@@ -59,7 +60,7 @@ func (m Model) renderReportMenu() string {
Foreground(lipgloss.Color("39")).
Italic(true)
- content.WriteString(infoStyle.Render(fmt.Sprintf("Last report generated: %s",
+ content.WriteString(infoStyle.Render(fmt.Sprintf(i18n.T("Last report generated: %s"),
m.Reporting.LastGenerated.Format("2006-01-02 15:04:05"))))
content.WriteString("\n\n")
}
@@ -69,15 +70,15 @@ func (m Model) renderReportMenu() string {
Foreground(lipgloss.Color("228")).
Bold(true)
- content.WriteString(actionStyle.Render("Available Actions:"))
+ content.WriteString(actionStyle.Render(i18n.T("Available Actions:")))
content.WriteString("\n\n")
actions := []struct {
key string
description string
}{
- {"g", "Generate new system health report"},
- {"l", "Load and view saved reports"},
+ {"g", i18n.T("Generate new system health report")},
+ {"l", i18n.T("Load and view saved reports")},
}
for _, action := range actions {
@@ -97,7 +98,7 @@ func (m Model) renderSavedReports() string {
Foreground(lipgloss.Color("228")).
Bold(true)
- content.WriteString(actionStyle.Render("Saved Reports:"))
+ content.WriteString(actionStyle.Render(i18n.T("Saved Reports:")))
content.WriteString("\n\n")
m.Reporting.RefreshSavedReports()
@@ -122,11 +123,11 @@ func (m Model) renderSavedReports() string {
}
content.WriteString("\n")
- content.WriteString(instructionsStyle().Render("Press ENTER to view selected report, 'd' to delete, 'b' to go back"))
+ content.WriteString(instructionsStyle().Render(i18n.T("Press ENTER to view selected report, 'd' to delete, 'b' to go back")))
} else {
- content.WriteString(instructionsStyle().Render("No saved reports found. Generate a report first."))
+ content.WriteString(instructionsStyle().Render(i18n.T("No saved reports found. Generate a report first.")))
content.WriteString("\n\n")
- content.WriteString(instructionsStyle().Render("Press 'b' to go back"))
+ content.WriteString(instructionsStyle().Render(i18n.T("Press 'b' to go back")))
}
return content.String()
@@ -143,7 +144,7 @@ func (m Model) renderGeneratingReport() string {
content.WriteString(spinnerStyle.Render("🔄 Generating System Health Report..."))
content.WriteString("\n\n")
- content.WriteString(instructionsStyle().Render("Please wait while we collect system data..."))
+ content.WriteString(instructionsStyle().Render(i18n.T("Please wait while we collect system data...")))
content.WriteString("\n\n")
// Show what's being collected
@@ -174,7 +175,7 @@ func (m Model) renderSavingReport() string {
content.WriteString(savingStyle.Render("💾 Saving Report..."))
content.WriteString("\n\n")
- content.WriteString(instructionsStyle().Render("Saving the current report to file..."))
+ content.WriteString(instructionsStyle().Render(i18n.T("Saving the current report to file...")))
return content.String()
}
@@ -196,7 +197,7 @@ func (m Model) renderViewingReport() string {
Foreground(lipgloss.Color("228")).
Italic(true)
- content.WriteString(navStyle.Render("Use arrow keys to scroll, 'q' to return to report menu, 's' to save"))
+ content.WriteString(navStyle.Render(i18n.T("Use arrow keys to scroll, 'q' to return to report menu, 's' to save")))
content.WriteString("\n\n")
// Report content in viewport
@@ -208,7 +209,7 @@ func (m Model) renderViewingReport() string {
} else {
content.WriteString(lipgloss.NewStyle().
Foreground(lipgloss.Color("196")).
- Render("No report content available. Generate a report first."))
+ Render(i18n.T("No report content available. Generate a report first.")))
}
return content.String()
diff --git a/widgets/utils.go b/widgets/utils.go
index 6bcafac..b1e275a 100644
--- a/widgets/utils.go
+++ b/widgets/utils.go
@@ -2,6 +2,7 @@ package widgets
import (
"fmt"
+ "github.com/Server-Pulse/server-pulse/i18n"
"math/rand"
"os/exec"
@@ -60,10 +61,10 @@ func canRunSudo() bool {
// getAdminRequiredChecks returns the list of diagnostic checks that require admin privileges
func (m Model) getAdminRequiredChecks() []string {
return []string{
- "Open Ports",
- "SSH Root Login",
- "Firewall Status",
- "System Updates",
+ i18n.T("Open Ports"),
+ i18n.T("SSH Root Login"),
+ i18n.T("Firewall Status"),
+ i18n.T("System Updates"),
}
}
diff --git a/widgets/vars/vars.go b/widgets/vars/vars.go
index a8b5450..ae09187 100644
--- a/widgets/vars/vars.go
+++ b/widgets/vars/vars.go
@@ -1,6 +1,7 @@
package vars
import (
+ "github.com/Server-Pulse/server-pulse/i18n"
model "github.com/Server-Pulse/server-pulse/widgets/model"
"github.com/charmbracelet/bubbles/spinner"
)
@@ -21,17 +22,16 @@ const (
ContainerViewConfirmation
)
+// English source labels. Always go through Localized* so language switching works.
var (
- dashboard = []string{"Monitor", "Diagnostic", "Network", "Reporting"}
- monitor = []string{"System", "Process", "Containers"}
- Menu = model.Menu{
- DashBoard: dashboard,
- Monitor: monitor,
- }
- NetworkNav = []string{"Interface", "Connectivity", "Configuration", "Protocol Analysis"}
- DiagnosticNav = []string{"Security Checks", "Performances", "Logs"}
+ dashboardEN = []string{"Monitor", "Diagnostic", "Network", "Reporting"}
+ monitorEN = []string{"System", "Process", "Containers"}
+
+ NetworkNavEN = []string{"Interface", "Connectivity", "Configuration", "Protocol Analysis"}
+ DiagnosticNavEN = []string{"Security Checks", "Performances", "Logs"}
+ ContainerTabsEN = []string{"General", "CPU", "MEM", "NET", "ENV"}
- ContainerMenuItems = []model.ContainerMenuItem{
+ containerMenuItemsEN = []model.ContainerMenuItem{
{Key: "o", Label: "Open detailed view", Description: "View detailed container information", Action: "open_single"},
{Key: "l", Label: "View logs", Description: "Show container logs", Action: "logs"},
{Key: "r", Label: "Restart", Description: "Restart container", Action: "restart"},
@@ -40,12 +40,14 @@ var (
{Key: "s", Label: "Stop/Start", Description: "Toggle container state", Action: "toggle_start"},
{Key: "p", Label: "Pause/Resume", Description: "Toggle pause state", Action: "toggle_pause"},
{Key: "e", Label: "Exec shell", Description: "Open interactive shell", Action: "exec"},
- // {Key: "t", Label: "Top/Stats", Description: "View real-time metrics", Action: "stats"},
- // {Key: "i", Label: "Inspect", Description: "Show container configuration", Action: "inspect"},
- // {Key: "c", Label: "Commit", Description: "Create image from container", Action: "commit"},
}
- ContainerTabs = []string{"General", "CPU", "MEM", "NET", "ENV"} // disk remove
+ // Back-compat aliases (English). Prefer Localized* at runtime.
+ NetworkNav = NetworkNavEN
+ DiagnosticNav = DiagnosticNavEN
+ ContainerTabs = ContainerTabsEN
+ ContainerMenuItems = containerMenuItemsEN
+ Menu = model.Menu{DashBoard: dashboardEN, Monitor: monitorEN}
Spinners = []spinner.Spinner{
spinner.Line,
@@ -59,3 +61,39 @@ var (
spinner.Monkey,
}
)
+
+func LocalizedMenu() model.Menu {
+ return model.Menu{
+ DashBoard: i18n.Ts(dashboardEN...),
+ Monitor: i18n.Ts(monitorEN...),
+ }
+}
+
+func LocalizedNetworkNav() []string {
+ return i18n.Ts(NetworkNavEN...)
+}
+
+func LocalizedDiagnosticNav() []string {
+ return i18n.Ts(DiagnosticNavEN...)
+}
+
+func LocalizedContainerTabs() []string {
+ return i18n.Ts(ContainerTabsEN...)
+}
+
+func LocalizedContainerMenuItems() []model.ContainerMenuItem {
+ items := make([]model.ContainerMenuItem, len(containerMenuItemsEN))
+ for i, it := range containerMenuItemsEN {
+ items[i] = model.ContainerMenuItem{
+ Key: it.Key,
+ Label: i18n.T(it.Label),
+ Description: i18n.T(it.Description),
+ Action: it.Action,
+ }
+ }
+ return items
+}
+
+func LocalizedPerformanceNav() []string {
+ return i18n.Ts("System Health", "I/O", "CPU", "Memory")
+}
diff --git a/widgets/view.go b/widgets/view.go
index d8cabc4..b654903 100644
--- a/widgets/view.go
+++ b/widgets/view.go
@@ -2,6 +2,7 @@ package widgets
import (
"fmt"
+ "github.com/Server-Pulse/server-pulse/i18n"
v "github.com/Server-Pulse/server-pulse/widgets/vars"
@@ -17,7 +18,7 @@ func (m Model) View() string {
return lipgloss.NewStyle().
Foreground(lipgloss.Color("196")).
Bold(true).
- Render("Terminal too small!\nMinimum size: (min: 80x20)\nCurrent: " +
+ Render(i18n.T("Terminal too small!\nMinimum size: (min: 80x20)\nCurrent: ") +
fmt.Sprintf("%dx%d", m.Ui.Width, m.Ui.Height))
}
diff --git a/widgets/widgets.go b/widgets/widgets.go
index 8c7f508..2ffbeb4 100644
--- a/widgets/widgets.go
+++ b/widgets/widgets.go
@@ -2,6 +2,7 @@ package widgets
import (
"fmt"
+ "github.com/Server-Pulse/server-pulse/i18n"
"sort"
"strings"
@@ -175,14 +176,14 @@ func (m *Model) updateNetworkTable() tea.Cmd {
var rows []table.Row
for _, iface := range m.Network.NetworkResource.Interfaces {
- statusText := "DOWN"
+ statusText := i18n.T("DOWN")
if iface.Status == "up" {
statusText = "UP"
}
ips := strings.Join(iface.IPs, ", ")
if ips == "" {
- ips = "No IP"
+ ips = i18n.T("No IP")
}
rows = append(rows, table.Row{