使用 Hammerspoon 在任何輸入框中快速進行翻譯

為了達到「在任何輸入框中進行快速翻譯」的功能:
1. 在 macOS 上安裝 Hammerspoon:可在官網下載或使用 brew。
2. 申請 xAI 的 API key。
3. 複製下方的 Lua 腳本,貼到 ~/.hammerspoon/init.lua 檔案內,將 xAI API key 貼進去,儲存並 reload config。
我一直很喜歡 Chrome 擴充功能「沉浸式翻譯」中的「輸入框增強」功能:快速連按三次空白鍵,就能將輸入框內的文字翻譯成英文。可惜這個功能僅限 Chrome 瀏覽器使用,而且有些輸入框無法支援 (例如網址列)。
前陣子看到 PJ Wu 的文章,使用了 Keyboard Maestro 呼叫 OpenAI API 進行文字翻譯和處理,在電腦的任何地方都可以翻譯。原本就對這類自動化工具非常感興趣,既然有人拋磚引玉,就決定動手試看看。
基本上,文字處理的過程就如同 PJ 文章中所述:選取文字,然後觸發腳本,腳本將文字藉由 API 完成翻譯後,貼回輸入框取代原文字。在這個架構上,我做了一些調整:
改用 Hammerspoon
跟 Keyboard Maestro 一樣,Hammerspoon 也是 macOS 平台上的自動化工具,但它開源免費😂。不同於 Keyboard Maestro 有圖形使用者界面 (GUI),Hammerspoon 所有的自動化功能都必須使用 Lua 程式語言撰寫腳本。對於沒有程式基礎的人來說,可能會有些挑戰。但身處 LLM 時代,這也不完全是個問題了~
話雖如此,一開始使用 GPT-o3-mini-high、Claude 3.7 Sonnet、Grok 3 寫出的腳本都沒能順利執行。最後是透過 Windsurf 編輯器搭配 Claude 3.7 Sonnet (Thinking) 才成功。
在此淚推 Windsurf 這個 AI 輔助的程式碼編輯器,有興趣的人歡迎使用下面的推薦連結註冊,你我都可以多獲得 500 個進階 API call 🙏

