yummyTranslator

Yuan.Sn

最近阅读英文文献,发现原有翻译流 (沙拉划词+Quiker) 由于Chrome安全策略原因失效。加之本人有学习英语的兴趣及其阅读英文文档的需求,萌生了开发一款 Translator Hub的想法。因此 Yummy Translator 诞生。此LOG记录整个项目的开发历程。

8.27(建项、架构确立)

通过与 Gemini 的”深入交流“,确定了以 React + Tauri的架构开发

模块 核心组件 技术选型/设计模式 主要职责与优势
应用框架 桌面端应用 Tauri (Rust + Web UI) 高性能、低资源占用,提供原生级的体验。
表现层 用户界面 (UI) Web技术 (React) 利用现代前端生态,实现高效、美观、灵活的界面开发。
核心逻辑层 Rust 后端 Tauri Command & Event System Rust保证了后端的稳定、安全和高并发处理能力。
服务层 翻译服务 适配器模式 (Adapter Pattern) 高扩展性、低耦合,未来增加新的翻译源无需修改核心代码。
服务层 输入修正服务 纯本地离线库 瞬时响应、完全离线、保护隐私,提升核心输入体验。
数据层 本地化存储 SQLite 数据库 功能强大、数据安全,为未来复杂的单词本功能铺平道路。

以 MVP的策略开发,先构建第一版v1 实现最基础的翻译功能

  • 程序主界面
  • 翻译结果界面
  • 翻译API接入
  • 本地化存储词典
  • 输入修正(修正 多余复制进去的标点、以及拼错情况)

9.14 (Prompt工程)

对于 React + Tauri 的前端开发, 本人的只有些许 H5 DOM的开发经验。为了提升整个开发效率,采用高度依赖Ai agent的形式开发。由于成本限制 本项目主要采用 Github Copilot 免费模型开发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/*
* Project Brief for GitHub Copilot
*
* Project Name: Yummy Translator
*
* ## 1. Project Vision
* A high-performance, multi-engine desktop translation application built with Tauri. The primary goal is to provide English learners and professionals with accurate and context-rich translations by aggregating results from multiple sources. The application must be fast, lightweight, and respect user privacy.
*
* ## 2. Core Features (Version 1.0)
* - Main UI: A clean interface for text input and displaying multiple translation results side-by-side.
* - Multi-API Aggregation: Fetch and display translation results simultaneously from different providers (e.g., Google Translate, DeepL, etc.).
* - Input Correction:
* - Automatically sanitize pasted text (e.g., remove extra line breaks).
* - Provide fast, local spell-checking for English input.
* - Local Storage:
* - Store translation history.
* - A basic vocabulary book to save words and phrases.
*
* ## 3. V1 Technical Architecture & Decisions
* We have made the following architectural decisions. Please generate code that aligns with this stack and these patterns.
*
* - **Framework:** Tauri (Rust backend, web-based frontend).
* - **Backend Language:** Rust.
* - **Frontend Framework:** Modern web framework (e.g., Vue.js or React) using TypeScript.
* - **Database:** SQLite for all local data storage.
* - It will manage tables for `settings`, `translation_history`, and `vocabulary_book`.
*
* - **Design Pattern for Translation APIs:** We will use the **Adapter Pattern**.
* - A core `Translator` trait will define the common interface.
* - Each translation service (Google, DeepL, etc.) will be implemented as a separate struct that implements this `Translator` trait.
* - Example of the core trait in Rust:
* ```rust
* pub trait Translator {
* fn name(&self) -> &str;
* async fn translate(&self, text: &str, source_lang: &str, target_lang: &str) -> Result<String, anyhow::Error>;
* }
* ```
*
* - **Input Correction Service:** This must be a **purely local and offline** feature for speed and privacy.
* - Use Rust's native string manipulation and regex for punctuation sanitization.
* - Use a local, dictionary-based Rust crate for spell-checking (e.g., a hunspell binding or similar). **Do not use online APIs for this feature.**
*
* - **State Management:** The Rust backend will be the single source of truth. The frontend will communicate with the backend via Tauri commands for data fetching and mutations.
*
* I will now start building this "Yummy Translator" project with your assistance based on the above specifications.
*/

输入关键词后,进过多次 prompt修正 形成第一版 DEMO页

2025-09-15 22-00-57_3
2025-09-15 22-00-57_3

9.15(架构逻辑)

学习了一下项目的基本结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
YUMMYTRANSLATOR
├── ......
├── src/
├── assets/
├── components/ #组件库
├── SearchBox/
├── SearchBox.tsx #主搜索框
└── SuggestionDropdown.tsx #下拉建议
└── SearchResult/
└── ResultPanel.tsx #结果页
├── App.css
├── App.tsx #状态管理、组件调用
├── main.tsx #程序入口
└── vite-env.d.ts
├── ......

采用了单向数据流 (One-Way Data Flow)状态提升 (Lifting State Up) 的架构方案,App 组件通过 props 将状态数据传递给子组件(SearchBoxSuggestionDropdown.tsx etc...),子组件再将UI样式回传回 App进行调用渲染。

