feat: add Russian localization and independent STT languages
CI / Typecheck, tests, lint (push) Canceled after 0s
CI / Standalone (mac-arm64) (push) Canceled after 0s
CI / Standalone (linux-x64) (push) Canceled after 0s
CI / Standalone (windows-x64) (push) Canceled after 0s

This commit is contained in:
2026-10-25 17:18:44 +03:00
parent b277ee3e0f
commit a50ca4b867
93 changed files with 3261 additions and 904 deletions
+13
View File
@@ -0,0 +1,13 @@
/** Native-shell catalog. Kept with the shell so packaged builds do not load server sources. */
const ru = {
"Check for Updates…": "Проверить обновления…", "Edit": "Правка", "View": "Вид", "Window": "Окно",
"Actual Size": "Исходный масштаб", "Zoom In": "Увеличить масштаб", "Zoom Out": "Уменьшить масштаб",
"About LearnHarness": "О LearnHarness", "Hide LearnHarness": "Скрыть LearnHarness", "Hide Others": "Скрыть остальные", "Quit LearnHarness": "Выйти из LearnHarness",
"Undo": "Отменить", "Redo": "Повторить", "Cut": "Вырезать", "Copy": "Копировать", "Paste": "Вставить", "Select All": "Выбрать всё",
"Reload": "Перезагрузить", "Developer Tools": "Инструменты разработчика", "Toggle Full Screen": "Полноэкранный режим", "Minimize": "Свернуть", "Close": "Закрыть",
"You're up to date.": "Установлена последняя версия.", "Couldn't check for updates.": "Не удалось проверить обновления.",
"LearnHarness {version} is the latest version.": "LearnHarness {version} — последняя версия.", "OK": "ОК",
};
function resolveLanguage(preference, languages = []) { return preference === "ru" || preference !== "en" && /^ru(?:-|_|$)/i.test(languages[0] || "") ? "ru" : "en"; }
function translate(language, text, params = {}) { return (language === "ru" ? ru[text] || text : text).replace(/\{(\w+)\}/g, (match, key) => Object.hasOwn(params, key) ? String(params[key]) : match); }
module.exports = { resolveLanguage, translate };
+34 -16
View File
@@ -214,6 +214,7 @@ function startServer(port) {
PORT: String(port),
HOSTNAME: "127.0.0.1", // the server defaults to 0.0.0.0 — never
LH_DATA_DIR: DATA_DIR(),
LH_SYSTEM_LANGUAGE: app.getPreferredSystemLanguages()[0] || "en",
// The auth/keys store — the child reads AND writes this file itself
// (no env injection anymore: a spawn-time copy would shadow every
// settings-page key change until restart; env wins over file).
@@ -456,11 +457,11 @@ function checkForUpdates({ manual = false } = {}) {
// update-downloaded. Manual checks additionally answer directly.
if (!manual) return;
const version = result?.updateInfo?.version;
if (!version) dialog.showMessageBox({ type: "info", message: "You're up to date.", detail: `LearnHarness ${app.getVersion()} is the latest version.` });
if (!version) dialog.showMessageBox({ type: "info", message: nt("You're up to date."), detail: nt("LearnHarness {version} is the latest version.", { version: app.getVersion() }), buttons: [nt("OK")] });
})
.catch((err) => {
log(`update check failed: ${err?.message ?? err}`);
if (manual) dialog.showMessageBox({ type: "warning", message: "Couldn't check for updates.", detail: String(err?.message ?? err) });
if (manual) dialog.showMessageBox({ type: "warning", message: nt("Couldn't check for updates."), buttons: [nt("OK")], detail: String(err?.message ?? err) });
});
}
@@ -524,53 +525,63 @@ function applyZoom(win, factor) {
/** macOS needs an application menu with the Edit roles — without one,
* Cmd+C/V/A do nothing in the renderer. Everything else stays minimal. */
const { resolveLanguage: resolveShellLanguage, translate: translateShell } = require("./language.cjs");
let shellLanguage;
function nativeLanguage() {
if (shellLanguage) return shellLanguage;
let preference;
try { preference = JSON.parse(fs.readFileSync(path.join(DATA_DIR(), "settings.json"), "utf8")).general?.language; } catch {}
return resolveShellLanguage(preference, app.getPreferredSystemLanguages());
}
function nt(text, params) { return translateShell(nativeLanguage(), text, params); }
function installMenu() {
const template = [
{
label: app.name,
submenu: [
{ role: "about" },
{ role: "about", label: nt("About LearnHarness") },
// Manual check (Wave C): answers via native dialog — no renderer
// round-trip. The 24h automatic checks stay silent.
{ label: "Check for Updates…", click: () => checkForUpdates({ manual: true }) },
{ label: nt("Check for Updates…"), click: () => checkForUpdates({ manual: true }) },
{ type: "separator" },
{ role: "hide" },
{ role: "hideOthers" },
{ role: "hide", label: nt("Hide LearnHarness") },
{ role: "hideOthers", label: nt("Hide Others") },
{ type: "separator" },
{ role: "quit" },
{ role: "quit", label: nt("Quit LearnHarness") },
],
},
{ label: "Edit", submenu: [{ role: "undo" }, { role: "redo" }, { type: "separator" }, { role: "cut" }, { role: "copy" }, { role: "paste" }, { role: "selectAll" }] },
{ label: nt("Edit"), submenu: [{ role: "undo", label: nt("Undo") }, { role: "redo", label: nt("Redo") }, { type: "separator" }, { role: "cut", label: nt("Cut") }, { role: "copy", label: nt("Copy") }, { role: "paste", label: nt("Paste") }, { role: "selectAll", label: nt("Select All") }] },
{
label: "View",
label: nt("View"),
submenu: [
{ role: "reload" },
{ role: "toggleDevTools" },
{ role: "reload", label: nt("Reload") },
{ role: "toggleDevTools", label: nt("Developer Tools") },
{ type: "separator" },
// OWNED zoom (see applyZoom): the roles change zoomLevel
// silently; these broadcast the factor and re-seat the
// traffic lights. The renderer's --lh-zoom depends on
// hearing every change.
{
label: "Actual Size",
label: nt("Actual Size"),
accelerator: "CommandOrControl+0",
click: () => applyZoom(BrowserWindow.getAllWindows()[0], 1),
},
{
label: "Zoom In",
label: nt("Zoom In"),
accelerator: "CommandOrControl+Plus",
click: () => zoomStep(BrowserWindow.getAllWindows()[0], +1),
},
{
label: "Zoom Out",
label: nt("Zoom Out"),
accelerator: "CommandOrControl+-",
click: () => zoomStep(BrowserWindow.getAllWindows()[0], -1),
},
{ type: "separator" },
{ role: "togglefullscreen" },
{ role: "togglefullscreen", label: nt("Toggle Full Screen") },
],
},
{ role: "windowMenu" },
{ label: nt("Window"), submenu: [{ role: "minimize", label: nt("Minimize") }, { role: "close", label: nt("Close") }] },
];
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
}
@@ -647,6 +658,13 @@ async function main() {
});
installMenu();
ipcMain.on("lh:language", (event, language) => {
if (!trustedAppWindows.has(BrowserWindow.fromWebContents(event.sender)) || !trustedSender(event, new URL(url).origin, BrowserWindow)) return;
if (language !== "en" && language !== "ru") return;
if (shellLanguage === language) return;
shellLanguage = language;
installMenu();
});
// Auto-update (Wave C): wire the updater before the window so the
// banner channel is live from first paint; the first check and the 24h
+1
View File
@@ -29,6 +29,7 @@ const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("lh", {
platform: process.platform,
setLanguage: (language) => ipcRenderer.send("lh:language", language),
chatgpt: {
status: () => ipcRenderer.invoke("lh:chatgpt:control", { op: "status" }),
login: (id) => ipcRenderer.invoke("lh:chatgpt:control", { op: "login", id }),