feat(ui): localize commit AI and git error messages to English
Translate commit AI prompts, model labels, and git/keychain errors to English so the app and generated messages are consistent. Also add a discard confirmation dialog and update the UI text/styles to match the new flow. - src-tauri/crates/commit_ai/src/cloud.rs - Translate HTTP and API error messages to English. - Keep request timeout and token sizing behavior unchanged. - src-tauri/crates/commit_ai/src/lib.rs - Translate model labels, prompt text, and validation errors. - Keep diff truncation and message sanitization logic intact. - src-tauri/src/git.rs - Translate git, credential, merge, and history errors. - Update AI provider validation messages to English. - src/lib/components/* - Update AI settings, commit panel, credential, and loading UI text. - Add discard confirmation dialog for destructive actions. - src/App.svelte, src/app.css - Adjust app layout and styling for the new dialog and text changes.
This commit is contained in:
@@ -4,16 +4,16 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{build_messages, sanitize_message};
|
||||
|
||||
// Großzügig bemessen, damit ein ausführlicher Body mit Stichpunkten nicht abgeschnitten wird.
|
||||
// Generous sizing so a detailed body with bullet points isn't cut off.
|
||||
const DEFAULT_MAX_TOKENS: u32 = 1500;
|
||||
// Ohne Timeout würde ein hängender Endpoint den "AI"-Button dauerhaft blockieren.
|
||||
// Without a timeout, a hanging endpoint would permanently block the "AI" button.
|
||||
const REQUEST_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
|
||||
fn http_client() -> Result<reqwest::Client, String> {
|
||||
reqwest::Client::builder()
|
||||
.timeout(REQUEST_TIMEOUT)
|
||||
.build()
|
||||
.map_err(|err| format!("HTTP-Client konnte nicht erstellt werden: {err}"))
|
||||
.map_err(|err| format!("Could not create HTTP client: {err}"))
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -71,19 +71,19 @@ async fn openai_compatible_request(
|
||||
let response = request
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| format!("Anfrage an das KI-Modell fehlgeschlagen: {err}"))?;
|
||||
.map_err(|err| format!("Request to the AI model failed: {err}"))?;
|
||||
let status = response.status();
|
||||
let text = response
|
||||
.text()
|
||||
.await
|
||||
.map_err(|err| format!("Antwort konnte nicht gelesen werden: {err}"))?;
|
||||
.map_err(|err| format!("Could not read response: {err}"))?;
|
||||
|
||||
if !status.is_success() {
|
||||
return Err(format!("API-Fehler ({status}): {text}"));
|
||||
return Err(format!("API error ({status}): {text}"));
|
||||
}
|
||||
|
||||
let parsed: OpenAiResponse = serde_json::from_str(&text)
|
||||
.map_err(|err| format!("Antwort konnte nicht verarbeitet werden: {err}"))?;
|
||||
.map_err(|err| format!("Could not process response: {err}"))?;
|
||||
|
||||
parsed
|
||||
.choices
|
||||
@@ -92,7 +92,7 @@ async fn openai_compatible_request(
|
||||
.and_then(|choice| choice.message.content)
|
||||
.map(|content| sanitize_message(&content))
|
||||
.filter(|content| !content.is_empty())
|
||||
.ok_or_else(|| "Das Modell hat keine Antwort geliefert.".to_string())
|
||||
.ok_or_else(|| "The model did not return a response.".to_string())
|
||||
}
|
||||
|
||||
pub async fn generate_openai(
|
||||
@@ -102,7 +102,7 @@ pub async fn generate_openai(
|
||||
notes: Option<&str>,
|
||||
) -> Result<String, String> {
|
||||
if api_key.trim().is_empty() {
|
||||
return Err("OpenAI-API-Key fehlt.".to_string());
|
||||
return Err("OpenAI API key is missing.".to_string());
|
||||
}
|
||||
openai_compatible_request(
|
||||
"https://api.openai.com/v1/chat/completions".to_string(),
|
||||
@@ -122,7 +122,7 @@ pub async fn generate_custom(
|
||||
notes: Option<&str>,
|
||||
) -> Result<String, String> {
|
||||
if base_url.trim().is_empty() {
|
||||
return Err("Endpoint-URL fehlt.".to_string());
|
||||
return Err("Endpoint URL is missing.".to_string());
|
||||
}
|
||||
let url = format!("{}/chat/completions", base_url.trim_end_matches('/'));
|
||||
openai_compatible_request(url, api_key, model, diff, notes).await
|
||||
@@ -161,7 +161,7 @@ pub async fn generate_anthropic(
|
||||
notes: Option<&str>,
|
||||
) -> Result<String, String> {
|
||||
if api_key.trim().is_empty() {
|
||||
return Err("Anthropic-API-Key fehlt.".to_string());
|
||||
return Err("Anthropic API key is missing.".to_string());
|
||||
}
|
||||
let (system, user) = build_messages(diff, notes)?;
|
||||
let body = AnthropicRequest {
|
||||
@@ -179,19 +179,19 @@ pub async fn generate_anthropic(
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| format!("Anfrage an Anthropic fehlgeschlagen: {err}"))?;
|
||||
.map_err(|err| format!("Request to Anthropic failed: {err}"))?;
|
||||
let status = response.status();
|
||||
let text = response
|
||||
.text()
|
||||
.await
|
||||
.map_err(|err| format!("Antwort konnte nicht gelesen werden: {err}"))?;
|
||||
.map_err(|err| format!("Could not read response: {err}"))?;
|
||||
|
||||
if !status.is_success() {
|
||||
return Err(format!("API-Fehler ({status}): {text}"));
|
||||
return Err(format!("API error ({status}): {text}"));
|
||||
}
|
||||
|
||||
let parsed: AnthropicResponse = serde_json::from_str(&text)
|
||||
.map_err(|err| format!("Antwort konnte nicht verarbeitet werden: {err}"))?;
|
||||
.map_err(|err| format!("Could not process response: {err}"))?;
|
||||
|
||||
parsed
|
||||
.content
|
||||
@@ -199,5 +199,5 @@ pub async fn generate_anthropic(
|
||||
.find_map(|block| block.text)
|
||||
.map(|text| sanitize_message(&text))
|
||||
.filter(|text| !text.is_empty())
|
||||
.ok_or_else(|| "Das Modell hat keine Antwort geliefert.".to_string())
|
||||
.ok_or_else(|| "The model did not return a response.".to_string())
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ pub const DEFAULT_LOCAL_MODEL_ID: &str = "qwen2.5-1.5b";
|
||||
pub const LOCAL_MODELS: &[LocalModelOption] = &[
|
||||
LocalModelOption {
|
||||
id: "qwen2.5-0.5b",
|
||||
label: "Qwen2.5 0.5B Instruct — schnell, geringere Qualität",
|
||||
label: "Qwen2.5 0.5B Instruct — fast, lower quality",
|
||||
approx_size_mb: 490,
|
||||
repo: "Qwen/Qwen2.5-0.5B-Instruct-GGUF",
|
||||
file: "qwen2.5-0.5b-instruct-q4_k_m.gguf",
|
||||
@@ -32,7 +32,7 @@ pub const LOCAL_MODELS: &[LocalModelOption] = &[
|
||||
},
|
||||
LocalModelOption {
|
||||
id: "qwen2.5-1.5b",
|
||||
label: "Qwen2.5 1.5B Instruct — empfohlen",
|
||||
label: "Qwen2.5 1.5B Instruct — recommended",
|
||||
approx_size_mb: 1050,
|
||||
repo: "Qwen/Qwen2.5-1.5B-Instruct-GGUF",
|
||||
file: "qwen2.5-1.5b-instruct-q4_k_m.gguf",
|
||||
@@ -40,7 +40,7 @@ pub const LOCAL_MODELS: &[LocalModelOption] = &[
|
||||
},
|
||||
LocalModelOption {
|
||||
id: "qwen2.5-3b",
|
||||
label: "Qwen2.5 3B Instruct — beste Qualität, langsamer",
|
||||
label: "Qwen2.5 3B Instruct — best quality, slower",
|
||||
approx_size_mb: 2100,
|
||||
repo: "Qwen/Qwen2.5-3B-Instruct-GGUF",
|
||||
file: "qwen2.5-3b-instruct-q4_k_m.gguf",
|
||||
@@ -128,7 +128,7 @@ impl CommitAiEngine {
|
||||
let mut guard = self.inner.write().await;
|
||||
guard.phase = CommitAiPhase::Error;
|
||||
guard.model_id = Some(model_id.to_string());
|
||||
guard.error = Some(format!("Unbekanntes lokales Modell: {model_id}"));
|
||||
guard.error = Some(format!("Unknown local model: {model_id}"));
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -174,7 +174,7 @@ impl CommitAiEngine {
|
||||
let guard = self.inner.read().await;
|
||||
match (guard.phase, &guard.model) {
|
||||
(CommitAiPhase::Ready, Some(model)) => model.clone(),
|
||||
_ => return Err("Das lokale KI-Modell ist noch nicht bereit.".to_string()),
|
||||
_ => return Err("The local AI model is not ready yet.".to_string()),
|
||||
}
|
||||
};
|
||||
|
||||
@@ -192,11 +192,11 @@ impl CommitAiEngine {
|
||||
.choices
|
||||
.first()
|
||||
.and_then(|choice| choice.message.content.clone())
|
||||
.ok_or_else(|| "Das Modell hat keine Antwort geliefert.".to_string())?;
|
||||
.ok_or_else(|| "The model did not return a response.".to_string())?;
|
||||
|
||||
let message = sanitize_message(&content);
|
||||
if message.is_empty() {
|
||||
return Err("Das Modell hat keine Antwort geliefert.".to_string());
|
||||
return Err("The model did not return a response.".to_string());
|
||||
}
|
||||
Ok(message)
|
||||
}
|
||||
@@ -223,36 +223,36 @@ pub(crate) fn sanitize_message(raw: &str) -> String {
|
||||
|
||||
pub(crate) fn build_messages(diff: &str, notes: Option<&str>) -> Result<(String, String), String> {
|
||||
if diff.trim().is_empty() {
|
||||
return Err("Keine gestagten Änderungen für eine Commit-Message vorhanden.".to_string());
|
||||
return Err("No staged changes available for a commit message.".to_string());
|
||||
}
|
||||
|
||||
// grobe Token-Schätzung, kleine Modelle haben oft 8–32k Kontext
|
||||
// Rough token estimate — small models often have an 8-32k context window.
|
||||
const MAX_CHARS: usize = 24_000;
|
||||
let diff = if diff.len() > MAX_CHARS {
|
||||
// Byte-Index auf eine gültige UTF-8-Zeichengrenze zurückziehen, sonst
|
||||
// paniken Slices mitten in einem Umlaut o. Ä.
|
||||
// Pull the byte index back to a valid UTF-8 char boundary, otherwise
|
||||
// slicing mid-multi-byte-character would panic.
|
||||
let mut cut = MAX_CHARS;
|
||||
while !diff.is_char_boundary(cut) {
|
||||
cut -= 1;
|
||||
}
|
||||
format!("{}\n\n[... Diff gekürzt ...]", &diff[..cut])
|
||||
format!("{}\n\n[... diff truncated ...]", &diff[..cut])
|
||||
} else {
|
||||
diff.to_string()
|
||||
};
|
||||
|
||||
let system = "Du bist ein Werkzeug, das Git-Commit-Messages erzeugt. \
|
||||
Antworte ausschließlich mit der Commit-Message im Conventional-Commits-Format \
|
||||
(<type>(<scope>): <subject>), gefolgt von einem Body nach einer Leerzeile. \
|
||||
Subject imperativ, max. 72 Zeichen. \
|
||||
Der Body ist Pflicht: Fasse in einem kurzen Absatz zusammen, was und warum geändert wurde, \
|
||||
und liste danach die wesentlichen Änderungen als Stichpunkte (- ...) auf, \
|
||||
gruppiert nach betroffenem Bereich/Datei. Zeilen im Body max. 72 Zeichen. \
|
||||
Kein Vorspann, keine Erklärung, keine Code-Fences, in Englisch antworten"
|
||||
let system = "You are a tool that generates Git commit messages. \
|
||||
Respond only with the commit message in Conventional Commits format \
|
||||
(<type>(<scope>): <subject>), followed by a body after a blank line. \
|
||||
Subject in imperative mood, max. 72 characters. \
|
||||
The body is required: summarize in a short paragraph what changed and why, \
|
||||
then list the key changes as bullet points (- ...), \
|
||||
grouped by affected area/file. Lines in the body max. 72 characters. \
|
||||
No preamble, no explanation, no code fences, answer in English"
|
||||
.to_string();
|
||||
|
||||
let mut user = String::new();
|
||||
if let Some(n) = notes.filter(|n| !n.trim().is_empty()) {
|
||||
user.push_str(&format!("Anmerkungen des Entwicklers:\n{n}\n\n"));
|
||||
user.push_str(&format!("Developer notes:\n{n}\n\n"));
|
||||
}
|
||||
user.push_str(&format!("Staged diff:\n{diff}"));
|
||||
Ok((system, user))
|
||||
|
||||
Reference in New Issue
Block a user