208 lines
7.3 KiB
Go
208 lines
7.3 KiB
Go
package i18n
|
|
|
|
import (
|
|
"errors"
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"net/http/httptest"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/QuantumNous/new-api/common"
|
|
"github.com/QuantumNous/new-api/constant"
|
|
"github.com/QuantumNous/new-api/relaykit/dto"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// The web console saves its own language codes (zhCN, zhTW, fr, ...) in the
|
|
// user setting; backend messages must follow them, and languages without a
|
|
// backend locale fall back to English.
|
|
func TestLanguageFromUserSetting(t *testing.T) {
|
|
require.NoError(t, Init())
|
|
for _, tc := range []struct {
|
|
saved string
|
|
want string
|
|
}{
|
|
{"zhCN", LangZhCN},
|
|
{"zhTW", LangZhTW},
|
|
{"zh-HK", LangZhTW},
|
|
{"zh_TW", LangZhTW},
|
|
{"zh_HK", LangZhTW},
|
|
{"zh-Hant-TW", LangZhTW},
|
|
{"zh", LangZhCN},
|
|
{"en", LangEn},
|
|
{"fr", LangEn},
|
|
{"ja", LangEn},
|
|
} {
|
|
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
|
c.Request = httptest.NewRequest("GET", "/v1/chat/completions", nil)
|
|
c.Set(string(constant.ContextKeyUserSetting), dto.UserSetting{Language: tc.saved})
|
|
assert.Equal(t, tc.want, StatedLang(c), tc.saved)
|
|
}
|
|
}
|
|
|
|
// TokenAuth puts the user's settings into the request. A user who saved no
|
|
// language must not be loaded again, which would cost a cache or database read
|
|
// on every relay request; a request without loaded settings still loads them.
|
|
func TestStatedLangLoadsTheUserOnlyWithoutLoadedSettings(t *testing.T) {
|
|
previous := userLangLoaderFunc
|
|
t.Cleanup(func() { userLangLoaderFunc = previous })
|
|
loads := 0
|
|
SetUserLangLoader(func(int) string {
|
|
loads++
|
|
return "zhTW"
|
|
})
|
|
|
|
relay, _ := gin.CreateTestContext(httptest.NewRecorder())
|
|
relay.Request = httptest.NewRequest("POST", "/v1/chat/completions", nil)
|
|
relay.Set("id", 7)
|
|
relay.Set(string(constant.ContextKeyUserSetting), dto.UserSetting{})
|
|
assert.Equal(t, "", StatedLang(relay))
|
|
assert.Zero(t, loads)
|
|
|
|
dashboard, _ := gin.CreateTestContext(httptest.NewRecorder())
|
|
dashboard.Request = httptest.NewRequest("GET", "/api/user/self", nil)
|
|
dashboard.Set("id", 7)
|
|
assert.Equal(t, LangZhTW, StatedLang(dashboard))
|
|
assert.Equal(t, 1, loads)
|
|
}
|
|
|
|
// A saved language or an Accept-Language header selects the language of a
|
|
// backend message. A reader who states neither gets DEFAULT_LANGUAGE, which is
|
|
// English when unset.
|
|
func TestBackendMessageLanguage(t *testing.T) {
|
|
require.NoError(t, Init())
|
|
t.Cleanup(func() { common.DefaultLanguage = "" })
|
|
quota := map[string]any{"Remaining": "$0.10"}
|
|
const english = "Insufficient user quota, remaining quota: $0.10"
|
|
const chinese = "用户额度不足, 剩余额度: $0.10"
|
|
for _, tc := range []struct {
|
|
name string
|
|
defaultLanguage string
|
|
saved string
|
|
acceptLanguage string
|
|
want string
|
|
}{
|
|
{"nothing stated", "", "", "", english},
|
|
{"no preference header", "", "", "*", english},
|
|
{"nothing stated with a Chinese default", "zh-CN", "", "", chinese},
|
|
{"nothing stated with an English default", "en", "", "", english},
|
|
{"header wins over the default", "zh-CN", "", "en-US,en;q=0.9", english},
|
|
{"header in a language without a locale", "zh-CN", "", "fr-FR", english},
|
|
{"saved language wins over the header", "", "zhTW", "en-US", "使用者額度不足,剩餘額度:$0.10"},
|
|
{"saved language", "zh-CN", "en", "", english},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
common.DefaultLanguage = tc.defaultLanguage
|
|
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
|
c.Request = httptest.NewRequest("POST", "/v1/chat/completions", nil)
|
|
if tc.acceptLanguage != "" {
|
|
c.Request.Header.Set("Accept-Language", tc.acceptLanguage)
|
|
}
|
|
if tc.saved != "" {
|
|
c.Set(string(constant.ContextKeyUserSetting), dto.UserSetting{Language: tc.saved})
|
|
}
|
|
assert.Equal(t, tc.want, T(c, MsgQuotaUserInsufficient, quota))
|
|
})
|
|
}
|
|
}
|
|
|
|
// A key missing from a locale, or a template that does not parse, reaches
|
|
// clients as the raw key.
|
|
func TestLocalesRenderEveryKey(t *testing.T) {
|
|
require.NoError(t, Init())
|
|
keysFile, err := parser.ParseFile(token.NewFileSet(), "keys.go", nil, parser.SkipObjectResolution)
|
|
require.NoError(t, err)
|
|
var declared []string
|
|
ast.Inspect(keysFile, func(node ast.Node) bool {
|
|
if spec, ok := node.(*ast.ValueSpec); ok {
|
|
for _, value := range spec.Values {
|
|
if literal, ok := value.(*ast.BasicLit); ok {
|
|
key, err := strconv.Unquote(literal.Value)
|
|
require.NoError(t, err)
|
|
declared = append(declared, key)
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
slices.Sort(declared)
|
|
|
|
keysByLang := make(map[string][]string)
|
|
for _, lang := range SupportedLanguages() {
|
|
data, err := localeFS.ReadFile("locales/" + lang + ".yaml")
|
|
require.NoError(t, err)
|
|
var messages map[string]string
|
|
require.NoError(t, yaml.Unmarshal(data, &messages))
|
|
for key := range messages {
|
|
keysByLang[lang] = append(keysByLang[lang], key)
|
|
}
|
|
slices.Sort(keysByLang[lang])
|
|
}
|
|
for _, lang := range SupportedLanguages() {
|
|
assert.Equal(t, declared, keysByLang[lang], lang)
|
|
for _, key := range keysByLang[lang] {
|
|
text := Translate(lang, key, map[string]any{})
|
|
assert.NotEqual(t, key, text, "%s %s", lang, key)
|
|
assert.False(t, strings.Contains(text, "{{"), "%s %s: %s", lang, key, text)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Web console responses carry the English source text as message_key plus its
|
|
// params, and the web console translates them. The backend sends the same body
|
|
// whatever language the request asks for.
|
|
func TestWebConsoleMessageResponse(t *testing.T) {
|
|
require.NoError(t, Init())
|
|
for _, tc := range []struct {
|
|
name string
|
|
err error
|
|
want string
|
|
}{
|
|
{
|
|
name: "params",
|
|
err: common.NewMessage("Channel {{name}} (#{{id}}) does not exist", map[string]any{"name": "{{id}}", "id": 7}),
|
|
want: `{"success":false,"message":"Channel {{id}} (#7) does not exist","message_key":"Channel {{name}} (#{{id}}) does not exist","message_params":{"id":7,"name":"{{id}}"}}`,
|
|
},
|
|
{
|
|
name: "no params",
|
|
err: common.NewMessage("Invalid parameters"),
|
|
want: `{"success":false,"message":"Invalid parameters","message_key":"Invalid parameters"}`,
|
|
},
|
|
{
|
|
name: "plain error",
|
|
err: errors.New("record not found"),
|
|
want: `{"success":false,"message":"record not found"}`,
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
recorder := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(recorder)
|
|
c.Request = httptest.NewRequest("GET", "/api/channel/7", nil)
|
|
c.Request.Header.Set("Accept-Language", "zh-CN")
|
|
common.ApiError(c, tc.err)
|
|
assert.JSONEq(t, tc.want, recorder.Body.String())
|
|
})
|
|
}
|
|
}
|
|
|
|
// Server log lines are English. DEFAULT_LANGUAGE=zh-CN prints the Chinese text of
|
|
// the lines that have one in common/log_text.zh-CN.json.
|
|
func TestServerLogLanguage(t *testing.T) {
|
|
t.Cleanup(func() { common.SetLogLanguage("") })
|
|
const line = "channel #%d has %d unfinished tasks"
|
|
assert.Equal(t, "channel #7 has 2 unfinished tasks", common.LogText(line, 7, 2))
|
|
common.SetLogLanguage("zh-CN")
|
|
assert.Equal(t, "渠道 #7 未完成的任务有: 2", common.LogText(line, 7, 2))
|
|
assert.Equal(t, "a line without a Chinese text: x", common.LogText("a line without a Chinese text: %s", "x"))
|
|
assert.Equal(t, "任务进度轮询开始", common.LogText("task progress polling started"))
|
|
common.SetLogLanguage("en")
|
|
assert.Equal(t, "channel #7 has 2 unfinished tasks", common.LogText(line, 7, 2))
|
|
}
|