Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 31 additions & 3 deletions src/integrations/terminal/ExecaTerminalProcess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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)
Expand Down
Loading