fix: handle Chinese and other non-Latin character encodings in terminal output

- Detect system locale and use appropriate UTF-8 encoding
- Convert GBK, SJIS, EUC-KR and other encodings to UTF-8
- Add comprehensive tests for locale detection
- Fixes #6768
This commit is contained in:
Roo Code 2025-08-06 20:01:43 +00:00
parent 2b647ed9a1
commit 1f1abd21e8
2 changed files with 78 additions and 7 deletions

View file

@ -32,21 +32,49 @@ export class ExecaTerminalProcess extends BaseTerminalProcess {
return terminal
}
/**
* Gets the appropriate UTF-8 locale based on the system's current locale.
* This ensures proper encoding for different languages, especially for
* non-Latin character sets like Chinese, Japanese, Korean, etc.
*/
private getUtf8Locale(): { LANG: string; LC_ALL: string } {
const currentLang = process.env.LANG || process.env.LC_ALL || ""
// Extract the language/country part before the encoding (e.g., "zh_CN" from "zh_CN.GBK")
const langMatch = currentLang.match(/^([a-z]{2}_[A-Z]{2})/i)
if (langMatch) {
// Use the detected locale with UTF-8 encoding
const utf8Locale = `${langMatch[1]}.UTF-8`
return {
LANG: utf8Locale,
LC_ALL: utf8Locale,
}
}
// Fallback to en_US.UTF-8 if no locale is detected
return {
LANG: "en_US.UTF-8",
LC_ALL: "en_US.UTF-8",
}
}
public override async run(command: string) {
this.command = command
try {
this.isHot = true
const utf8Locale = this.getUtf8Locale()
this.subprocess = execa({
shell: true,
cwd: this.terminal.getCurrentWorkingDirectory(),
all: true,
env: {
...process.env,
// Ensure UTF-8 encoding for Ruby, CocoaPods, etc.
LANG: "en_US.UTF-8",
LC_ALL: "en_US.UTF-8",
// Ensure UTF-8 encoding based on system locale
...utf8Locale,
},
})`${command}`

View file

@ -56,7 +56,10 @@ describe("ExecaTerminalProcess", () => {
})
describe("UTF-8 encoding fix", () => {
it("should set LANG and LC_ALL to en_US.UTF-8", async () => {
it("should set LANG and LC_ALL to en_US.UTF-8 when no locale is set", async () => {
delete process.env.LANG
delete process.env.LC_ALL
terminalProcess = new ExecaTerminalProcess(mockTerminal)
await terminalProcess.run("echo test")
const execaMock = vitest.mocked(execa)
expect(execaMock).toHaveBeenCalledWith(
@ -81,9 +84,49 @@ describe("ExecaTerminalProcess", () => {
expect(calledOptions.env.EXISTING_VAR).toBe("existing")
})
it("should override existing LANG and LC_ALL values", async () => {
process.env.LANG = "C"
process.env.LC_ALL = "POSIX"
it("should convert Chinese GBK locale to UTF-8", async () => {
process.env.LANG = "zh_CN.GBK"
terminalProcess = new ExecaTerminalProcess(mockTerminal)
await terminalProcess.run("echo test")
const execaMock = vitest.mocked(execa)
const calledOptions = execaMock.mock.calls[0][0] as any
expect(calledOptions.env.LANG).toBe("zh_CN.UTF-8")
expect(calledOptions.env.LC_ALL).toBe("zh_CN.UTF-8")
})
it("should convert Japanese locale to UTF-8", async () => {
process.env.LANG = "ja_JP.SJIS"
terminalProcess = new ExecaTerminalProcess(mockTerminal)
await terminalProcess.run("echo test")
const execaMock = vitest.mocked(execa)
const calledOptions = execaMock.mock.calls[0][0] as any
expect(calledOptions.env.LANG).toBe("ja_JP.UTF-8")
expect(calledOptions.env.LC_ALL).toBe("ja_JP.UTF-8")
})
it("should handle locale from LC_ALL when LANG is not set", async () => {
delete process.env.LANG
process.env.LC_ALL = "ko_KR.EUC-KR"
terminalProcess = new ExecaTerminalProcess(mockTerminal)
await terminalProcess.run("echo test")
const execaMock = vitest.mocked(execa)
const calledOptions = execaMock.mock.calls[0][0] as any
expect(calledOptions.env.LANG).toBe("ko_KR.UTF-8")
expect(calledOptions.env.LC_ALL).toBe("ko_KR.UTF-8")
})
it("should handle already UTF-8 locale", async () => {
process.env.LANG = "zh_CN.UTF-8"
terminalProcess = new ExecaTerminalProcess(mockTerminal)
await terminalProcess.run("echo test")
const execaMock = vitest.mocked(execa)
const calledOptions = execaMock.mock.calls[0][0] as any
expect(calledOptions.env.LANG).toBe("zh_CN.UTF-8")
expect(calledOptions.env.LC_ALL).toBe("zh_CN.UTF-8")
})
it("should fallback to en_US.UTF-8 for invalid locale format", async () => {
process.env.LANG = "invalid_locale"
terminalProcess = new ExecaTerminalProcess(mockTerminal)
await terminalProcess.run("echo test")
const execaMock = vitest.mocked(execa)