refactor: remove LocaleSwitcher component and update locale handling

- Removed LocaleSwitcher from NavUser, SiteHeader, and WorkbenchHeader components.
- Updated tests to reflect the removal of LocaleSwitcher.
- Changed default locale from "en-US" to "zh-CN" in i18n configuration.
- Refactored locale resolution logic to read configured locale without relying on browser language detection.
- Updated AgentDesk SDK to support language configuration from public API.
- Cleaned up unused auth options fetching in the auth API.
This commit is contained in:
mlogclub
2026-06-26 14:46:01 +08:00
parent 12d188fc7e
commit f827a7471c
32 changed files with 215 additions and 268 deletions
+2
View File
@@ -4,6 +4,7 @@ import (
"agent-desk/internal/ai/rag/vectordb"
"agent-desk/internal/oidcclient"
"agent-desk/internal/pkg/config"
"agent-desk/internal/pkg/i18nx"
"agent-desk/internal/pkg/logx"
"agent-desk/internal/services/cronx"
"agent-desk/internal/wxwork"
@@ -20,6 +21,7 @@ func Init(configPath string) error {
return err
}
config.SetCurrent(cfg)
i18nx.SetDefaultLocale(cfg.LanguageOrDefault())
logx.Init(logx.Config{
Level: cfg.Logger.Level,
-1
View File
@@ -11,7 +11,6 @@ import (
func registerApiAuthRoutes(group *gin.RouterGroup) {
group.POST("/login", api.Login)
group.POST("/logout", api.Logout)
group.GET("/options", api.AuthOptions)
group.GET("/profile", api.Profile)
group.GET("/wxwork_callback", api.WxWorkCallback)
group.POST("/wxwork_exchange", api.WxWorkExchange)
+2
View File
@@ -26,6 +26,7 @@ import (
func NewServer() (*gin.Engine, error) {
cfg := config.Current()
i18nx.SetDefaultLocale(cfg.LanguageOrDefault())
gin.SetMode(gin.ReleaseMode)
printBanner()
@@ -156,6 +157,7 @@ func addRouter(app *gin.Engine) {
apiGroup := app.Group("/api")
apiGroup.GET("/health", api.Health)
apiGroup.GET("/config", api.PublicConfig)
registerApiAuthRoutes(apiGroup.Group("/auth"))
registerApiChannelRoutes(apiGroup.Group("/channel"))
registerApiCustomerRoutes(apiGroup.Group("/customer"))
+33 -4
View File
@@ -32,6 +32,7 @@ func TestNewServerRegistersGinRoutes(t *testing.T) {
expected := []string{
http.MethodPost + " /api/auth/login",
http.MethodGet + " /api/config",
http.MethodGet + " /api/health",
http.MethodGet + " /api/auth/oidc_login",
http.MethodGet + " /api/auth/oidc_callback",
@@ -93,8 +94,9 @@ func TestNewServerHealthEndpointIsPublic(t *testing.T) {
}
}
func TestNewServerExposesPublicAuthOptions(t *testing.T) {
func TestNewServerExposesPublicConfig(t *testing.T) {
config.SetCurrent(&config.Config{
Language: "zh-CN",
Storage: config.StorageConfig{
Local: config.LocalStorageConfig{
Root: "storage",
@@ -116,7 +118,7 @@ func TestNewServerExposesPublicAuthOptions(t *testing.T) {
}
rec := httptest.NewRecorder()
app.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/auth/options", nil))
app.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/config", nil))
if rec.Code != http.StatusOK {
t.Fatalf("status=%d want %d", rec.Code, http.StatusOK)
@@ -125,8 +127,9 @@ func TestNewServerExposesPublicAuthOptions(t *testing.T) {
var body struct {
Success bool `json:"success"`
Data struct {
WxWorkEnabled bool `json:"wxworkEnabled"`
OIDCEnabled bool `json:"oidcEnabled"`
Language string `json:"language"`
WxWorkEnabled bool `json:"wxworkEnabled"`
OIDCEnabled bool `json:"oidcEnabled"`
} `json:"data"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
@@ -135,6 +138,9 @@ func TestNewServerExposesPublicAuthOptions(t *testing.T) {
if !body.Success {
t.Fatalf("success=false, body=%s", rec.Body.String())
}
if body.Data.Language != "zh-CN" {
t.Fatalf("language=%q want zh-CN", body.Data.Language)
}
if !body.Data.WxWorkEnabled {
t.Fatalf("wxworkEnabled=false want true")
}
@@ -146,6 +152,29 @@ func TestNewServerExposesPublicAuthOptions(t *testing.T) {
}
}
func TestNewServerDoesNotExposeLegacyAuthOptions(t *testing.T) {
config.SetCurrent(&config.Config{
Storage: config.StorageConfig{
Local: config.LocalStorageConfig{
Root: "storage",
BaseURL: "/storage",
},
},
})
app, err := NewServer()
if err != nil {
t.Fatalf("NewServer() error = %v", err)
}
rec := httptest.NewRecorder()
app.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/auth/options", nil))
if rec.Code != http.StatusNotFound {
t.Fatalf("status=%d want %d, body=%s", rec.Code, http.StatusNotFound, rec.Body.String())
}
}
func TestNewServerSeparatesAPIStaticAndSPA(t *testing.T) {
config.SetCurrent(&config.Config{
Storage: config.StorageConfig{
+3 -2
View File
@@ -30,9 +30,10 @@ func Login(ctx *gin.Context) {
httpx.WriteJSON(ctx, ret)
}
func AuthOptions(ctx *gin.Context) {
func PublicConfig(ctx *gin.Context) {
cfg := config.Current()
httpx.WriteJSON(ctx, &response.AuthOptionsResponse{
httpx.WriteJSON(ctx, &response.PublicConfigResponse{
Language: cfg.LanguageOrDefault(),
WxWorkEnabled: cfg.WxWork.Enabled,
OIDCEnabled: cfg.OIDC.Enabled,
})
+12
View File
@@ -9,6 +9,7 @@ import (
)
type Config struct {
Language string `yaml:"language"`
Server ServerConfig `yaml:"server"`
DB DBConfig `yaml:"db"`
Logger LoggerConfig `yaml:"logger"`
@@ -21,6 +22,17 @@ type Config struct {
CustomerSession CustomerSessionConfig `yaml:"customerSession"`
}
func (c Config) LanguageOrDefault() string {
switch strings.ToLower(strings.TrimSpace(c.Language)) {
case "zh", "zh-cn", "zh_cn", "zh-hans":
return "zh-CN"
case "en", "en-us", "en_us":
return "en-US"
default:
return "zh-CN"
}
}
type WxWorkNotifyConfig struct {
Enabled bool `yaml:"enabled"`
ToUsers []int64 `yaml:"toUsers"`
+4 -3
View File
@@ -19,7 +19,8 @@ type LoginResponse struct {
Roles []string `json:"roles"`
}
type AuthOptionsResponse struct {
WxWorkEnabled bool `json:"wxworkEnabled"`
OIDCEnabled bool `json:"oidcEnabled"`
type PublicConfigResponse struct {
Language string `json:"language"`
WxWorkEnabled bool `json:"wxworkEnabled"`
OIDCEnabled bool `json:"oidcEnabled"`
}
+1 -1
View File
@@ -86,7 +86,7 @@ func (e *I18nError) Error() string {
if e == nil {
return ""
}
return e.Message(i18nx.LocaleZhCN)
return e.Message(i18nx.DefaultLocale)
}
func (e *I18nError) Unwrap() error {
+1 -1
View File
@@ -20,7 +20,7 @@ func (e *Error) Error() string {
if e == nil {
return ""
}
return e.Message(LocaleZhCN)
return e.Message(DefaultLocale)
}
func (e *Error) Message(locale string) string {
+16 -21
View File
@@ -9,27 +9,24 @@ import (
)
func TestNormalizeLocale(t *testing.T) {
t.Parallel()
tests := []struct {
name string
in string
want string
}{
{name: "default for blank", in: "", want: LocaleEnUS},
{name: "default for blank", in: "", want: LocaleZhCN},
{name: "exact chinese", in: "zh-CN", want: LocaleZhCN},
{name: "underscore chinese", in: "zh_CN", want: LocaleZhCN},
{name: "short chinese", in: "zh", want: LocaleZhCN},
{name: "exact english", in: "en-US", want: LocaleEnUS},
{name: "underscore english", in: "en_US", want: LocaleEnUS},
{name: "short english", in: "en", want: LocaleEnUS},
{name: "unsupported falls back", in: "fr-FR", want: LocaleEnUS},
{name: "unsupported falls back", in: "fr-FR", want: LocaleZhCN},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := NormalizeLocale(tt.in); got != tt.want {
t.Fatalf("NormalizeLocale(%q) = %q, want %q", tt.in, got, tt.want)
}
@@ -37,36 +34,33 @@ func TestNormalizeLocale(t *testing.T) {
}
}
func TestResolveLocaleFromHeaders(t *testing.T) {
t.Parallel()
func TestResolveLocaleUsesDefaultLocale(t *testing.T) {
SetDefaultLocale(LocaleZhCN)
req := httptest.NewRequest(http.MethodGet, "/api/dashboard/user/list", nil)
req.Header.Set("Accept-Language", "fr-FR, en-US;q=0.9, zh-CN;q=0.8")
if got := ResolveRequestLocale(req); got != LocaleEnUS {
t.Fatalf("ResolveRequestLocale() = %q, want %q", got, LocaleEnUS)
if got := ResolveRequestLocale(req); got != LocaleZhCN {
t.Fatalf("ResolveRequestLocale() = %q, want %q", got, LocaleZhCN)
}
}
func TestResolveLocalePrefersXLocale(t *testing.T) {
t.Parallel()
func TestResolveLocaleIgnoresRequestLocaleHeaders(t *testing.T) {
SetDefaultLocale(LocaleZhCN)
req := httptest.NewRequest(http.MethodGet, "/api/dashboard/user/list", nil)
req.Header.Set("X-Locale", "en-US")
req.Header.Set("Accept-Language", "zh-CN")
if got := ResolveRequestLocale(req); got != LocaleEnUS {
t.Fatalf("ResolveRequestLocale() = %q, want %q", got, LocaleEnUS)
if got := ResolveRequestLocale(req); got != LocaleZhCN {
t.Fatalf("ResolveRequestLocale() = %q, want %q", got, LocaleZhCN)
}
}
func TestTranslateFallsBackToEnglish(t *testing.T) {
t.Parallel()
func TestTranslateUsesConfiguredDefaultForUnsupportedLocale(t *testing.T) {
SetDefaultLocale(LocaleZhCN)
if got := TLocale(LocaleEnUS, "error.auth.expired"); got != "Your session has expired. Please sign in again." {
t.Fatalf("english translation = %q", got)
}
if got := TLocale("fr-FR", "error.auth.expired"); got != "Your session has expired. Please sign in again." {
if got := TLocale("fr-FR", "error.auth.expired"); got != "未登录或登录已过期" {
t.Fatalf("fallback translation = %q", got)
}
}
@@ -143,6 +137,7 @@ func TestGetfFallsBackToKey(t *testing.T) {
}
func TestMiddlewareStoresLocale(t *testing.T) {
SetDefaultLocale(LocaleZhCN)
gin.SetMode(gin.TestMode)
router := gin.New()
router.Use(Middleware())
@@ -159,7 +154,7 @@ func TestMiddlewareStoresLocale(t *testing.T) {
if recorder.Code != http.StatusOK {
t.Fatalf("status = %d, want %d", recorder.Code, http.StatusOK)
}
if got := recorder.Body.String(); got != LocaleEnUS {
t.Fatalf("middleware locale = %q, want %q", got, LocaleEnUS)
if got := recorder.Body.String(); got != LocaleZhCN {
t.Fatalf("middleware locale = %q, want %q", got, LocaleZhCN)
}
}
+9 -45
View File
@@ -5,13 +5,11 @@ import (
"strings"
"github.com/gin-gonic/gin"
"golang.org/x/text/language"
)
const (
LocaleZhCN = "zh-CN"
LocaleEnUS = "en-US"
DefaultLocale = LocaleEnUS
LocaleZhCN = "zh-CN"
LocaleEnUS = "en-US"
)
var supportedLocales = map[string]string{
@@ -24,6 +22,12 @@ var supportedLocales = map[string]string{
"en_us": LocaleEnUS,
}
var DefaultLocale = LocaleZhCN
func SetDefaultLocale(locale string) {
DefaultLocale = NormalizeLocale(locale)
}
func NormalizeLocale(value string) string {
key := strings.ToLower(strings.TrimSpace(value))
if key == "" {
@@ -35,19 +39,7 @@ func NormalizeLocale(value string) string {
return DefaultLocale
}
func ResolveRequestLocale(req *http.Request) string {
if req == nil {
return DefaultLocale
}
if locale := normalizeSupportedLocale(req.Header.Get("X-Locale")); locale != "" {
return locale
}
if locale := resolveAcceptLanguage(req.Header.Get("Accept-Language")); locale != "" {
return locale
}
if locale := normalizeSupportedLocale(req.URL.Query().Get("locale")); locale != "" {
return locale
}
func ResolveRequestLocale(_ *http.Request) string {
return DefaultLocale
}
@@ -57,31 +49,3 @@ func Middleware() gin.HandlerFunc {
ctx.Next()
}
}
func normalizeSupportedLocale(value string) string {
key := strings.ToLower(strings.TrimSpace(value))
if key == "" {
return ""
}
if locale, ok := supportedLocales[key]; ok {
return locale
}
return ""
}
func resolveAcceptLanguage(value string) string {
tags, _, err := language.ParseAcceptLanguage(value)
if err != nil {
return ""
}
for _, tag := range tags {
if locale := normalizeSupportedLocale(tag.String()); locale != "" {
return locale
}
base, _ := tag.Base()
if locale := normalizeSupportedLocale(base.String()); locale != "" {
return locale
}
}
return ""
}