diff --git a/src/integrations/terminal/ExecaTerminalProcess.ts b/src/integrations/terminal/ExecaTerminalProcess.ts index 2f8ebfa7a8..bb743dc992 100644 --- a/src/integrations/terminal/ExecaTerminalProcess.ts +++ b/src/integrations/terminal/ExecaTerminalProcess.ts @@ -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}` diff --git a/src/integrations/terminal/__tests__/ExecaTerminalProcess.spec.ts b/src/integrations/terminal/__tests__/ExecaTerminalProcess.spec.ts index 873b8f85ab..03ab6db24c 100644 --- a/src/integrations/terminal/__tests__/ExecaTerminalProcess.spec.ts +++ b/src/integrations/terminal/__tests__/ExecaTerminalProcess.spec.ts @@ -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)