9.16(组件及其参数)

剖析一下程序的组件及其参数

App.tsx

先把主要控制处理组件 App 列出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119

import React, { useState, useRef } from "react";
import { Box } from "@mui/material";
import SearchBox from "./components/SearchBox/SearchBox";
import SuggestionDropdown from "./components/SearchBox/SuggestionDropdown";
import ResultPanel from "./components/SearchResult/ResultPanel";

const SUGGESTIONS = [
"apple", "abandon", "absolutely", "ash", "accommodation", "banana", "cat", "dog", "elephant", "fish", "grape", "house", "ice", "juice",
"kite", "lemon", "mountain", "night", "orange", "pencil", "queen", "river", "sun", "tree", "umbrella", "violin", "water", "xylophone", "yogurt", "zebra",
"book", "car", "desk", "egg", "flower", "garden", "hat", "island", "jacket", "key", "lamp", "moon", "nest", "ocean", "pizza", "quiet", "road", "star", "train", "unicorn", "vase", "window", "yard", "zipper"
];

function App() {
/**
* @variable {string} searchValue - 当前搜索框输入内容
* @variable {boolean} showDropdown - 是否显示下拉建议
* @variable {boolean} isActivated - 是否激活结果区
* @variable {Array<{ name: string; result: string }>} results - 翻译结果列表
* @variable {React.RefObject<HTMLInputElement>} inputRef - 搜索框的引用
*/

const [searchValue, setSearchValue] = useState("");
const [showDropdown, setShowDropdown] = useState(false);
const [isActivated, setIsActivated] = useState(false);
const [results, setResults] = useState<Array<{ name: string; result: string }>>([]);
const inputRef = useRef<HTMLInputElement>(null);

// 输入内容 过滤建议
const filteredSuggestions = SUGGESTIONS.filter(
(word) => searchValue && word.startsWith(searchValue.toLowerCase())
);

// 模拟API返回结果(仅演示用)
function getApiResults(word: string): Array<{ name: string; result: string }> {
return [
{
name: "Google 翻译",
result: `${word} 的 Google 翻译结果占位。`
},
{
name: "DeepL 翻译",
result: `${word} 的 DeepL 翻译结果占位。`
},
{
name: "有道翻译",
result: `${word} 的有道翻译结果占位。`
}
];
}


// 处理下拉选项点击或回车选择
function handleSelect(word: string) {
setSearchValue(word);
setShowDropdown(false);
setIsActivated(true);
setResults(getApiResults(word));
}

// 处理输入框内容变化
function handleInputChange(e: React.ChangeEvent<HTMLInputElement>) {
const value = e.target.value;
setSearchValue(value);
setShowDropdown(!!value && filteredSuggestions.length > 0);
}

// 处理回车键事件
function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
if (e.key === "Enter" && searchValue) {
setShowDropdown(false);
setIsActivated(true);
setResults(getApiResults(searchValue));
}
}

return (
<Box
sx={{
minHeight: "100vh",
display: "flex",
flexDirection: "column",
alignItems: "center",
background: "none",
}}
>
{/* 搜索框和下拉建议 */}
<Box
sx={{
position: isActivated ? "fixed" : "absolute",
top: isActivated ? 32 : "50%",
left: "50%",
transform: isActivated ? "translate(-50%, 0)" : "translate(-50%, -50%)",
width: 400,
maxWidth: "90vw",
zIndex: 10,
transition: "top 0.5s, transform 0.5s",
}}
>
<SearchBox
value={searchValue}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
inputRef={inputRef}
/>
<SuggestionDropdown
suggestions={showDropdown ? filteredSuggestions : []}
onSelect={handleSelect}
/>
</Box>

{/* 结果区 */}
<ResultPanel results={results} />
</Box>
);
}

export default App;

组件 (Components)

  • Box: (来自 @mui/material) 用于布局和样式化的容器组件

  • SearchBox: 自定义的搜索框组件

  • SuggestionDropdown:自定义的搜索建议下拉框组件

  • ResultPanel: 自定义的结果展示面板组件。

变量 / 状态 (Variables / State)

  • SUGGESTIONS(arr): 一个建议单词测试数组,数量优先 后期会修改

  • filteredSuggestions(arr):根据 searchValue 从 SUGGESTIONS 中过滤出的建议列表。

  • searchValue (String): 存储当前搜索框中的输入内容

  • showDropdown(Boolen):是否显示下拉建议

  • isActivated(Boolen): 是否处于结果展示模式

  • results(arr): 存储模拟 API 返回的翻译结果 ,后期修改接入API

  • inputRef(useRef): 获取 SearchBox 内部 input 元素的直接引用

函数 (Functions)

  • getApiResults(word): 模拟 API 请求,根据输入的单词返回一个包含多个翻译结果的数组。

  • handleSelect(word): 处理用户在下拉建议中选择一项的逻辑。

  • handleInputChange(e): 处理输入框内容变化的逻辑。

  • handleKeyDown(e): 处理在输入框中按下键盘(特别是回车键)的逻辑。

