fix: wired engines that exist to UI
This commit is contained in:
86
crates/bds-core/src/engine/calendar.rs
Normal file
86
crates/bds-core/src/engine/calendar.rs
Normal file
@@ -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<String, u32> = BTreeMap::new();
|
||||
let mut months: BTreeMap<String, u32> = BTreeMap::new();
|
||||
let mut days: BTreeMap<String, u32> = 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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
161
crates/bds-core/src/engine/search.rs
Normal file
161
crates/bds-core/src/engine/search.rs
Normal file
@@ -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<ReindexReport> {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
196
crates/bds-core/src/engine/validate_translations.rs
Normal file
196
crates/bds-core/src/engine/validate_translations.rs
Normal file
@@ -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<String>,
|
||||
pub file_path: Option<String>,
|
||||
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<TranslationIssue>,
|
||||
pub fs_issues: Vec<TranslationIssue>,
|
||||
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<TranslationValidationReport> {
|
||||
let posts = post_q::list_posts_by_project(conn, project_id)?;
|
||||
|
||||
let post_map: std::collections::HashMap<String, &crate::model::Post> =
|
||||
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,
|
||||
})
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user