mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-09 01:46:46 +08:00
* feat(i18n): add zh-Hant locale to web and desktop Add Traditional Chinese (zh-Hant) as a supported locale with a full translation of the shared views bundles. Browser tags zh-TW/zh-HK/ zh-Hant now resolve to zh-Hant instead of collapsing to zh-Hans; the document lang is zh-TW, and docs/use-case links reuse the /zh pages. Co-Authored-By: Claude Opus 5.5 <[email protected]> (cherry picked from commit a6b3f6ba53a605c3e06fd6c4d30fd9df6fca8645) * feat(mobile): add zh-Hant locale Ship Traditional Chinese bundles for mobile and resolve zh-Hant/TW/HK/MO device locales to them instead of falling back to English. Co-Authored-By: Claude Opus 5.5 <[email protected]> (cherry picked from commit 04563c22662bdb735a9a6c04be784abd9f87da06) * feat(ui-lab): add zh-Hant locale Co-Authored-By: Claude Opus 5.5 <[email protected]> (cherry picked from commit 1351ea572ed5d885d8a29034d6c0317de58641ea) * feat(server): accept zh-Hant as user language Co-Authored-By: Claude Opus 5.5 <[email protected]> (cherry picked from commit 50dfe77a4c45452fc877c62497aba226d5aaa426) * fix(i18n): sync zh-Hant with upstream copy changes Upstream added conversation-starter, instruction-preview and board column strings, removed restated descriptions (MUL-8162) and renamed Qoder Cloud Agents. Mirror those key additions, removals and rewordings in zh-Hant so the locale parity test passes. Co-Authored-By: Claude Opus 5.5 <[email protected]> (cherry picked from commit 3a97dfc7e0bbf7568925f735c00fc69b6d94313d) * fix(i18n): add zh-Hant copy for member-scoped runtime visibility Mirror the runtime visibility "members" choice and the reworded revoke dialog in zh-Hant so the locale parity test keeps passing. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(i18n): replace Mainland terms in zh-Hant copy with Taiwan usage Several zh-Hant strings were converted character by character from zh-Hans and kept Mainland vocabulary, which reads wrong to Traditional Chinese users: 郵箱 → 信箱, 賬單/賬號 → 帳單/帳號, and similar. Values only; no keys added or removed. * fix(i18n): polish zh-Hant copy from review - 分割槽 → 分區 and 許可權 → 權限 (OpenCC phrase mis-conversions) - drop stray blank lines before zh-Hant workspace names Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Multica Eve <[email protected]> Co-authored-by: multica-agent <[email protected]>
199 lines
6.1 KiB
Go
199 lines
6.1 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/multica-ai/multica/server/internal/testutil"
|
|
)
|
|
|
|
func TestUpdateMeFrenchLanguageRoundTrip(t *testing.T) {
|
|
if testHandler == nil || testPool == nil {
|
|
t.Skip("database not available")
|
|
}
|
|
userID := dbfx.User(t, "French user", "[email protected]", testutil.Cols{"language": "en"})
|
|
var updated UserResponse
|
|
testutil.Call(t, testHandler.UpdateMe, newPatchMeRequest(userID, `{"language":"fr"}`)).Want(http.StatusOK).JSON(&updated)
|
|
if updated.Language == nil || *updated.Language != "fr" {
|
|
t.Fatalf("updated language = %v, want fr", updated.Language)
|
|
}
|
|
|
|
// A fresh request after reload must restore French, not the old preference.
|
|
get := newRequest(http.MethodGet, "/api/me", nil)
|
|
get.Header.Set("X-User-ID", userID)
|
|
var reloaded UserResponse
|
|
testutil.Call(t, testHandler.GetMe, get).Want(http.StatusOK).JSON(&reloaded)
|
|
if reloaded.Language == nil || *reloaded.Language != "fr" {
|
|
t.Fatalf("reloaded language = %v, want fr", reloaded.Language)
|
|
}
|
|
}
|
|
|
|
func TestUpdateMeTraditionalChineseLanguageRoundTrip(t *testing.T) {
|
|
if testHandler == nil || testPool == nil {
|
|
t.Skip("database not available")
|
|
}
|
|
userID := dbfx.User(t, "Traditional Chinese user", "[email protected]", testutil.Cols{"language": "en"})
|
|
var updated UserResponse
|
|
testutil.Call(t, testHandler.UpdateMe, newPatchMeRequest(userID, `{"language":"zh-Hant"}`)).Want(http.StatusOK).JSON(&updated)
|
|
if updated.Language == nil || *updated.Language != "zh-Hant" {
|
|
t.Fatalf("updated language = %v, want zh-Hant", updated.Language)
|
|
}
|
|
|
|
get := newRequest(http.MethodGet, "/api/me", nil)
|
|
get.Header.Set("X-User-ID", userID)
|
|
var reloaded UserResponse
|
|
testutil.Call(t, testHandler.GetMe, get).Want(http.StatusOK).JSON(&reloaded)
|
|
if reloaded.Language == nil || *reloaded.Language != "zh-Hant" {
|
|
t.Fatalf("reloaded language = %v, want zh-Hant", reloaded.Language)
|
|
}
|
|
}
|
|
|
|
func newLanguageTestUser(t *testing.T, email string) string {
|
|
t.Helper()
|
|
ctx := context.Background()
|
|
|
|
var userID string
|
|
if err := testPool.QueryRow(ctx,
|
|
`INSERT INTO "user" (name, email) VALUES ($1, $2) RETURNING id`,
|
|
"Language Test", email,
|
|
).Scan(&userID); err != nil {
|
|
t.Fatalf("insert test user: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
testPool.Exec(ctx, `DELETE FROM "user" WHERE id = $1`, userID)
|
|
})
|
|
return userID
|
|
}
|
|
|
|
func newPatchMeRequest(userID, body string) *http.Request {
|
|
req := httptest.NewRequest("PATCH", "/api/me", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("X-User-ID", userID)
|
|
return req
|
|
}
|
|
|
|
func TestUpdateMeAcceptsLanguage(t *testing.T) {
|
|
userID := newLanguageTestUser(t, "[email protected]")
|
|
|
|
w := httptest.NewRecorder()
|
|
req := newPatchMeRequest(userID, `{"language":"zh-Hans"}`)
|
|
testHandler.UpdateMe(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var lang *string
|
|
if err := testPool.QueryRow(context.Background(),
|
|
`SELECT language FROM "user" WHERE id = $1`, userID,
|
|
).Scan(&lang); err != nil {
|
|
t.Fatalf("lookup user: %v", err)
|
|
}
|
|
if lang == nil || *lang != "zh-Hans" {
|
|
t.Fatalf("expected language=zh-Hans, got %v", lang)
|
|
}
|
|
|
|
var resp map[string]any
|
|
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if got, _ := resp["language"].(string); got != "zh-Hans" {
|
|
t.Fatalf("expected response language=zh-Hans, got %v", resp["language"])
|
|
}
|
|
}
|
|
|
|
func TestUpdateMeAcceptsKoreanLanguage(t *testing.T) {
|
|
userID := newLanguageTestUser(t, "[email protected]")
|
|
|
|
w := httptest.NewRecorder()
|
|
req := newPatchMeRequest(userID, `{"language":"ko"}`)
|
|
testHandler.UpdateMe(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var resp map[string]any
|
|
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if got, _ := resp["language"].(string); got != "ko" {
|
|
t.Fatalf("expected response language=ko, got %v", resp["language"])
|
|
}
|
|
}
|
|
|
|
func TestUpdateMeAcceptsJapaneseLanguage(t *testing.T) {
|
|
userID := newLanguageTestUser(t, "[email protected]")
|
|
|
|
w := httptest.NewRecorder()
|
|
req := newPatchMeRequest(userID, `{"language":"ja"}`)
|
|
testHandler.UpdateMe(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var resp map[string]any
|
|
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if got, _ := resp["language"].(string); got != "ja" {
|
|
t.Fatalf("expected response language=ja, got %v", resp["language"])
|
|
}
|
|
}
|
|
|
|
func TestUpdateMeRejectsUnsupportedLanguage(t *testing.T) {
|
|
userID := newLanguageTestUser(t, "[email protected]")
|
|
|
|
w := httptest.NewRecorder()
|
|
req := newPatchMeRequest(userID, `{"language":"<script>"}`)
|
|
testHandler.UpdateMe(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var lang *string
|
|
if err := testPool.QueryRow(context.Background(),
|
|
`SELECT language FROM "user" WHERE id = $1`, userID,
|
|
).Scan(&lang); err != nil {
|
|
t.Fatalf("lookup user: %v", err)
|
|
}
|
|
if lang != nil {
|
|
t.Fatalf("expected language unchanged (NULL), got %v", *lang)
|
|
}
|
|
}
|
|
|
|
// COALESCE semantics: omitting language must NOT clear an existing value.
|
|
func TestUpdateMePreservesLanguageWhenNotProvided(t *testing.T) {
|
|
userID := newLanguageTestUser(t, "[email protected]")
|
|
|
|
if _, err := testPool.Exec(context.Background(),
|
|
`UPDATE "user" SET language = 'en' WHERE id = $1`, userID,
|
|
); err != nil {
|
|
t.Fatalf("preset language: %v", err)
|
|
}
|
|
|
|
w := httptest.NewRecorder()
|
|
req := newPatchMeRequest(userID, `{"name":"Updated Name"}`)
|
|
testHandler.UpdateMe(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var lang *string
|
|
if err := testPool.QueryRow(context.Background(),
|
|
`SELECT language FROM "user" WHERE id = $1`, userID,
|
|
).Scan(&lang); err != nil {
|
|
t.Fatalf("lookup user: %v", err)
|
|
}
|
|
if lang == nil || *lang != "en" {
|
|
t.Fatalf("expected language=en preserved, got %v", lang)
|
|
}
|
|
}
|