增加多语言支持

This commit is contained in:
2026-07-06 16:52:04 +08:00
parent 1e3db0a724
commit 1f1794fcc7
11 changed files with 511 additions and 83 deletions
+73
View File
@@ -0,0 +1,73 @@
WindowTitle = "LMVPN"
StatusLabel = "Status"
ProfileLabel = "Profile"
StateDisconnected = "Disconnected"
StateConnecting = "Connecting..."
StateConnected = "Connected"
StateReconnecting = "Reconnecting..."
StateError = "Error"
BtnConnect = "Connect"
BtnDisconnect = "Disconnect"
BtnAddProfile = "Add Profile"
BtnEdit = "Edit"
BtnDelete = "Delete"
BtnSave = "Save"
BtnCancel = "Cancel"
BtnOK = "OK"
IpLabel = "IP: {{.ip}}"
IpNone = "IP: —"
UptimeLabel = "Uptime: {{.uptime}}"
UptimeNone = "Uptime: —"
RxLabel = "↓ {{.bytes}}"
TxLabel = "↑ {{.bytes}}"
RxZero = "↓ 0 B"
TxZero = "↑ 0 B"
DlgNoProfileTitle = "No Profile"
DlgNoProfileEditMsg = "Select a profile to edit."
DlgNoProfileConnectMsg = "Please select or create a profile first."
DlgDeleteProfileTitle = "Delete Profile"
DlgDeleteProfileMsg = 'Delete profile "{{.name}}" and its stored credentials?'
DlgProfileTitle = "Profile"
DlgValidationTitle = "Validation"
DlgValidationMsg = "Name, Server URL, and Username are required."
DlgDaemonError = "Daemon Error"
DlgCredentialError = "Credential Error"
DlgCredentialErrorMsg = "No password stored for this profile. Edit the profile to set it."
DlgIPCError = "IPC Error"
DlgVPNError = "VPN Error"
DlgSaveError = "Save Error"
DlgKeychainError = "Keychain Error"
DlgError = "Error"
TrayShowWindow = "Show Window"
TrayConnect = "Connect"
TrayDisconnect = "Disconnect"
TrayLanguage = "Language"
TrayLanguageAuto = "Auto"
TrayQuit = "Quit"
FieldName = "Name"
FieldServerURL = "Server URL"
FieldUsername = "Username"
FieldPassword = "Password"
FieldAuthMode = "Auth Mode"
FieldRoutingMode = "Routing Mode"
FieldCustomCIDRs = "Custom CIDRs (comma-separated)"
FieldMTUOverride = "MTU Override"
PlaceholderCIDRs = "10.0.0.0/8, 172.16.0.0/12"
PlaceholderMTU = "0 = use server MTU"
PlaceholderPasswordUnchanged = "(unchanged)"
AuthModeBoth = "Both (JWT + Password)"
AuthModeJWT = "JWT"
AuthModePassword = "Password"
RoutingModeFull = "Full Tunnel"
RoutingModeSplit = "Split Tunnel"
RoutingModeCustom = "Custom"
+125
View File
@@ -0,0 +1,125 @@
// Package i18n provides internationalization support for the LMVPN
// UI. It loads translation catalogs (embedded TOML files) via go-i18n
// and exposes a simple T() lookup helper. The active language is
// chosen from the user's saved preference or, when unset, detected
// from the operating system locale.
package i18n
import (
"embed"
"fmt"
"strings"
"sync"
"github.com/BurntSushi/toml"
"github.com/jeandeaual/go-locale"
"github.com/nicksnyder/go-i18n/v2/i18n"
"golang.org/x/text/language"
)
//go:embed en.toml zh-Hans.toml
var localeFS embed.FS
// Supported language identifiers. LangAuto means "detect from the OS".
const (
LangAuto = "auto"
LangEn = "en"
LangZhHans = "zh-Hans"
)
var (
mu sync.RWMutex
bundle *i18n.Bundle
localizer *i18n.Localizer
currentLang string
)
// Init loads the embedded catalogs and activates the given language.
// An empty string or LangAuto triggers OS locale detection.
func Init(lang string) error {
bundle = i18n.NewBundle(language.English)
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
for _, name := range []string{"en.toml", "zh-Hans.toml"} {
data, err := localeFS.ReadFile(name)
if err != nil {
return fmt.Errorf("i18n: read %s: %w", name, err)
}
if _, err := bundle.ParseMessageFileBytes(data, name); err != nil {
return fmt.Errorf("i18n: parse %s: %w", name, err)
}
}
resolved := resolveLanguage(lang)
localizer = i18n.NewLocalizer(bundle, resolved)
currentLang = resolved
return nil
}
// SetLanguage switches the active language at runtime. The caller is
// responsible for rebuilding any cached UI strings afterwards.
func SetLanguage(lang string) {
mu.Lock()
defer mu.Unlock()
resolved := resolveLanguage(lang)
localizer = i18n.NewLocalizer(bundle, resolved)
currentLang = resolved
}
// CurrentLanguage returns the active language tag (e.g. "en", "zh-Hans").
func CurrentLanguage() string {
mu.RLock()
defer mu.RUnlock()
return currentLang
}
// T translates a message identified by key. An optional map is used as
// template data (e.g. {"name": "work"}). If the bundle is not yet
// initialised or the key is missing, the key itself is returned.
func T(key string, data ...interface{}) string {
mu.RLock()
loc := localizer
mu.RUnlock()
if loc == nil {
return key
}
cfg := &i18n.LocalizeConfig{MessageID: key}
if len(data) > 0 {
if m, ok := data[0].(map[string]interface{}); ok {
cfg.TemplateData = m
}
}
msg, err := loc.Localize(cfg)
if err != nil || msg == "" {
return key
}
return msg
}
// DetectSystemLanguage inspects the OS locale list and returns the
// best supported match. Any Chinese variant maps to zh-Hans; everything
// else falls back to en.
func DetectSystemLanguage() string {
tags, err := locale.GetLocales()
if err == nil {
for _, t := range tags {
if strings.HasPrefix(strings.ToLower(t), "zh") {
return LangZhHans
}
}
}
// Fall back to the single-locale API.
tag, err := locale.GetLocale()
if err == nil && strings.HasPrefix(strings.ToLower(tag), "zh") {
return LangZhHans
}
return LangEn
}
// resolveLanguage maps LangAuto / "" to a concrete language tag.
func resolveLanguage(lang string) string {
if lang == "" || lang == LangAuto {
return DetectSystemLanguage()
}
return lang
}
+73
View File
@@ -0,0 +1,73 @@
WindowTitle = "LMVPN"
StatusLabel = "状态"
ProfileLabel = "配置"
StateDisconnected = "已断开"
StateConnecting = "连接中..."
StateConnected = "已连接"
StateReconnecting = "重新连接中..."
StateError = "错误"
BtnConnect = "连接"
BtnDisconnect = "断开连接"
BtnAddProfile = "添加配置"
BtnEdit = "编辑"
BtnDelete = "删除"
BtnSave = "保存"
BtnCancel = "取消"
BtnOK = "确定"
IpLabel = "IP: {{.ip}}"
IpNone = "IP: —"
UptimeLabel = "运行时长: {{.uptime}}"
UptimeNone = "运行时长: —"
RxLabel = "↓ {{.bytes}}"
TxLabel = "↑ {{.bytes}}"
RxZero = "↓ 0 B"
TxZero = "↑ 0 B"
DlgNoProfileTitle = "无配置"
DlgNoProfileEditMsg = "请选择要编辑的配置。"
DlgNoProfileConnectMsg = "请先选择或创建一个配置。"
DlgDeleteProfileTitle = "删除配置"
DlgDeleteProfileMsg = '删除配置"{{.name}}"及其存储的凭据?'
DlgProfileTitle = "配置"
DlgValidationTitle = "验证"
DlgValidationMsg = "名称、服务器地址和用户名为必填项。"
DlgDaemonError = "守护进程错误"
DlgCredentialError = "凭据错误"
DlgCredentialErrorMsg = "此配置未存储密码。请编辑配置以设置密码。"
DlgIPCError = "IPC 错误"
DlgVPNError = "VPN 错误"
DlgSaveError = "保存错误"
DlgKeychainError = "钥匙串错误"
DlgError = "错误"
TrayShowWindow = "显示窗口"
TrayConnect = "连接"
TrayDisconnect = "断开连接"
TrayLanguage = "语言"
TrayLanguageAuto = "自动"
TrayQuit = "退出"
FieldName = "名称"
FieldServerURL = "服务器地址"
FieldUsername = "用户名"
FieldPassword = "密码"
FieldAuthMode = "认证方式"
FieldRoutingMode = "路由模式"
FieldCustomCIDRs = "自定义 CIDR(逗号分隔)"
FieldMTUOverride = "MTU 覆盖"
PlaceholderCIDRs = "10.0.0.0/8, 172.16.0.0/12"
PlaceholderMTU = "0 = 使用服务器 MTU"
PlaceholderPasswordUnchanged = "(未更改)"
AuthModeBoth = "全部(JWT + 密码)"
AuthModeJWT = "JWT"
AuthModePassword = "密码"
RoutingModeFull = "全隧道"
RoutingModeSplit = "分离隧道"
RoutingModeCustom = "自定义"