Files
2026-07-19 17:35:53 +08:00

196 lines
5.3 KiB
Go

package locale
import (
"context"
"strings"
"sync/atomic"
"golang.org/x/text/language"
)
var (
localeRegistry = map[string]*Locale{
"en": defaultLocale,
}
localeMatcher = language.NewMatcher(
[]language.Tag{
language.English,
language.SimplifiedChinese,
language.TraditionalChinese,
language.Persian,
language.Russian,
},
language.PreferSameScript(true),
)
localeNames = []string{"en", "zh-Hans", "zh-Hant", "fa", "ru"}
current atomic.Pointer[Locale]
)
type Locale struct {
Locale string
DeprecatedMessage string
DeprecatedMessageNoLink string
InsecureFeatureMessage string
ExternalPathFeature string
TailscaleInitializing string
TailscaleInUse string
TailscaleNeedsLogin string
TailscaleNeedsApproval string
TailscaleStopped string
TailscaleStarting string
TailscaleRunning string
VPNConnecting string
VPNAuthentication string
VPNConnected string
VPNError string
Unknown string
}
var defaultLocale = &Locale{
Locale: "en",
DeprecatedMessage: "%s is deprecated in sing-box %s and will be removed in sing-box %s. Please check the documentation for migration.",
DeprecatedMessageNoLink: "%s is deprecated in sing-box %s and will be removed in sing-box %s.",
InsecureFeatureMessage: "%s is considered insecure in the graphical client for sing-box on %s. Enable Insecure Mode in `Settings - Core - Insecure Mode` to use it.",
ExternalPathFeature: "Access to %s (outside of the working directory) is considered insecure in the graphical client for sing-box on %s. Enable Insecure Mode in `Settings - Core - Insecure Mode` to use it.",
TailscaleInitializing: "Initializing",
TailscaleInUse: "In use by another user",
TailscaleNeedsLogin: "Needs login",
TailscaleNeedsApproval: "Needs approval",
TailscaleStopped: "Stopped",
TailscaleStarting: "Starting",
TailscaleRunning: "Running",
VPNConnecting: "Connecting",
VPNAuthentication: "Authentication required",
VPNConnected: "Connected",
VPNError: "Error",
Unknown: "Unknown",
}
type contextKey struct{}
func init() {
current.Store(defaultLocale)
}
func Current() *Locale {
return current.Load()
}
func ContextWithLocale(ctx context.Context, localeID string) (context.Context, bool) {
selectedLocale, loaded := selectLocale(localeID)
if !loaded {
return ctx, false
}
return context.WithValue(ctx, contextKey{}, selectedLocale), true
}
func FromContext(ctx context.Context) *Locale {
selectedLocale, loaded := ctx.Value(contextKey{}).(*Locale)
if loaded {
return selectedLocale
}
return Current()
}
func selectLocale(localeID string) (*Locale, bool) {
localeName, loaded := match(localeID)
if !loaded {
return nil, false
}
selectedLocale, loaded := localeRegistry[localeName]
return selectedLocale, loaded
}
func Set(localeID string) bool {
selectedLocale, loaded := selectLocale(localeID)
if !loaded {
return false
}
current.Store(selectedLocale)
return true
}
func match(localeID string) (string, bool) {
localeEntries := strings.Split(localeID, ",")
for i, localeEntry := range localeEntries {
languageID, options, hasOptions := strings.Cut(localeEntry, ";")
languageID, _, _ = strings.Cut(strings.TrimSpace(languageID), "@")
languageID = strings.ReplaceAll(languageID, "_", "-")
if !hasOptions {
languageID, _, _ = strings.Cut(languageID, ".")
}
switch {
case strings.EqualFold(languageID, "C"), strings.EqualFold(languageID, "POSIX"):
languageID = "en"
case strings.EqualFold(languageID, "zh-CHS"):
languageID = "zh-Hans"
case strings.EqualFold(languageID, "zh-CHT"):
languageID = "zh-Hant"
}
localeEntries[i] = languageID
if hasOptions {
localeEntries[i] += ";" + options
}
}
localeID = strings.Join(localeEntries, ",")
tags, _, err := language.ParseAcceptLanguage(localeID)
if err != nil || len(tags) == 0 {
return "", false
}
for i, tag := range tags {
base, script, region := tag.Raw()
if base.String() != "zh" && base.String() != "cmn" {
continue
}
if script.String() == "Hans" || script.String() == "Hant" {
continue
}
languageID := "zh-Hans"
if region.String() == "TW" || region.String() == "HK" || region.String() == "MO" {
languageID = "zh-Hant"
}
if region.String() != "ZZ" {
languageID += "-" + region.String()
}
tags[i] = language.MustParse(languageID)
}
_, localeIndex, _ := localeMatcher.Match(tags...)
return localeNames[localeIndex], true
}
func (l *Locale) TailscaleStateText(state string) string {
switch state {
case "NoState":
return l.TailscaleInitializing
case "InUseOtherUser":
return l.TailscaleInUse
case "NeedsLogin":
return l.TailscaleNeedsLogin
case "NeedsMachineAuth":
return l.TailscaleNeedsApproval
case "Stopped":
return l.TailscaleStopped
case "Starting":
return l.TailscaleStarting
case "Running":
return l.TailscaleRunning
default:
return l.Unknown
}
}
func (l *Locale) VPNStateText(state string) string {
switch state {
case "connecting":
return l.VPNConnecting
case "auth-pending":
return l.VPNAuthentication
case "connected":
return l.VPNConnected
case "error":
return l.VPNError
default:
return l.Unknown
}
}