fix(studio): pass the system language to the kernel host so the tray follows it

This commit is contained in:
esengine committed 2026-10-02 08:56:31 -07:00
1 parent 7bdaae2025
commit d3d3cba4ea
6 files changed
+97 -9

No files matched your search

+28
View File
@@ -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)
}
}
}
+4 -2
View File
@@ -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);
+1
View File
@@ -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");
+18 -1
View File
@@ -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 });
+4 -6
View File
@@ -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))
@@ -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")
}
})
}
}