Files
new-api-custom/i18n/i18n_test.go
T

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))
}