改用 Cmd + Opt + T 觸發
PJ 的腳本是使用滑鼠右鍵觸發,但我習慣手不用離開鍵盤做事,這部分就看各人喜好囉。
改用 xAI API
之所以使用 xAI API 是因為,只要儲值 5 美金一次,並且同意分享數據給 xAI,每月就可以獲得 150 美金的免費 API 額度,偉哉馬斯克!如果只是翻譯非機密文字的話,我覺得這是個可以好好利用的資源。
Lua 腳本分享
在電腦上的 ~/.hammerspoon
資料夾中找到 init.lua
檔案,由於該資料夾預設是隱藏的,可以使用 cmd + shift + .
顯示隱藏資料夾。複製下方的腳本,將你自己的 xAI API key 貼進去,儲存並 reload config,就可以使用了。
-- Hammerspoon configuration for text translation using xAI API
-- Set up the keyboard shortcut (cmd+opt+T)
local hotkeyModifiers = {"cmd", "alt"}
local hotkeyKey = "t"
-- xAI API configuration
local xAI_API_KEY = "YOUR_API_KEY"
local xAI_API_URL = "https://api.x.ai/v1/chat/completions"
-- Define the translation function
function translateSelectedText()
-- Get the current selection by doing a copy
local originalClipboard = hs.pasteboard.getContents()
hs.eventtap.keyStroke({"cmd"}, "c")
hs.timer.usleep(200000) -- Wait 0.2 seconds for copy to complete
local selectedText = hs.pasteboard.getContents()
-- Check if we got any text
if not selectedText or selectedText == "" or selectedText == originalClipboard then
hs.alert.show("No text selected")
return
end
-- No translating alert as per user request
-- Create request body for xAI API
local requestBody = {
model = "grok-beta",
messages = {
{
role = "system",
content = "You are a translator. Translate the given text to English. Only provide the translation without any explanation or additional text."
},
{
role = "user",
content = selectedText
}
},
temperature = 0.3
}
-- Convert request to JSON
local jsonBody = hs.json.encode(requestBody)
-- Set up headers with API key
local headers = {
["Content-Type"] = "application/json",
["Authorization"] = "Bearer " .. xAI_API_KEY
}
-- Send API request
hs.http.asyncPost(xAI_API_URL, jsonBody, headers, function(status, body)
if status == 200 then
-- Try to parse the response
local success, response = pcall(function() return hs.json.decode(body) end)
if success and response and response.choices and
response.choices[1] and response.choices[1].message then
-- Get the translation
local translation = response.choices[1].message.content
-- Replace the selected text with translation
hs.pasteboard.setContents(translation)
hs.eventtap.keyStroke({"cmd"}, "v")
-- No success alert as requested
else
-- Show error if we couldn't parse response
hs.alert.show("Error parsing API response")
end
else
-- Show error if API request failed
hs.alert.show("API Error: " .. status)
end
end)
end
-- Register the hotkey
hs.hotkey.bind(hotkeyModifiers, hotkeyKey, translateSelectedText)
-- Show confirmation that the module loaded
hs.alert.show("Translation module loaded - press cmd+opt+t to translate")
加碼 AutoHotKey 腳本
#Requires AutoHotkey v2.0
#SingleInstance Force
SendMode "Input"
SetWorkingDir A_ScriptDir
; 定義熱鍵:Win+Alt+T
#!t::
{
OldClipboard := ClipboardAll()
A_Clipboard := ""
Send "^c"
if !ClipWait(0.5, 1)
{
MsgBox "未檢測到選中的文本"
A_Clipboard := OldClipboard
return
}
; 獲取選中的中文文本
textToTranslate := A_Clipboard
; 呼叫xAI API進行翻譯
translatedText := TranslateWithxAI(textToTranslate)
; 將翻譯結果貼回取代原文
A_Clipboard := translatedText
Send "^v"
; 恢復原始剪貼簿內容
Sleep 200
A_Clipboard := OldClipboard
}
; xAI API 翻譯功能
TranslateWithxAI(text) {
; 替換以下變數為您的xAI API金鑰
apiKey := "YOUR_API_KEY"
apiUrl := "https://api.x.ai/v1/chat/completions"
; 準備API請求
oHttp := ComObject("WinHttp.WinHttpRequest.5.1")
oHttp.Open("POST", apiUrl, false)
oHttp.SetRequestHeader("Content-Type", "application/json")
oHttp.SetRequestHeader("Authorization", "Bearer " . apiKey)
; 準備JSON請求體
payload := '{"model":"grok-beta","messages":[{"role":"system","content":"You are a professional translator. Translate the following Chinese text to English. Respond with only the translated text, no explanations or additional content."},{"role":"user","content":"' . EscapeJSON(text) . '"}],"temperature":0.3}'
; 發送請求
try {
oHttp.Send(payload)
response := oHttp.ResponseText
; 解析JSON響應以獲取翻譯結果
; 假設回應格式類似 {"choices":[{"message":{"content":"翻譯結果"}}]}
if (RegExMatch(response, '"content"\s*:\s*"(.*?)"', &match)) {
return match[1]
} else {
MsgBox "API回應格式不符: " response
return text ; 返回原文
}
} catch as e {
MsgBox "API請求失敗: " e.Message
return text
}
}
; 輔助函數:轉義JSON字串中的特殊字符
EscapeJSON(str) {
str := StrReplace(str, "\", "\\")
str := StrReplace(str, '"', '\"')
str := StrReplace(str, "`n", "\n")
str := StrReplace(str, "`r", "\r")
str := StrReplace(str, "`t", "\t")
return str
}