feat: add Korean-English localization and light theme

Localize studio, curriculum, visualization and loading interfaces. Add persistent language and theme settings, refine module loading states, and normalize remote shell locales.
This commit is contained in:
2026-09-29 13:27:11 +09:00
parent 4125f71792
commit d44180db67
74 changed files with 2519 additions and 998 deletions
+85
View File
@@ -0,0 +1,85 @@
const assert = require('node:assert/strict')
const { test } = require('node:test')
const { readFileSync, mkdtempSync, writeFileSync, rmSync } = require('node:fs')
const { tmpdir } = require('node:os')
const { join } = require('node:path')
const { spawn } = require('node:child_process')
const vm = require('node:vm')
const ts = require('typescript')
const transpile = (source) => ts.transpileModule(source, {
compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS }
}).outputText
const helpers = { exports: {} }
vm.runInNewContext(transpile(readFileSync(join(__dirname, '../src/main/remoteShell.ts'), 'utf8')), helpers)
const { remoteShellCommand, remoteShellEnvironment } = helpers.exports
const source = readFileSync(join(__dirname, '../src/main/index.ts'), 'utf8')
const start = source.indexOf('async function runCommandOutput(')
const end = source.indexOf('\nasync function runCommand(', start)
const context = { spawn, process, setTimeout, clearTimeout, createModuleProgressParser: () => () => {} }
vm.runInNewContext(transpile(source.slice(start, end)), context)
const run = context.runCommandOutput
test('SSH data output excludes locale warnings even when command succeeds', async () => {
const result = await run('/bin/bash', ['--noprofile', '--norc', '-c', 'printf %s "/tmp/module with spaces"'], undefined,
{ LC_ALL: 'missing_TEST.UTF-8' }, 5000, 'stdout')
assert.equal(result, '/tmp/module with spaces')
})
test('failed commands retain stderr diagnostics and stdout', async () => {
await assert.rejects(run('/bin/sh', ['-c', 'printf context; printf "permission denied" >&2; exit 7'], undefined, undefined, 5000, 'stdout'),
error => error.message.includes('context') && error.message.includes('permission denied'))
})
test('non-SSH combined output stays compatible and explicit log merging works', async () => {
const command = 'printf output; printf diagnostic >&2'
const combined = await run('/bin/sh', ['-c', command])
assert.match(combined, /output/)
assert.match(combined, /diagnostic/)
const logs = await run('/bin/sh', ['-c', `{ ${command}; } 2>&1`], undefined, undefined, 5000, 'stdout')
assert.match(logs, /diagnostic/)
})
test('SSH environment neutralizes locale forwarding without losing other settings', () => {
const original = { LANG: 'ko_KR.UTF-8', LC_ALL: 'en_US.UTF-8', LC_CTYPE: 'bad', LANGUAGE: 'ko:en', PATH: '/bin', TERM: 'xterm-256color' }
const env = remoteShellEnvironment(original)
assert.equal(env.LANG, 'C')
assert.equal(env.LC_ALL, 'C')
assert.equal(env.LC_CTYPE, undefined)
assert.equal(env.LANGUAGE, undefined)
assert.equal(env.PATH, '/bin')
assert.equal(env.TERM, 'xterm-256color')
assert.equal(original.LC_CTYPE, 'bad')
})
test('remote wrapper preserves multiline commands, Unicode, quotes and exit status', async () => {
const command = `cat <<'END'
/tmp/한글 module's folder
END`
const result = await run('/bin/sh', ['-c', remoteShellCommand(command)], undefined, remoteShellEnvironment(process.env), 5000, 'stdout')
assert.equal(result, "/tmp/한글 module's folder\n")
await assert.rejects(run('/bin/sh', ['-c', remoteShellCommand('echo remote-failure >&2; exit 9')], undefined, undefined, 5000, 'stdout'), /remote-failure/)
})
test('locale selection prefers C UTF-8, supports other UTF-8 locales and falls back to C', async () => {
const dir = mkdtempSync(join(tmpdir(), 'lms-locale-test-'))
try {
for (const [locales, expected] of [['en_GB.utf8 C.utf8', 'C.utf8'], ['C en_GB.utf8', 'en_GB.utf8'], ['C POSIX', 'C']]) {
writeFileSync(join(dir, 'locale'), `#!/bin/sh\nprintf '%s\\n' ${locales}\n`, { mode: 0o755 })
const result = await run('/bin/sh', ['-c', remoteShellCommand('printf "%s|%s" "$LANG" "$LC_ALL"')], undefined,
{ PATH: `${dir}:${process.env.PATH}`, LC_ALL: 'C' }, 5000, 'stdout')
assert.equal(result, `${expected}|${expected}`)
}
} finally { rmSync(dir, { recursive: true, force: true }) }
})
test('queued SSH, stats probes and remote PTY use normalization; container logs keep stderr', () => {
const ssh = source.slice(source.indexOf('async function runSshOutput('), source.indexOf('async function readRemoteDockerStatsOutput('))
assert.equal((ssh.match(/remoteShellCommand\(command\)/g) || []).length, 2)
assert.equal((ssh.match(/'stdout'/g) || []).length, 2)
const terminal = source.slice(source.indexOf("ipcMain.handle('remote-workspace:terminal:start'"), source.indexOf("ipcMain.handle('terminal-resize'"))
assert.match(terminal, /remoteShellCommand\(mode === 'container'/)
assert.match(terminal, /remoteShellEnvironment\(terminalEnvironment/)
assert.match(source, /target \? remoteShellCommand\(init\)/)
assert.match(source, /docker logs --timestamps --tail 300.*2>&1/)
})