Skip to content

Commit 1f1abd2

Browse files
committed
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
1 parent 2b647ed commit 1f1abd2

File tree

2 files changed

+78
-7
lines changed

2 files changed

+78
-7
lines changed

src/integrations/terminal/ExecaTerminalProcess.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,21 +32,49 @@ export class ExecaTerminalProcess extends BaseTerminalProcess {
3232
return terminal
3333
}
3434

35+
/**
36+
* Gets the appropriate UTF-8 locale based on the system's current locale.
37+
* This ensures proper encoding for different languages, especially for
38+
* non-Latin character sets like Chinese, Japanese, Korean, etc.
39+
*/
40+
private getUtf8Locale(): { LANG: string; LC_ALL: string } {
41+
const currentLang = process.env.LANG || process.env.LC_ALL || ""
42+
43+
// Extract the language/country part before the encoding (e.g., "zh_CN" from "zh_CN.GBK")
44+
const langMatch = currentLang.match(/^([a-z]{2}_[A-Z]{2})/i)
45+
46+
if (langMatch) {
47+
// Use the detected locale with UTF-8 encoding
48+
const utf8Locale = `${langMatch[1]}.UTF-8`
49+
return {
50+
LANG: utf8Locale,
51+
LC_ALL: utf8Locale,
52+
}
53+
}
54+
55+
// Fallback to en_US.UTF-8 if no locale is detected
56+
return {
57+
LANG: "en_US.UTF-8",
58+
LC_ALL: "en_US.UTF-8",
59+
}
60+
}
61+
3562
public override async run(command: string) {
3663
this.command = command
3764

3865
try {
3966
this.isHot = true
4067

68+
const utf8Locale = this.getUtf8Locale()
69+
4170
this.subprocess = execa({
4271
shell: true,
4372
cwd: this.terminal.getCurrentWorkingDirectory(),
4473
all: true,
4574
env: {
4675
...process.env,
47-
// Ensure UTF-8 encoding for Ruby, CocoaPods, etc.
48-
LANG: "en_US.UTF-8",
49-
LC_ALL: "en_US.UTF-8",
76+
// Ensure UTF-8 encoding based on system locale
77+
...utf8Locale,
5078
},
5179
})`${command}`
5280

src/integrations/terminal/__tests__/ExecaTerminalProcess.spec.ts

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,10 @@ describe("ExecaTerminalProcess", () => {
5656
})
5757

5858
describe("UTF-8 encoding fix", () => {
59-
it("should set LANG and LC_ALL to en_US.UTF-8", async () => {
59+
it("should set LANG and LC_ALL to en_US.UTF-8 when no locale is set", async () => {
60+
delete process.env.LANG
61+
delete process.env.LC_ALL
62+
terminalProcess = new ExecaTerminalProcess(mockTerminal)
6063
await terminalProcess.run("echo test")
6164
const execaMock = vitest.mocked(execa)
6265
expect(execaMock).toHaveBeenCalledWith(
@@ -81,9 +84,49 @@ describe("ExecaTerminalProcess", () => {
8184
expect(calledOptions.env.EXISTING_VAR).toBe("existing")
8285
})
8386

84-
it("should override existing LANG and LC_ALL values", async () => {
85-
process.env.LANG = "C"
86-
process.env.LC_ALL = "POSIX"
87+
it("should convert Chinese GBK locale to UTF-8", async () => {
88+
process.env.LANG = "zh_CN.GBK"
89+
terminalProcess = new ExecaTerminalProcess(mockTerminal)
90+
await terminalProcess.run("echo test")
91+
const execaMock = vitest.mocked(execa)
92+
const calledOptions = execaMock.mock.calls[0][0] as any
93+
expect(calledOptions.env.LANG).toBe("zh_CN.UTF-8")
94+
expect(calledOptions.env.LC_ALL).toBe("zh_CN.UTF-8")
95+
})
96+
97+
it("should convert Japanese locale to UTF-8", async () => {
98+
process.env.LANG = "ja_JP.SJIS"
99+
terminalProcess = new ExecaTerminalProcess(mockTerminal)
100+
await terminalProcess.run("echo test")
101+
const execaMock = vitest.mocked(execa)
102+
const calledOptions = execaMock.mock.calls[0][0] as any
103+
expect(calledOptions.env.LANG).toBe("ja_JP.UTF-8")
104+
expect(calledOptions.env.LC_ALL).toBe("ja_JP.UTF-8")
105+
})
106+
107+
it("should handle locale from LC_ALL when LANG is not set", async () => {
108+
delete process.env.LANG
109+
process.env.LC_ALL = "ko_KR.EUC-KR"
110+
terminalProcess = new ExecaTerminalProcess(mockTerminal)
111+
await terminalProcess.run("echo test")
112+
const execaMock = vitest.mocked(execa)
113+
const calledOptions = execaMock.mock.calls[0][0] as any
114+
expect(calledOptions.env.LANG).toBe("ko_KR.UTF-8")
115+
expect(calledOptions.env.LC_ALL).toBe("ko_KR.UTF-8")
116+
})
117+
118+
it("should handle already UTF-8 locale", async () => {
119+
process.env.LANG = "zh_CN.UTF-8"
120+
terminalProcess = new ExecaTerminalProcess(mockTerminal)
121+
await terminalProcess.run("echo test")
122+
const execaMock = vitest.mocked(execa)
123+
const calledOptions = execaMock.mock.calls[0][0] as any
124+
expect(calledOptions.env.LANG).toBe("zh_CN.UTF-8")
125+
expect(calledOptions.env.LC_ALL).toBe("zh_CN.UTF-8")
126+
})
127+
128+
it("should fallback to en_US.UTF-8 for invalid locale format", async () => {
129+
process.env.LANG = "invalid_locale"
87130
terminalProcess = new ExecaTerminalProcess(mockTerminal)
88131
await terminalProcess.run("echo test")
89132
const execaMock = vitest.mocked(execa)

0 commit comments

Comments
 (0)