SearchBox.tsx

然后是搜索组件 SerchBox

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import React from "react";
import { Box, TextField } from "@mui/material";
import SearchIcon from "@mui/icons-material/Search";

interface SearchBoxProps {
value: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
onKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => void;
inputRef: React.RefObject<HTMLInputElement | null>;
}

// 搜索框组件
const SearchBox: React.FC<SearchBoxProps> = ({ value, onChange, onKeyDown, inputRef }) => (
<Box
sx={{
display: "flex",
alignItems: "center",
boxShadow: 2,
px: 2,
py: 1.5,
borderRadius: "32px",
background: "#fff",
border: "1px solid #e0e0e0",
}}
>
<SearchIcon color="primary" sx={{ mr: 2 }} />
<TextField
inputRef={inputRef}
fullWidth
placeholder="请输入查询单词..."
value={value}
onChange={onChange}
onKeyDown={onKeyDown}
autoFocus
variant="standard"
InputProps={{
disableUnderline: true,
sx: {
fontSize: "1.25rem",
background: "none",
},
}}
/>
</Box>
);

export default SearchBox;

组件 (Components)

  • SearchBox: 组件本身,渲染带样式的输入框

  • Box: (@mui/material) 作为 SearchBox 的容器

  • TextField: (@mui/material) 核心的文本输入框组件

  • SearchIcon: (@mui/icons-material/Search) 搜索图标组件

参数 (Parameters)

  • SearchBox 组件通过 Props 接收参数:

    • value: (string) 需要在输入框中显示的值。

    • onChange: (function) 当输入框内容改变时需要触发的回调函数。

    • onKeyDown: (function) 当在输入框中按下按键时需要触发的回调函数。

    • inputRef: (object) 从父组件传递过来的 ref 对象。

SuggestionDropdown.tsx

再是搜索建议组件 SuggestionDropdown

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import React from "react";
import { Box, List, ListItem, ListItemButton } from "@mui/material";

interface SuggestionDropdownProps {
suggestions: string[];
onSelect: (word: string) => void;
}

// 下拉建议组件
const SuggestionDropdown: React.FC<SuggestionDropdownProps> = ({ suggestions, onSelect }) => {
if (suggestions.length === 0) return null;
return (
<Box
sx={{
mt: 1,
boxShadow: 3,
borderRadius: "22px",
background: "#fff",
border: "1px solid #e0e0e0",
maxHeight: 300,
overflowY: "auto",
}}
>
<List sx={{ p: 0 }}>
{suggestions.map((word) => (
<ListItem key={word} disablePadding sx={{ borderRadius: "24px" }}>
<ListItemButton
onClick={() => onSelect(word)}
sx={{
borderRadius: "24px",
px: 3,
py: 1.5,
fontSize: "1.1rem",
color: "#333",
'&:hover': {
background: "#f5f5f5",
},
}}
>
{word}
</ListItemButton>
</ListItem>
))}
</List>
</Box>
);
};

export default SuggestionDropdown;

组件 (Components)

  • Box, List, ListItem, ListItemButton: (@mui/material) 构建列表结构的 UI 组件

参数 (Parameters)

  • SuggestionDropdown 组件通过 Props 接收参数:

    • suggestions: (string[]) 需要在列表中显示的建议单词数组

    • onSelect: (function) 当用户点击某一个建议项时需要触发的回调函数

  • 在 .map() 循环内部:

    • word: 代表 suggestions 数组中当前正在被渲染的单词

ResultPanel.tsx

最后是结果页 ResultPanel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import React from "react";
import { Box, Paper, Typography, TextField } from "@mui/material";

interface ResultPanelProps {
results: Array<{ name: string; result: string }>;
}

// 结果区组件
const ResultPanel: React.FC<ResultPanelProps> = ({ results }) => {
if (results.length === 0) return null;
return (
<Box sx={{ width: 400, maxWidth: "90vw", mt: 16 }}>
{results.map((api) => (
<Paper key={api.name} elevation={2} sx={{ p: 3, mb: 3, borderRadius: "18px" }}>
<Typography variant="h6" color="primary" sx={{ mb: 1 }}>
{api.name}
</Typography>
<TextField
multiline
fullWidth
value={api.result}
InputProps={{
readOnly: true,
sx: { fontSize: "1.1rem", background: "none" },
}}
variant="outlined"
/>
</Paper>
))}
</Box>
);
};

export default ResultPanel;

组件 (Components)

  • Box, Paper, Typography, TextField: (@mui/material`) 用于构建结果卡片样式的 UI 组件

参数 (Parameters)

  • ResultPanel 组件通过 Props 接收参数:
    • results: (Array) 包含多个翻译结果对象的数组。
  • .map() 循环内部:
    • api: 代表 results 数组中当前正在被渲染的那个结果对象 (e.g., { name: "Google 翻译", result: "..." })。
Comments