Skip to content
Merged
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
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ tab.
directly, without selecting anything.
- **Local vocabulary study.** Eligible translated words and short phrases build
a private on-device wordbook with pronunciation, recall tracking, related-word
discovery, downloaded textbooks, and bidirectional practice.
discovery, downloaded textbooks, and bidirectional practice. When a selection
sits in a clear sentence, that sentence is saved as the word's example.
- **Stays out of the way.** Lives in the menu bar with no Dock icon, and can
start at login.
- **English and Simplified Chinese UI.** Switch immediately from Settings or
Expand Down Expand Up @@ -72,8 +73,9 @@ The study window brings four tools together:
including everyday, academic, TOEIC, business, and general-reference
vocabulary. A textbook hit is copied into your personal wordbook.
- **Related words** combines roots and shared meanings across your wordbook and
compatible downloaded textbooks, shows where every result came from, and lets
you add useful connections with one click.
compatible downloaded textbooks, shows the saved selection sentence when one
exists, and keeps English as the study anchor when Chinese is translated to
English.
- **Practice** tests word-to-meaning, meaning-to-word, or a random mix. Related
words and the active textbook supply more challenging distractors, while
mastered words leave the active queue until their recall fades.
Expand Down
2 changes: 1 addition & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
- 支持 Google Cloud、百度翻译和微软翻译;微软可选择全球或中国区云环境。
- API 凭据通过原生安全输入框保存到系统钥匙串,不会进入 WebView 或设置文件。
- 原文和译文均可使用系统语音朗读。
- 自动建立本地个人词库,支持词性、发音、相关词、双向测试和记忆分数。
- 自动建立本地个人词库,支持词性、发音、相关词、双向测试和记忆分数;划词时若能确定所在句子,会把该句保存为例句,中译英词条以英文为相关词锚点
- 内置一册离线的英汉入门词书;无需下载、无需配置 API 即可开始学习。

![个人词库、记忆分数、词性和发音按钮](docs/screenshots/vocabulary-study-wordbook.png)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "desktop-translator",
"private": true,
"version": "0.4.1",
"version": "0.5.0",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "desktop-translator"
version = "0.4.1"
version = "0.5.0"
description = "Lightweight cross-platform selection translator"
authors = ["Desktop Translator Contributors"]
edition = "2021"
Expand Down
23 changes: 23 additions & 0 deletions src-tauri/src/contracts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ pub struct PhysicalRect {
pub struct SelectionSnapshot {
pub id: u64,
pub text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub example_sentence: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub source_application_id: Option<String>,
pub bounds_physical_px: Vec<PhysicalRect>,
Expand Down Expand Up @@ -186,6 +188,8 @@ impl UserSettings {
pub struct TranslationRequest {
pub selection_id: u64,
pub text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub example_sentence: Option<String>,
pub source_language: LanguageCode,
pub target_language: LanguageCode,
}
Expand All @@ -211,6 +215,8 @@ pub struct VocabularyEntry {
pub id: i64,
pub source_text: String,
pub translated_text: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub example_sentence: Option<String>,
pub requested_source_language: LanguageCode,
pub effective_source_language: LanguageCode,
pub target_language: LanguageCode,
Expand Down Expand Up @@ -502,6 +508,7 @@ impl ValidateContract for SelectionSnapshot {
if self.id > JS_SAFE_INTEGER_MAX
|| self.captured_at_epoch_ms > JS_SAFE_INTEGER_MAX
|| self.text.trim().is_empty()
|| !valid_example_sentence(self.example_sentence.as_deref())
{
return Err(validation_error("selection violates schema constraints"));
}
Expand Down Expand Up @@ -553,6 +560,7 @@ impl ValidateContract for TranslationRequest {
|| self.text.trim().is_empty()
|| self.source_language.trim().is_empty()
|| self.target_language.trim().is_empty()
|| !valid_example_sentence(self.example_sentence.as_deref())
{
return Err(validation_error(
"translation request contains an empty field",
Expand All @@ -562,6 +570,10 @@ impl ValidateContract for TranslationRequest {
}
}

fn valid_example_sentence(value: Option<&str>) -> bool {
value.is_none_or(|value| !value.trim().is_empty() && value.chars().count() <= 5_000)
}

impl ValidateContract for TranslationResult {
fn validate(&self) -> Result<(), AppError> {
if self.selection_id > JS_SAFE_INTEGER_MAX
Expand Down Expand Up @@ -696,6 +708,16 @@ mod tests {

assert!(decode_validated::<SelectionSnapshot>(invalid).is_err());

let invalid_example = r#"{
"id": 1,
"text": "word",
"exampleSentence": " ",
"boundsPhysicalPx": [{"x": 0.0, "y": 0.0, "width": 1.0, "height": 1.0}],
"anchorPhysicalPx": {"x": 0.0, "y": 0.0, "width": 1.0, "height": 1.0},
"capturedAtEpochMs": 1
}"#;
assert!(decode_validated::<SelectionSnapshot>(invalid_example).is_err());

let invalid_result = r#"{
"selectionId": 1,
"translatedText": "hola",
Expand Down Expand Up @@ -724,6 +746,7 @@ mod tests {
let request = TranslationRequest {
selection_id: 7,
text: "persistence".into(),
example_sentence: None,
source_language: "auto".into(),
target_language: "en".into(),
};
Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/coordinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ mod tests {
SelectionSnapshot {
id,
text: "selected".into(),
example_sentence: None,
source_application_id: None,
bounds_physical_px: vec![bounds],
anchor_physical_px: bounds,
Expand Down
5 changes: 5 additions & 0 deletions src-tauri/src/integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ fn selection() -> SelectionSnapshot {
SelectionSnapshot {
id: 7,
text: "hello".into(),
example_sentence: None,
source_application_id: Some("fixture.app".into()),
bounds_physical_px: vec![bounds],
anchor_physical_px: bounds,
Expand All @@ -173,6 +174,7 @@ fn request() -> TranslationRequest {
TranslationRequest {
selection_id: 7,
text: "hello".into(),
example_sentence: None,
source_language: "auto".into(),
target_language: "es".into(),
}
Expand Down Expand Up @@ -287,6 +289,7 @@ async fn typed_input_translates_without_a_native_selection() {
.translate_input(TranslationRequest {
selection_id: 0,
text: "hello".into(),
example_sentence: None,
source_language: "auto".into(),
target_language: "es".into(),
})
Expand Down Expand Up @@ -314,6 +317,7 @@ async fn typed_input_rejects_an_empty_request() {
.translate_input(TranslationRequest {
selection_id: 0,
text: " ".into(),
example_sentence: None,
source_language: "auto".into(),
target_language: "es".into(),
})
Expand Down Expand Up @@ -389,6 +393,7 @@ async fn selection_does_not_call_provider_until_explicit_translate() {
let request = TranslationRequest {
selection_id: selected.id,
text: selected.text.clone(),
example_sentence: selected.example_sentence.clone(),
source_language: "auto".into(),
target_language: "es".into(),
};
Expand Down
Loading
Loading