增加多语言支持
This commit is contained in:
@@ -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"
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 = "自定义"
|
||||
Reference in New Issue
Block a user