mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
Merge pull request #11759 from esengine/feat/tray-language
fix(studio): pass the system language to the kernel host so the tray follows it
This commit is contained in:
8 files changed
+249
-9
No files matched your search
@@ -0,0 +1,28 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/i18n"
|
||||
"reasonix/internal/contract/config"
|
||||
)
|
||||
|
||||
func TestKernelLanguageWithShellSystemLanguage(t *testing.T) {
|
||||
before := i18n.CurrentLanguage()
|
||||
t.Cleanup(func() { i18n.DetectLanguage(before) })
|
||||
for _, key := range []string{"REASONIX_LANG", "LC_ALL", "LC_MESSAGES", "LANG"} {
|
||||
t.Setenv(key, "")
|
||||
}
|
||||
t.Setenv("REASONIX_SYSTEM_LANG", "zh-CN")
|
||||
for _, tt := range []struct{ setting, env, want string }{
|
||||
{"auto", "", "zh"},
|
||||
{"en", "", "en"},
|
||||
{"auto", "en", "en"},
|
||||
{"zh-TW", "en", "zh-TW"},
|
||||
} {
|
||||
t.Setenv("REASONIX_LANG", tt.env)
|
||||
if got := resolveKernelLanguage(&config.Config{Language: tt.setting}); got != tt.want {
|
||||
t.Errorf("setting %q, env %q: language = %q, want %q", tt.setting, tt.env, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"reasonix/internal/base/i18n"
|
||||
"reasonix/internal/base/testenv"
|
||||
"reasonix/internal/contract/config"
|
||||
"reasonix/internal/frontend/serve"
|
||||
)
|
||||
|
||||
func TestAssembledTrayLanguage(t *testing.T) {
|
||||
before := i18n.CurrentLanguage()
|
||||
t.Cleanup(func() { i18n.DetectLanguage(before) })
|
||||
for _, tt := range []struct{ name, system, desktop, env, locale, want string }{
|
||||
{"system", "zh-Hans-CN", "auto", "", "", "zh"},
|
||||
{"traditional", "zh-Hant-TW", "auto", "", "", "zh-TW"},
|
||||
{"desktop", "zh-Hans-CN", "en", "", "", "en"},
|
||||
{"reasonix", "zh-Hans-CN", "auto", "en-US", "", "en"},
|
||||
{"desktop-over-env", "en-US", "zh", "en", "", "zh"},
|
||||
{"c-locale", "zh-Hans-CN", "auto", "", "C", "en"},
|
||||
{"posix-locale", "zh-Hans-CN", "auto", "", "POSIX.UTF-8", "en"},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Setenv("REASONIX_HOME", testenv.TempDir(t))
|
||||
t.Chdir(testenv.TempDir(t))
|
||||
for _, key := range []string{"REASONIX_LANG", "LC_ALL", "LC_MESSAGES", "LANG"} {
|
||||
t.Setenv(key, "")
|
||||
}
|
||||
t.Setenv("REASONIX_SYSTEM_LANG", tt.system)
|
||||
t.Setenv("REASONIX_LANG", tt.env)
|
||||
t.Setenv("LANG", tt.locale)
|
||||
cfg := config.Default()
|
||||
disabled := false
|
||||
cfg.Desktop.Telemetry, cfg.Desktop.Metrics = &disabled, &disabled
|
||||
if err := cfg.SetDesktopLanguage(tt.desktop); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := cfg.SaveTo(config.UserConfigPath()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer cancel()
|
||||
hub, err := assemble(ctx, io.Discard, io.Discard, shellIdentity{}, nil, newStartupPhases(io.Discard, time.Now))
|
||||
if err != nil {
|
||||
t.Fatalf("assemble: %v", err)
|
||||
}
|
||||
defer hub.Shutdown()
|
||||
b, err := bind(hub.Handler())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- b.serve(ctx) }()
|
||||
defer func() {
|
||||
cancel()
|
||||
select {
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
case <-time.After(30 * time.Second):
|
||||
t.Error("host did not drain")
|
||||
}
|
||||
}()
|
||||
check := func(want string) {
|
||||
t.Helper()
|
||||
status, body := ask(t, b, http.MethodGet, "/tray/state", nil)
|
||||
if status != http.StatusOK {
|
||||
t.Fatalf("tray status = %d", status)
|
||||
}
|
||||
var state serve.TrayState
|
||||
if err := json.Unmarshal([]byte(body), &state); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
say := i18n.Catalog(want)
|
||||
labels := serve.TrayLabels{Open: say.TrayOpen, CloseToTray: say.TrayCloseToTray, Quit: say.TrayQuit}
|
||||
if state.Labels != labels || state.Line != say.TrayIdle {
|
||||
t.Fatalf("tray = %+v, want labels %+v and line %q", state, labels, say.TrayIdle)
|
||||
}
|
||||
}
|
||||
check(tt.want)
|
||||
for _, lang := range []string{"en", "zh"} {
|
||||
status, _ := ask(t, b, http.MethodPost, "/appearance", func(r *http.Request) {
|
||||
r.Body = io.NopCloser(strings.NewReader(`{"language":"` + lang + `"}`))
|
||||
})
|
||||
if status != http.StatusOK {
|
||||
t.Fatalf("appearance status = %d", status)
|
||||
}
|
||||
check(lang)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -25,8 +25,10 @@ class HostStartError extends Error {
|
||||
// the only channel pointing this way. stdin is the lease: the kernel drains
|
||||
// when this end closes, which is what stops it outliving a parent that exited
|
||||
// without being asked to.
|
||||
function start(binary, args, { onStderr, onExit, onAct, onSlow, timeoutMs = HANDSHAKE_TIMEOUT_MS } = {}) {
|
||||
const child = spawn(binary, args, { stdio: ["pipe", "pipe", "pipe"] });
|
||||
function start(binary, args, { onStderr, onExit, onAct, onSlow, systemLanguage, timeoutMs = HANDSHAKE_TIMEOUT_MS } = {}) {
|
||||
const env = { ...process.env };
|
||||
if (systemLanguage) env.REASONIX_SYSTEM_LANG = systemLanguage;
|
||||
const child = spawn(binary, args, { stdio: ["pipe", "pipe", "pipe"], env });
|
||||
child.stderr.setEncoding("utf8");
|
||||
if (onStderr) child.stderr.on("data", onStderr);
|
||||
if (onExit) child.on("exit", onExit);
|
||||
|
||||
@@ -94,6 +94,7 @@ async function launchKernel(args) {
|
||||
const began = Date.now();
|
||||
logs.shell.line(`host: starting ${hostBinary} (attempt ${attempt})`);
|
||||
kernel = start(hostBinary, args, {
|
||||
systemLanguage: app.getPreferredSystemLanguages()[0] ?? app.getLocale(),
|
||||
timeoutMs: handshakeTimeout(),
|
||||
onSlow: () => {
|
||||
logs.shell.line("host: no handshake yet; showing the starting window");
|
||||
|
||||
@@ -6,7 +6,7 @@ import os from "node:os";
|
||||
import fs from "node:fs";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const { parse, readActs } = require("../src/host.js");
|
||||
const { parse, readActs, start } = require("../src/host.js");
|
||||
const { contextTemplate, editMenuTemplate, applicationMenuTemplate, menuInstaller } = require("../src/editmenu.js");
|
||||
const { uiLanguage } = require("../src/uilang.js");
|
||||
const { externalTarget } = require("../src/links.js");
|
||||
@@ -17,6 +17,23 @@ const { pick, loadPrefs, savePrefs, registerPrefs } = require("../src/prefs.js")
|
||||
const TOKEN = "a".repeat(64);
|
||||
const line = (over) => JSON.stringify({ version: 1, origin: "http://127.0.0.1:8080", token: TOKEN, ...over });
|
||||
|
||||
test("the spawned host receives the system language and inherits explicit locale overrides", async () => {
|
||||
const script = `console.log(${JSON.stringify(line())});
|
||||
console.log(JSON.stringify({act: JSON.stringify({system: process.env.REASONIX_SYSTEM_LANG,
|
||||
explicit: process.env.REASONIX_LANG})}));`;
|
||||
const before = process.env.REASONIX_SYSTEM_LANG;
|
||||
const explicit = process.env.REASONIX_LANG;
|
||||
const state = new Promise((resolve) => {
|
||||
const host = start(process.execPath, ["-e", script], {
|
||||
systemLanguage: "zh-Hant-TW",
|
||||
onAct: (act) => resolve(JSON.parse(act)),
|
||||
});
|
||||
host.ready.catch(resolve);
|
||||
});
|
||||
assert.deepEqual(await state, { system: "zh-Hant-TW", ...(explicit === undefined ? {} : { explicit }) });
|
||||
assert.equal(process.env.REASONIX_SYSTEM_LANG, before);
|
||||
});
|
||||
|
||||
test("the handshake is accepted only when every field accounts for itself", () => {
|
||||
assert.deepEqual(parse(line()), { origin: "http://127.0.0.1:8080", token: TOKEN });
|
||||
|
||||
|
||||
@@ -5,11 +5,9 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// DetectLanguage selects a catalogue from override (e.g. cfg.Language) or the
|
||||
// environment and installs it as M. Returns the resolved tag ("en", "zh") so
|
||||
// callers can log or expose it.
|
||||
//
|
||||
// Priority: override > REASONIX_LANG > LC_ALL > LC_MESSAGES > LANG > "en".
|
||||
// DetectLanguage installs the catalogue for an explicit setting or locale.
|
||||
// Explicit settings and locale variables take priority over the shell's system
|
||||
// language and the native OS probe; unresolved locales fall back to English.
|
||||
func DetectLanguage(override string) string {
|
||||
for _, c := range append([]string{override}, envCandidates()...) {
|
||||
if tag := normalize(c); tag != "" {
|
||||
@@ -24,7 +22,7 @@ func DetectLanguage(override string) string {
|
||||
var osLanguage = detectOSLanguage
|
||||
|
||||
func envCandidates() []string {
|
||||
keys := []string{"REASONIX_LANG", "LC_ALL", "LC_MESSAGES", "LANG"}
|
||||
keys := []string{"REASONIX_LANG", "LC_ALL", "LC_MESSAGES", "LANG", "REASONIX_SYSTEM_LANG"}
|
||||
out := make([]string, 0, len(keys)+1)
|
||||
for _, k := range keys {
|
||||
out = append(out, os.Getenv(k))
|
||||
@@ -58,6 +56,9 @@ func normalize(s string) string {
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
if s == "c" || s == "posix" || strings.HasPrefix(s, "c.") || strings.HasPrefix(s, "posix.") {
|
||||
return "en"
|
||||
}
|
||||
if strings.HasPrefix(s, "zh-tw") || strings.HasPrefix(s, "zh-hant") || strings.Contains(s, "chinese traditional") || strings.Contains(s, "繁體") {
|
||||
return "zh-TW"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package i18n
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNormalizePOSIXLocales(t *testing.T) {
|
||||
for _, tt := range []struct{ locale, want string }{
|
||||
{"C", "en"}, {"POSIX", "en"}, {"C.UTF-8", "en"},
|
||||
{"C.ISO8859-1", "en"}, {"POSIX.UTF-8", "en"}, {"POSIX.ASCII", "en"},
|
||||
{" c.utf-8 ", "en"}, {"", ""}, {" ", ""},
|
||||
{"zh-Hans-CN", "zh"}, {"zh_TW.UTF-8", "zh-TW"}, {"en-US", "en"},
|
||||
{"ca-ES", ""}, {"posix-extra", ""},
|
||||
} {
|
||||
t.Run(tt.locale, func(t *testing.T) {
|
||||
if got := normalize(tt.locale); got != tt.want {
|
||||
t.Fatalf("normalize(%q) = %q, want %q", tt.locale, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPOSIXLocaleWinsOverSystemLanguage(t *testing.T) {
|
||||
before, probe := CurrentLanguage(), osLanguage
|
||||
osLanguage = func() string { return "zh-TW" }
|
||||
t.Cleanup(func() { setLanguage(before); osLanguage = probe })
|
||||
for _, key := range []string{"LC_ALL", "LC_MESSAGES", "LANG"} {
|
||||
for _, locale := range []string{"C", "POSIX", "C.UTF-8", "C.ASCII", "POSIX.UTF-8"} {
|
||||
t.Run(key+"/"+locale, func(t *testing.T) {
|
||||
for _, candidate := range []string{"REASONIX_LANG", "LC_ALL", "LC_MESSAGES", "LANG"} {
|
||||
t.Setenv(candidate, "")
|
||||
}
|
||||
t.Setenv("REASONIX_SYSTEM_LANG", "zh-Hans-CN")
|
||||
t.Setenv(key, locale)
|
||||
if got := DetectLanguage("auto"); got != "en" {
|
||||
t.Fatalf("language = %q, want en", got)
|
||||
}
|
||||
if got := CatalogFor("auto").TrayOpen; got != English.TrayOpen {
|
||||
t.Fatalf("tray open = %q, want %q", got, English.TrayOpen)
|
||||
}
|
||||
t.Setenv("REASONIX_LANG", "zh-TW")
|
||||
if got := DetectLanguage("auto"); got != "zh-TW" {
|
||||
t.Fatalf("environment override = %q, want zh-TW", got)
|
||||
}
|
||||
if got := DetectLanguage("zh"); got != "zh" {
|
||||
t.Fatalf("explicit override = %q, want zh", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package i18n
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestShellSystemLanguagePriority(t *testing.T) {
|
||||
probe := osLanguage
|
||||
osLanguage = func() string { return "en-US" }
|
||||
t.Cleanup(func() { osLanguage = probe })
|
||||
before := CurrentLanguage()
|
||||
t.Cleanup(func() { setLanguage(before) })
|
||||
for _, tt := range []struct {
|
||||
name, override, key, value, system, want string
|
||||
}{
|
||||
{"auto", "auto", "", "", "zh-CN", "zh"},
|
||||
{"traditional", "auto", "", "", "zh-Hant-TW", "zh-TW"},
|
||||
{"setting", "en", "", "", "zh-CN", "en"},
|
||||
{"reasonix", "auto", "REASONIX_LANG", "en", "zh-CN", "en"},
|
||||
{"all", "auto", "LC_ALL", "en_US.UTF-8", "zh-CN", "en"},
|
||||
{"messages", "auto", "LC_MESSAGES", "en_US.UTF-8", "zh-CN", "en"},
|
||||
{"lang", "auto", "LANG", "en_US.UTF-8", "zh-CN", "en"},
|
||||
{"unsupported", "auto", "", "", "fr-FR", "en"},
|
||||
{"absent", "auto", "", "", "", "en"},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
for _, key := range []string{"REASONIX_LANG", "LC_ALL", "LC_MESSAGES", "LANG"} {
|
||||
t.Setenv(key, "")
|
||||
}
|
||||
t.Setenv("REASONIX_SYSTEM_LANG", tt.system)
|
||||
if tt.key != "" {
|
||||
t.Setenv(tt.key, tt.value)
|
||||
}
|
||||
if got := DetectLanguage(tt.override); got != tt.want {
|
||||
t.Fatalf("language = %q, want %q", got, tt.want)
|
||||
}
|
||||
say := CatalogFor(tt.override)
|
||||
want := Catalog(tt.want)
|
||||
if say.TrayOpen != want.TrayOpen || say.TrayCloseToTray != want.TrayCloseToTray || say.TrayQuit != want.TrayQuit || say.TrayIdle != want.TrayIdle {
|
||||
t.Fatal("tray catalogue does not follow the resolved language")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user