Files
multica/server/internal/handler/user_language_test.go
Anderson Shindy Oki 1aa742053b i18n: add japanese locale (MUL-2893) (#3538)
* i18n: add japanese locale

* fix: spacing issues

* refactor

* fix(desktop): set <html lang> before paint to avoid JA Kanji font flash

Switch the documentElement.lang sync from useEffect to useLayoutEffect so
lang is committed before the first paint. Otherwise Japanese desktop users
saw one frame of Kanji rendered with the Chinese-first fallback stack before
the html[lang|="ja"] CJK override applied. Also fix the stale selector in the
HTML_LANG comment (html[lang^="ja"] -> html[lang|="ja"]).

Addresses review nits on MUL-2893.

Co-authored-by: multica-agent <github@multica.ai>

* fix(docs): tokenize the ideographic iteration mark in JA search

Add U+3005 (々) to the Japanese search tokenizer character class. It sits just
below the kana blocks, so words like 様々 / 日々 / 個々 previously dropped the
mark and split awkwardly, hurting recall.

Addresses a review nit on MUL-2893.

Co-authored-by: multica-agent <github@multica.ai>

* fix(i18n): restore ja locale parity after merging main

Merging main brought new EN strings into agents/chat/onboarding/settings/
squads that the ja bundle (authored against an older snapshot) lacked, breaking
the locales parity test. Add the Japanese translations for the new keys
(workspace logo upload, agents runtime filter, chat session-history stop
dialog, onboarding social_github, squad archived status) and drop the two
renamed chat window keys (active_group / archived_group) that EN removed in
favour of history_group.

Fixes the failing @multica/views parity.test.ts on the FE CI for MUL-2893.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-02 14:29:29 +08:00

156 lines
4.2 KiB
Go

package handler
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
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, "lang-set@multica.ai")
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, "lang-ko@multica.ai")
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, "lang-ja@multica.ai")
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, "lang-reject@multica.ai")
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, "lang-preserve@multica.ai")
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)
}
}