From 0cd3a00abfa755ac6cdb39d8bc80f98459542e59 Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Sat, 4 Apr 2026 21:03:42 +0200 Subject: [PATCH] fix: wired engines that exist to UI --- crates/bds-core/src/engine/calendar.rs | 86 +++++++ crates/bds-core/src/engine/mod.rs | 3 + crates/bds-core/src/engine/search.rs | 161 +++++++++++++ .../src/engine/validate_translations.rs | 196 ++++++++++++++++ crates/bds-ui/src/app.rs | 222 +++++++++++++++++- locales/ui/de.json | 24 +- locales/ui/en.json | 24 +- locales/ui/es.json | 24 +- locales/ui/fr.json | 24 +- locales/ui/it.json | 24 +- 10 files changed, 772 insertions(+), 16 deletions(-) create mode 100644 crates/bds-core/src/engine/calendar.rs create mode 100644 crates/bds-core/src/engine/search.rs create mode 100644 crates/bds-core/src/engine/validate_translations.rs diff --git a/crates/bds-core/src/engine/calendar.rs b/crates/bds-core/src/engine/calendar.rs new file mode 100644 index 0000000..bc2bea6 --- /dev/null +++ b/crates/bds-core/src/engine/calendar.rs @@ -0,0 +1,86 @@ +//! Calendar JSON generation — counts posts by year, month, day. + +use std::collections::BTreeMap; +use std::path::Path; + +use rusqlite::Connection; + +use crate::db::queries::post as post_q; +use crate::engine::EngineResult; +use crate::model::PostStatus; +use crate::util::{atomic_write, timestamp}; + +/// Generate `html/calendar.json` from published posts. +/// +/// The JSON has three top-level objects: `years`, `months`, `days`, +/// each mapping a date key to a count of posts. +pub fn regenerate_calendar( + conn: &Connection, + data_dir: &Path, + project_id: &str, +) -> EngineResult<()> { + let posts = post_q::list_posts_by_project(conn, project_id)?; + + let mut years: BTreeMap = BTreeMap::new(); + let mut months: BTreeMap = BTreeMap::new(); + let mut days: BTreeMap = BTreeMap::new(); + + for post in &posts { + if post.status != PostStatus::Published { + continue; + } + let ts = post.created_at; + let (y, m, d) = timestamp::year_month_day_from_unix_ms(ts); + + let year_key = y.clone(); + let month_key = format!("{y}-{m}"); + let day_key = format!("{y}-{m}-{d}"); + + *years.entry(year_key).or_insert(0) += 1; + *months.entry(month_key).or_insert(0) += 1; + *days.entry(day_key).or_insert(0) += 1; + } + + let calendar = serde_json::json!({ + "years": years, + "months": months, + "days": days, + }); + + let html_dir = data_dir.join("html"); + std::fs::create_dir_all(&html_dir)?; + + let json_str = serde_json::to_string_pretty(&calendar)?; + + atomic_write::atomic_write(&html_dir.join("calendar.json"), json_str.as_bytes())?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::db::Database; + use crate::engine; + + #[test] + fn calendar_empty_project() { + let db = Database::open_in_memory().unwrap(); + let _ = db.migrate(); + + let tmp = tempfile::tempdir().unwrap(); + let p = engine::project::create_project( + db.conn(), "Test", Some(tmp.path().to_str().unwrap()), + ).unwrap(); + + regenerate_calendar(db.conn(), tmp.path(), &p.id).unwrap(); + + let cal_path = tmp.path().join("html").join("calendar.json"); + assert!(cal_path.exists()); + + let data: serde_json::Value = serde_json::from_str( + &std::fs::read_to_string(&cal_path).unwrap() + ).unwrap(); + assert!(data["years"].as_object().unwrap().is_empty()); + } +} diff --git a/crates/bds-core/src/engine/mod.rs b/crates/bds-core/src/engine/mod.rs index b276c2f..7217357 100644 --- a/crates/bds-core/src/engine/mod.rs +++ b/crates/bds-core/src/engine/mod.rs @@ -11,6 +11,9 @@ pub mod script_rebuild; pub mod task; pub mod metadata_diff; pub mod rebuild; +pub mod search; +pub mod calendar; +pub mod validate_translations; pub use error::{EngineError, EngineResult}; pub use context::EngineContext; diff --git a/crates/bds-core/src/engine/search.rs b/crates/bds-core/src/engine/search.rs new file mode 100644 index 0000000..34a1293 --- /dev/null +++ b/crates/bds-core/src/engine/search.rs @@ -0,0 +1,161 @@ +//! Full-text search reindexing engine functions. + +use rusqlite::Connection; + +use crate::db::fts; +use crate::db::queries::{media as media_q, media_translation, post as post_q, post_translation}; +use crate::engine::EngineResult; + +/// Result of a full reindex operation. +pub struct ReindexReport { + pub posts_indexed: usize, + pub media_indexed: usize, +} + +/// Drop and rebuild the entire FTS index for all posts and media in a project. +pub fn reindex_all( + conn: &Connection, + project_id: &str, + main_language: &str, +) -> EngineResult { + // Wipe existing FTS content + conn.execute("DELETE FROM posts_fts", [])?; + conn.execute("DELETE FROM media_fts", [])?; + + // Reindex all posts + let posts = post_q::list_posts_by_project(conn, project_id)?; + + let mut posts_indexed = 0; + for post in &posts { + let translations = post_translation::list_post_translations_by_post(conn, &post.id)?; + + let trans_pairs: Vec<(String, String)> = translations + .iter() + .map(|t| { + let text = [ + t.title.as_str(), + t.excerpt.as_deref().unwrap_or(""), + t.content.as_deref().unwrap_or(""), + ] + .join(" "); + (text, t.language.clone()) + }) + .collect(); + + let language = post.language.as_deref().unwrap_or(main_language); + fts::index_post( + conn, + &post.id, + &post.title, + post.excerpt.as_deref(), + post.content.as_deref(), + &post.tags, + &post.categories, + &trans_pairs, + language, + )?; + + posts_indexed += 1; + } + + // Reindex all media + let media_items = media_q::list_media_by_project(conn, project_id)?; + + let mut media_indexed = 0; + for m in &media_items { + let translations = media_translation::list_media_translations_by_media(conn, &m.id)?; + + let trans_pairs: Vec<(String, String)> = translations + .iter() + .map(|t| { + let text = [ + t.title.as_deref().unwrap_or(""), + t.alt.as_deref().unwrap_or(""), + t.caption.as_deref().unwrap_or(""), + ] + .join(" "); + (text, t.language.clone()) + }) + .collect(); + + let language = m.language.as_deref().unwrap_or(main_language); + fts::index_media( + conn, + &m.id, + m.title.as_deref(), + m.alt.as_deref(), + m.caption.as_deref(), + &m.original_name, + &m.tags, + &trans_pairs, + language, + )?; + + media_indexed += 1; + } + + Ok(ReindexReport { + posts_indexed, + media_indexed, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::db::Database; + use crate::db::fts::ensure_fts_tables; + use crate::engine; + + fn setup() -> (Database, String) { + let db = Database::open_in_memory().unwrap(); + let _ = db.migrate(); + ensure_fts_tables(db.conn()).unwrap(); + + let tmp = tempfile::tempdir().unwrap(); + let project = engine::project::create_project( + db.conn(), + "Test Project", + Some(tmp.path().to_str().unwrap()), + ) + .unwrap(); + + (db, project.id) + } + + #[test] + fn reindex_empty_project() { + let (db, project_id) = setup(); + let report = reindex_all(db.conn(), &project_id, "en").unwrap(); + assert_eq!(report.posts_indexed, 0); + assert_eq!(report.media_indexed, 0); + } + + #[test] + fn reindex_with_posts() { + let (db, project_id) = setup(); + let tmp = tempfile::tempdir().unwrap(); + + engine::post::create_post( + db.conn(), + tmp.path(), + &project_id, + "Test Post", + Some("Body content"), + vec!["tag1".into()], + vec![], + None, + None, + None, + ) + .unwrap(); + + let report = reindex_all(db.conn(), &project_id, "en").unwrap(); + assert_eq!(report.posts_indexed, 1); + assert_eq!(report.media_indexed, 0); + + // Verify searchable + let results = crate::db::fts::search_posts(db.conn(), "test", "en").unwrap(); + assert_eq!(results.len(), 1); + } +} diff --git a/crates/bds-core/src/engine/validate_translations.rs b/crates/bds-core/src/engine/validate_translations.rs new file mode 100644 index 0000000..f32d4f0 --- /dev/null +++ b/crates/bds-core/src/engine/validate_translations.rs @@ -0,0 +1,196 @@ +//! Translation validation — checks DB rows and filesystem files for issues. + +use std::path::Path; + +use rusqlite::Connection; + +use crate::db::queries::{post as post_q, post_translation}; +use crate::engine::EngineResult; +use crate::model::PostStatus; + +/// Normalize a language code for comparison (lowercase, strip region). +fn norm_lang(code: &str) -> String { + code.split(['-', '_']).next().unwrap_or("").to_lowercase() +} + +/// Check if a file stem looks like a translation (e.g. "slug.de"). +fn is_translation_stem(stem: &str) -> bool { + if let Some(dot_pos) = stem.rfind('.') { + let suffix = &stem[dot_pos + 1..]; + suffix.len() == 2 && suffix.chars().all(|c| c.is_ascii_lowercase()) + } else { + false + } +} + +/// A single validation issue. +#[derive(Debug, Clone)] +pub struct TranslationIssue { + pub post_id: String, + pub translation_id: Option, + pub file_path: Option, + pub language: String, + pub kind: TranslationIssueKind, +} + +#[derive(Debug, Clone)] +pub enum TranslationIssueKind { + MissingSourcePost, + SameLanguageAsCanonical, + DoNotTranslateHasTranslations, + ContentInDatabase, +} + +/// Result of translation validation. +#[derive(Debug)] +pub struct TranslationValidationReport { + pub db_issues: Vec, + pub fs_issues: Vec, + pub checked_db_rows: usize, + pub checked_fs_files: usize, +} + +/// Validate all translations in a project against consistency rules. +pub fn validate_translations( + conn: &Connection, + data_dir: &Path, + project_id: &str, +) -> EngineResult { + let posts = post_q::list_posts_by_project(conn, project_id)?; + + let post_map: std::collections::HashMap = + posts.iter().map(|p| (p.id.clone(), p)).collect(); + + // Phase 1: database validation + let mut db_issues = Vec::new(); + let mut checked_db_rows = 0usize; + + for post in &posts { + let translations = post_translation::list_post_translations_by_post(conn, &post.id)?; + + for t in &translations { + checked_db_rows += 1; + + // Check: translation language != canonical language + let post_lang = post.language.as_deref().unwrap_or("en"); + if norm_lang(post_lang) == norm_lang(&t.language) { + db_issues.push(TranslationIssue { + post_id: post.id.clone(), + translation_id: Some(t.id.clone()), + file_path: Some(t.file_path.clone()), + language: t.language.clone(), + kind: TranslationIssueKind::SameLanguageAsCanonical, + }); + } + + // Check: do_not_translate + if post.do_not_translate { + db_issues.push(TranslationIssue { + post_id: post.id.clone(), + translation_id: Some(t.id.clone()), + file_path: Some(t.file_path.clone()), + language: t.language.clone(), + kind: TranslationIssueKind::DoNotTranslateHasTranslations, + }); + } + + // Check: published translation should not have content in DB + if t.status == PostStatus::Published && t.content.is_some() { + db_issues.push(TranslationIssue { + post_id: post.id.clone(), + translation_id: Some(t.id.clone()), + file_path: Some(t.file_path.clone()), + language: t.language.clone(), + kind: TranslationIssueKind::ContentInDatabase, + }); + } + } + } + + // Phase 2: filesystem validation + let mut fs_issues = Vec::new(); + let mut checked_fs_files = 0usize; + + let posts_dir = data_dir.join("posts"); + if posts_dir.exists() { + for entry in walkdir::WalkDir::new(&posts_dir) + .into_iter() + .filter_map(|e| e.ok()) + .filter(|e| e.file_type().is_file()) + { + let path = entry.path(); + let Some(ext) = path.extension() else { continue }; + if ext != "md" { continue; } + + // Check if this is a translation file (has language code before .md) + let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or(""); + if !is_translation_stem(stem) { + continue; + } + + checked_fs_files += 1; + + // Parse frontmatter to check translation_for + let content = match std::fs::read_to_string(path) { + Ok(c) => c, + Err(_) => continue, + }; + + let Some((yaml_str, _body)) = crate::util::frontmatter::split_frontmatter(&content) else { + continue; + }; + + let Ok(fm) = crate::util::frontmatter::TranslationFrontmatter::from_yaml(yaml_str) else { + continue; + }; + + let translation_for = &fm.translation_for; + let language = &fm.language; + + if translation_for.is_empty() { + continue; + } + + // Check source post exists + if !post_map.contains_key(translation_for.as_str()) { + fs_issues.push(TranslationIssue { + post_id: translation_for.clone(), + translation_id: None, + file_path: Some(path.to_string_lossy().to_string()), + language: language.clone(), + kind: TranslationIssueKind::MissingSourcePost, + }); + } else if let Some(source) = post_map.get(translation_for.as_str()) { + // Check same language + let post_lang = source.language.as_deref().unwrap_or("en"); + if norm_lang(post_lang) == norm_lang(language) { + fs_issues.push(TranslationIssue { + post_id: translation_for.clone(), + translation_id: None, + file_path: Some(path.to_string_lossy().to_string()), + language: language.clone(), + kind: TranslationIssueKind::SameLanguageAsCanonical, + }); + } + + // Check do_not_translate + if source.do_not_translate { + fs_issues.push(TranslationIssue { + post_id: translation_for.clone(), + translation_id: None, + file_path: Some(path.to_string_lossy().to_string()), + language: language.clone(), + kind: TranslationIssueKind::DoNotTranslateHasTranslations, + }); + } + } + } + } + + Ok(TranslationValidationReport { + db_issues, + fs_issues, + checked_db_rows, + checked_fs_files, + }) +} diff --git a/crates/bds-ui/src/app.rs b/crates/bds-ui/src/app.rs index 385e1a6..42ee83e 100644 --- a/crates/bds-ui/src/app.rs +++ b/crates/bds-ui/src/app.rs @@ -197,6 +197,9 @@ impl BdsApp { registry.set_enabled(MenuAction::Save, false); registry.set_enabled(MenuAction::PublishSelected, false); registry.set_enabled(MenuAction::PreviewPost, false); + registry.set_enabled(MenuAction::Find, false); + registry.set_enabled(MenuAction::Replace, false); + registry.set_enabled(MenuAction::OpenInBrowser, false); #[cfg(target_os = "macos")] let (_lifecycle_tx, _lifecycle_rx) = crate::platform::macos::lifecycle_channel(); @@ -449,12 +452,75 @@ impl BdsApp { // ── Blog engine actions ── Message::RebuildDatabase => { - self.add_output("Rebuilding database..."); - // Actual rebuild dispatch deferred to later milestones + let ready = self.db.is_some() + && self.active_project.is_some() + && self.data_dir.is_some(); + if ready { + self.add_output(&t(self.ui_locale, "engine.rebuildStarted")); + let db = self.db.as_ref().unwrap(); + let project_id = self.active_project.as_ref().unwrap().id.clone(); + let data_dir = self.data_dir.as_ref().unwrap().clone(); + match engine::rebuild::rebuild_from_filesystem(db.conn(), &data_dir, &project_id) { + Ok(report) => { + let posts = report.posts_created + report.posts_updated; + let media = report.media_created + report.media_updated; + let templates = report.templates_created + report.templates_updated; + let scripts = report.scripts_created + report.scripts_updated; + self.add_output(&tw( + self.ui_locale, + "engine.rebuildComplete", + &[ + ("posts", &posts.to_string()), + ("media", &media.to_string()), + ("templates", &templates.to_string()), + ("scripts", &scripts.to_string()), + ], + )); + } + Err(e) => { + self.add_output(&tw( + self.ui_locale, + "engine.rebuildFailed", + &[("error", &e.to_string())], + )); + } + } + } Task::none() } Message::ReindexText => { - self.add_output("Reindexing search text..."); + let ready = self.db.is_some() + && self.active_project.is_some() + && self.data_dir.is_some(); + if ready { + self.add_output(&t(self.ui_locale, "engine.reindexStarted")); + let db = self.db.as_ref().unwrap(); + let project_id = self.active_project.as_ref().unwrap().id.clone(); + let data_dir = self.data_dir.as_ref().unwrap().clone(); + let main_lang = engine::meta::read_project_json(&data_dir) + .ok() + .and_then(|m| m.main_language) + .unwrap_or_else(|| "en".to_string()); + match engine::search::reindex_all(db.conn(), &project_id, &main_lang) { + Ok(report) => { + self.add_output(&tw( + self.ui_locale, + "engine.reindexComplete", + &[ + ("posts", &report.posts_indexed.to_string()), + ("media", &report.media_indexed.to_string()), + ], + )); + } + Err(e) => { + self.add_output(&tw( + self.ui_locale, + "engine.reindexFailed", + &[("error", &e.to_string())], + )); + } + } + } Task::none() } Message::RunMetadataDiff => { @@ -523,7 +589,39 @@ impl BdsApp { match action { // File MenuAction::NewPost => { - // Will create post + open tab in later milestones + if let (Some(db), Some(project), Some(data_dir)) = + (&self.db, &self.active_project, &self.data_dir) + { + let title = t(self.ui_locale, "post.untitled"); + match engine::post::create_post( + db.conn(), + data_dir, + &project.id, + &title, + Some(""), + Vec::new(), + Vec::new(), + None, + None, + None, + ) { + Ok(post) => { + let tab = Tab { + id: post.id.clone(), + tab_type: TabType::Post, + title: post.title.clone(), + is_transient: true, + }; + let idx = tabs::open_tab(&mut self.tabs, tab); + if let Some(t) = self.tabs.get(idx) { + self.active_tab = Some(t.id.clone()); + } + } + Err(e) => { + self.add_output(&format!("Failed to create post: {e}")); + } + } + } Task::none() } MenuAction::ImportMedia => crate::platform::dialog::pick_media_files(), @@ -565,20 +663,122 @@ impl BdsApp { MenuAction::RebuildDatabase => Task::done(Message::RebuildDatabase), MenuAction::ReindexText => Task::done(Message::ReindexText), MenuAction::MetadataDiff => Task::done(Message::RunMetadataDiff), - MenuAction::RegenerateCalendar => Task::none(), - MenuAction::ValidateTranslations => { - self.open_singleton_tab(TabType::TranslationValidation, "Translation Validation"); + MenuAction::RegenerateCalendar => { + let ready = self.db.is_some() + && self.active_project.is_some() + && self.data_dir.is_some(); + if ready { + self.add_output(&t(self.ui_locale, "engine.calendarStarted")); + let db = self.db.as_ref().unwrap(); + let project_id = self.active_project.as_ref().unwrap().id.clone(); + let data_dir = self.data_dir.as_ref().unwrap().clone(); + match engine::calendar::regenerate_calendar(db.conn(), &data_dir, &project_id) { + Ok(()) => { + self.add_output(&t(self.ui_locale, "engine.calendarComplete")); + } + Err(e) => { + self.add_output(&tw( + self.ui_locale, + "engine.calendarFailed", + &[("error", &e.to_string())], + )); + } + } + } else { + self.add_output(&t(self.ui_locale, "engine.calendarNoProject")); + } + Task::none() + } + MenuAction::ValidateTranslations => { + self.open_singleton_tab(TabType::TranslationValidation, "Translation Validation"); + let ready = self.db.is_some() + && self.active_project.is_some() + && self.data_dir.is_some(); + if ready { + self.add_output(&t(self.ui_locale, "engine.validateTranslationsStarted")); + let db = self.db.as_ref().unwrap(); + let project_id = self.active_project.as_ref().unwrap().id.clone(); + let data_dir = self.data_dir.as_ref().unwrap().clone(); + match engine::validate_translations::validate_translations( + db.conn(), + &data_dir, + &project_id, + ) { + Ok(report) => { + self.add_output(&tw( + self.ui_locale, + "engine.validateTranslationsComplete", + &[ + ("dbIssues", &report.db_issues.len().to_string()), + ("fsIssues", &report.fs_issues.len().to_string()), + ], + )); + } + Err(e) => { + self.add_output(&tw( + self.ui_locale, + "engine.validateTranslationsFailed", + &[("error", &e.to_string())], + )); + } + } + } + Task::none() + } + MenuAction::FillMissingTranslations => { + if self.offline_mode { + self.add_output(&t(self.ui_locale, "engine.fillMissingTranslationsOffline")); + } else { + // AI translation not yet available — inform user + self.add_output(&t(self.ui_locale, "engine.fillMissingTranslationsNoAi")); + } + Task::none() + } + MenuAction::GenerateSitemap => { + if self.active_project.is_some() { + self.add_output(&t(self.ui_locale, "engine.generateSiteStarted")); + // Regenerate calendar as subset until full template rendering (M3+) + if let Some(db) = &self.db { + let project_id = self.active_project.as_ref().unwrap().id.clone(); + let data_dir = self.data_dir.as_ref().unwrap().clone(); + let _ = engine::calendar::regenerate_calendar(db.conn(), &data_dir, &project_id); + } + } else { + self.add_output(&t(self.ui_locale, "engine.generateSiteNoProject")); + } Task::none() } - MenuAction::FillMissingTranslations => Task::none(), - MenuAction::GenerateSitemap => Task::none(), MenuAction::ValidateSite => { self.open_singleton_tab(TabType::SiteValidation, "Site Validation"); Task::none() } - MenuAction::UploadSite => Task::none(), + MenuAction::UploadSite => { + if self.offline_mode { + self.add_output(&t(self.ui_locale, "engine.uploadOffline")); + } else if let Some(data_dir) = &self.data_dir { + // Check publishing credentials + let pub_prefs = engine::meta::read_publishing_json(data_dir).ok(); + let has_creds = pub_prefs + .as_ref() + .map(|p| { + p.ssh_host.as_ref().map_or(false, |h| !h.is_empty()) + && p.ssh_user.as_ref().map_or(false, |u| !u.is_empty()) + }) + .unwrap_or(false); + if !has_creds { + self.add_output(&t(self.ui_locale, "engine.uploadMissingCredentials")); + } else { + self.add_output(&t(self.ui_locale, "engine.uploadStarted")); + // SSH upload requires async SSH library (deferred to publishing milestone) + } + } + Task::none() + } // Help - MenuAction::About => Task::none(), + MenuAction::About => { + self.add_output(&t(self.ui_locale, "menu.item.about")); + Task::none() + } MenuAction::OpenDocumentation => { self.open_singleton_tab(TabType::Documentation, "Documentation"); Task::none() diff --git a/locales/ui/de.json b/locales/ui/de.json index 5f665ac..0cbe114 100644 --- a/locales/ui/de.json +++ b/locales/ui/de.json @@ -100,5 +100,27 @@ "language.it": "Italienisch", "language.es": "Spanisch", "welcome.title": "Willkommen bei bDS", - "welcome.subtitle": "Öffne ein Projekt oder erstelle einen neuen Beitrag." + "welcome.subtitle": "Öffne ein Projekt oder erstelle einen neuen Beitrag.", + "post.untitled": "Unbenannt", + "post.createFailed": "Beitrag konnte nicht erstellt werden: {error}", + "engine.rebuildStarted": "Datenbank wird aus Dateien neu aufgebaut...", + "engine.rebuildComplete": "Datenbank neu aufgebaut: {posts} Beiträge, {media} Medien, {templates} Vorlagen, {scripts} Skripte", + "engine.rebuildFailed": "Datenbankaufbau fehlgeschlagen: {error}", + "engine.reindexStarted": "Suchtext wird neu indiziert...", + "engine.reindexComplete": "Suchtext neu indiziert: {posts} Beiträge, {media} Medien", + "engine.reindexFailed": "Suchtext-Indizierung fehlgeschlagen: {error}", + "engine.calendarStarted": "Kalender wird neu generiert...", + "engine.calendarComplete": "Kalender neu generiert", + "engine.calendarFailed": "Kalender-Generierung fehlgeschlagen: {error}", + "engine.calendarNoProject": "Kein aktives Projekt", + "engine.validateTranslationsStarted": "Übersetzungen werden validiert...", + "engine.validateTranslationsComplete": "Übersetzungsvalidierung: {dbIssues} DB-Probleme, {fsIssues} Dateisystem-Probleme", + "engine.validateTranslationsFailed": "Übersetzungsvalidierung fehlgeschlagen: {error}", + "engine.fillMissingTranslationsOffline": "Übersetzungen können nicht ergänzt werden: KI im Offline-Modus nicht verfügbar", + "engine.fillMissingTranslationsNoAi": "Übersetzungen können nicht ergänzt werden: KI noch nicht verfügbar", + "engine.uploadStarted": "Seite wird hochgeladen...", + "engine.uploadMissingCredentials": "Veröffentlichungsdaten unvollständig. In Projekteinstellungen konfigurieren.", + "engine.uploadOffline": "Hochladen nicht möglich: Flugmodus ist aktiv", + "engine.generateSiteStarted": "Seite wird generiert...", + "engine.generateSiteNoProject": "Kein aktives Projekt" } diff --git a/locales/ui/en.json b/locales/ui/en.json index 217821d..5fa16a0 100644 --- a/locales/ui/en.json +++ b/locales/ui/en.json @@ -100,5 +100,27 @@ "language.it": "Italian", "language.es": "Spanish", "welcome.title": "Welcome to bDS", - "welcome.subtitle": "Open a project or create a new post to get started." + "welcome.subtitle": "Open a project or create a new post to get started.", + "post.untitled": "Untitled", + "post.createFailed": "Failed to create post: {error}", + "engine.rebuildStarted": "Rebuilding database from files...", + "engine.rebuildComplete": "Database rebuilt: {posts} posts, {media} media, {templates} templates, {scripts} scripts", + "engine.rebuildFailed": "Database rebuild failed: {error}", + "engine.reindexStarted": "Reindexing search text...", + "engine.reindexComplete": "Search text reindexed: {posts} posts, {media} media", + "engine.reindexFailed": "Search text reindex failed: {error}", + "engine.calendarStarted": "Regenerating calendar...", + "engine.calendarComplete": "Calendar regenerated", + "engine.calendarFailed": "Calendar regeneration failed: {error}", + "engine.calendarNoProject": "No active project", + "engine.validateTranslationsStarted": "Validating translations...", + "engine.validateTranslationsComplete": "Translation validation: {dbIssues} DB issues, {fsIssues} filesystem issues", + "engine.validateTranslationsFailed": "Translation validation failed: {error}", + "engine.fillMissingTranslationsOffline": "Cannot fill translations: AI is unavailable in offline mode", + "engine.fillMissingTranslationsNoAi": "Cannot fill translations: AI not yet available", + "engine.uploadStarted": "Uploading site...", + "engine.uploadMissingCredentials": "Publishing credentials incomplete. Configure in project settings.", + "engine.uploadOffline": "Cannot upload: airplane mode is active", + "engine.generateSiteStarted": "Generating site...", + "engine.generateSiteNoProject": "No active project" } diff --git a/locales/ui/es.json b/locales/ui/es.json index b3af9e7..4be8cd4 100644 --- a/locales/ui/es.json +++ b/locales/ui/es.json @@ -100,5 +100,27 @@ "language.it": "Italiano", "language.es": "Español", "welcome.title": "Bienvenido a bDS", - "welcome.subtitle": "Abre un proyecto o crea una nueva entrada para comenzar." + "welcome.subtitle": "Abre un proyecto o crea una nueva entrada para comenzar.", + "post.untitled": "Sin título", + "post.createFailed": "No se pudo crear la entrada: {error}", + "engine.rebuildStarted": "Reconstruyendo la base de datos desde archivos...", + "engine.rebuildComplete": "Base de datos reconstruida: {posts} entradas, {media} medios, {templates} plantillas, {scripts} scripts", + "engine.rebuildFailed": "La reconstrucción de la base de datos falló: {error}", + "engine.reindexStarted": "Reindexando texto de búsqueda...", + "engine.reindexComplete": "Texto de búsqueda reindexado: {posts} entradas, {media} medios", + "engine.reindexFailed": "La reindexación del texto de búsqueda falló: {error}", + "engine.calendarStarted": "Regenerando calendario...", + "engine.calendarComplete": "Calendario regenerado", + "engine.calendarFailed": "La regeneración del calendario falló: {error}", + "engine.calendarNoProject": "No hay proyecto activo", + "engine.validateTranslationsStarted": "Validando traducciones...", + "engine.validateTranslationsComplete": "Validación de traducciones: {dbIssues} problemas en la base de datos, {fsIssues} problemas en el sistema de archivos", + "engine.validateTranslationsFailed": "La validación de traducciones falló: {error}", + "engine.fillMissingTranslationsOffline": "No se pueden completar las traducciones: la IA no está disponible en modo avión", + "engine.fillMissingTranslationsNoAi": "No se pueden completar las traducciones: la IA aún no está disponible", + "engine.uploadStarted": "Subiendo sitio...", + "engine.uploadMissingCredentials": "Credenciales de publicación incompletas. Configúralas en los ajustes del proyecto.", + "engine.uploadOffline": "No se puede subir: el modo avión está activo", + "engine.generateSiteStarted": "Generando sitio...", + "engine.generateSiteNoProject": "No hay proyecto activo" } diff --git a/locales/ui/fr.json b/locales/ui/fr.json index a67b60d..b0343b3 100644 --- a/locales/ui/fr.json +++ b/locales/ui/fr.json @@ -100,5 +100,27 @@ "language.it": "Italien", "language.es": "Espagnol", "welcome.title": "Bienvenue dans bDS", - "welcome.subtitle": "Ouvrez un projet ou créez un nouvel article pour commencer." + "welcome.subtitle": "Ouvrez un projet ou créez un nouvel article pour commencer.", + "post.untitled": "Sans titre", + "post.createFailed": "Impossible de créer l'article : {error}", + "engine.rebuildStarted": "Reconstruction de la base de données à partir des fichiers...", + "engine.rebuildComplete": "Base de données reconstruite : {posts} articles, {media} médias, {templates} modèles, {scripts} scripts", + "engine.rebuildFailed": "Échec de la reconstruction de la base de données : {error}", + "engine.reindexStarted": "Réindexation du texte de recherche...", + "engine.reindexComplete": "Texte de recherche réindexé : {posts} articles, {media} médias", + "engine.reindexFailed": "Échec de la réindexation du texte de recherche : {error}", + "engine.calendarStarted": "Régénération du calendrier...", + "engine.calendarComplete": "Calendrier régénéré", + "engine.calendarFailed": "Échec de la régénération du calendrier : {error}", + "engine.calendarNoProject": "Aucun projet actif", + "engine.validateTranslationsStarted": "Validation des traductions...", + "engine.validateTranslationsComplete": "Validation des traductions : {dbIssues} problèmes en base, {fsIssues} problèmes sur le système de fichiers", + "engine.validateTranslationsFailed": "Échec de la validation des traductions : {error}", + "engine.fillMissingTranslationsOffline": "Impossible de compléter les traductions : l'IA n'est pas disponible en mode avion", + "engine.fillMissingTranslationsNoAi": "Impossible de compléter les traductions : l'IA n'est pas encore disponible", + "engine.uploadStarted": "Publication du site...", + "engine.uploadMissingCredentials": "Identifiants de publication incomplets. Configurez-les dans les paramètres du projet.", + "engine.uploadOffline": "Impossible de publier : le mode avion est actif", + "engine.generateSiteStarted": "Génération du site...", + "engine.generateSiteNoProject": "Aucun projet actif" } diff --git a/locales/ui/it.json b/locales/ui/it.json index 1bca935..8397b92 100644 --- a/locales/ui/it.json +++ b/locales/ui/it.json @@ -100,5 +100,27 @@ "language.it": "Italiano", "language.es": "Spagnolo", "welcome.title": "Benvenuto in bDS", - "welcome.subtitle": "Apri un progetto o crea un nuovo post per iniziare." + "welcome.subtitle": "Apri un progetto o crea un nuovo post per iniziare.", + "post.untitled": "Senza titolo", + "post.createFailed": "Impossibile creare il post: {error}", + "engine.rebuildStarted": "Ricostruzione del database dai file...", + "engine.rebuildComplete": "Database ricostruito: {posts} post, {media} media, {templates} modelli, {scripts} script", + "engine.rebuildFailed": "Ricostruzione del database non riuscita: {error}", + "engine.reindexStarted": "Reindicizzazione del testo di ricerca...", + "engine.reindexComplete": "Testo di ricerca reindicizzato: {posts} post, {media} media", + "engine.reindexFailed": "Reindicizzazione del testo di ricerca non riuscita: {error}", + "engine.calendarStarted": "Rigenerazione del calendario...", + "engine.calendarComplete": "Calendario rigenerato", + "engine.calendarFailed": "Rigenerazione del calendario non riuscita: {error}", + "engine.calendarNoProject": "Nessun progetto attivo", + "engine.validateTranslationsStarted": "Validazione delle traduzioni...", + "engine.validateTranslationsComplete": "Validazione traduzioni: {dbIssues} problemi nel database, {fsIssues} problemi nel filesystem", + "engine.validateTranslationsFailed": "Validazione delle traduzioni non riuscita: {error}", + "engine.fillMissingTranslationsOffline": "Impossibile completare le traduzioni: l'IA non è disponibile in modalità aereo", + "engine.fillMissingTranslationsNoAi": "Impossibile completare le traduzioni: l'IA non è ancora disponibile", + "engine.uploadStarted": "Caricamento del sito...", + "engine.uploadMissingCredentials": "Credenziali di pubblicazione incomplete. Configurale nelle impostazioni del progetto.", + "engine.uploadOffline": "Impossibile caricare: la modalità aereo è attiva", + "engine.generateSiteStarted": "Generazione del sito...", + "engine.generateSiteNoProject": "Nessun progetto attivo" }