今天我讓整個翻譯頁面正式「活起來」了!
原本只是靜態介面,現在可以輸入文字,選擇中翻英或英翻中,按下按鈕後就能即時取得翻譯結果,並且會自動記錄翻譯歷史。
這次我改用MyMemory Translation API。它是一個免費、免註冊的公開翻譯API,不用擔心金鑰或額度問題。這個API的呼叫方式非常簡單,只要使用fetch傳入查詢字串,就能立即拿到翻譯結果,非常適合練習階段使用。
const response = await fetch(
`https://api.mymemory.translated.net/get?q=${encodeURIComponent(input)}&langpair=${source}|${target}`
)
const data = await response.json()
setOutput(data.responseData.translatedText)
const [source, target] = direction === "en2zh" ? ["en", "zh-TW"] : ["zh-TW", "en"]
翻譯紀錄管理
每次翻譯都會把結果存入 history 陣列,可以查看之前的翻譯並刪除。
UX優化
翻譯中會顯示「翻譯中...」
若翻譯錯誤會顯示紅字警告
自動清空輸入框與翻譯結果
//TranslationTool.jsx
import { useState } from "react"
export default function TranslationTool() {
const [input, setInput] = useState("")
const [output, setOutput] = useState("")
const [loading, setLoading] = useState(false)
const [error, setError] = useState("")
const [history, setHistory] = useState([])
const [direction, setDirection] = useState("en2zh")
const handleTranslate = async () => {
if (!input.trim()) return
setLoading(true)
setError("")
setOutput("")
const [source, target] =
direction === "en2zh" ? ["en", "zh-TW"] : ["zh-TW", "en"]
try {
// 直接使用 MyMemory Translation API
const response = await fetch(
`https://api.mymemory.translated.net/get?q=${encodeURIComponent(
input
)}&langpair=${source}|${target}`
)
const data = await response.json()
console.log("🌍 回傳資料:", data)
if (!data?.responseData?.translatedText)
throw new Error("API 回傳錯誤或內容無法解析")
const result = data.responseData.translatedText
setOutput(result)
const newRecord = {
id: Date.now(),
from: input,
to: result,
type: direction,
}
setHistory([newRecord, ...history])
setInput("")
} catch (err) {
console.error("❌ 翻譯錯誤:", err)
setError("⚠️ 翻譯失敗,請稍後再試")
} finally {
setLoading(false)
}
}
const handleDelete = (id) => {
setHistory(history.filter((item) => item.id !== id))
}
return (
<div className="space-y-6">
<div className="bg-white p-4 rounded shadow space-y-3">
<h2 className="text-lg font-semibold">🌐 多語言翻譯工具</h2>
<div className="flex space-x-2">
<select
value={direction}
onChange={(e) => setDirection(e.target.value)}
className="border px-2 py-1 rounded"
>
<option value="en2zh">英文 → 中文</option>
<option value="zh2en">中文 → 英文</option>
</select>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="輸入要翻譯的文字..."
className="flex-1 border px-3 py-2 rounded"
/>
<button
onClick={handleTranslate}
disabled={loading}
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 disabled:opacity-50"
>
{loading ? "翻譯中..." : "翻譯"}
</button>
</div>
{error && <p className="text-red-500 text-sm">{error}</p>}
{output && (
<div className="mt-4 bg-gray-100 p-3 rounded">
<p className="text-gray-700 font-medium">翻譯結果:</p>
<p>{output}</p>
</div>
)}
</div>
<div className="bg-white p-4 rounded shadow">
<h3 className="font-semibold mb-2">📜 翻譯紀錄</h3>
{history.length === 0 ? (
<p className="text-gray-500 text-sm">目前沒有紀錄。</p>
) : (
<ul className="space-y-2">
{history.map((item) => (
<li
key={item.id}
className="flex justify-between bg-gray-50 p-2 rounded border"
>
<div>
<p className="text-sm text-gray-600">
<strong>原文:</strong> {item.from}
</p>
<p className="text-sm text-gray-800">
<strong>譯文:</strong> {item.to}
</p>
</div>
<button
onClick={() => handleDelete(item.id)}
className="text-red-600 hover:text-red-800 text-sm"
>
刪除
</button>
</li>
))}
</ul>
)}
</div>
</div>
)
}
// vite.config.js
import { defineConfig } from "vite"
import react from "@vitejs/plugin-react"
import tailwindcss from "@tailwindcss/vite"
export default defineConfig({
plugins: [react(), tailwindcss()],
server: {
port: 5173,
},
})