From c187108f893031973068c9c3c7912f4d696afb97 Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Fri, 24 Jul 2026 09:20:12 +0200 Subject: [PATCH] Localize category labels in blog menus. --- README.md | 2 +- crates/bds-core/src/render/site.rs | 61 ++++++++++++++++++++++++++---- specs/rendering.allium | 6 +++ 3 files changed, 60 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 8e84ca3..ac6af48 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ The project is under active development. Core blogging workflows are broadly ava - SQLite and filesystem persistence with byte-canonical bDS2 frontmatter, media sidecars, metadata JSON, and OPML menus; rebuild; bidirectional metadata diff/repair including project categories and publishing preferences; stale post-path cleanup on republish; bDS2-compatible checksums and NFD slug generation; and FTS5 search. - Optional on-device multilingual semantic search and similar-post tag suggestions backed by a persistent USearch index, plus always-available partial-name tag autocomplete and duplicate-post review in the desktop workspace. - Read-only in-app browsers in the Help menu for the bundled global `DOCUMENTATION.md`, the generated Lua API reference with public types and runnable examples, the [CLI/server/TUI documentation](CLI.md), and the [MCP server documentation](MCP.md), with safe GFM rendering and confirmed external links. -- A localized Tags workspace manages tags and category settings; its category table includes the main language and every configured translation, whose titles are used by the matching category archives. +- A localized Tags workspace manages tags and category settings; its category table includes the main language and every configured translation, whose titles are used by matching category archives and menu entries. - A localized OPML menu editor manages pages, submenus, and category archives with protected Home ordering, keyboard-accessible tree controls, drag-and-drop, and bDS2-compatible persistence. - Project-scoped typed domain events synchronize desktop views with shared-engine and future CLI mutations; persisted CLI notifications are consumed once, and the selected UI language is shared through settings. - Headless `bds-cli` automation for rebuild/repair/render, publishing and Git sync, post/media/gallery creation, effective shared settings with secret-presence redaction, projects, utility Lua tasks, JSON I/O, airplane-mode AI routing, and guarded launcher installation from Settings → Data or `bds-cli install`. diff --git a/crates/bds-core/src/render/site.rs b/crates/bds-core/src/render/site.rs index b0af4eb..c31e093 100644 --- a/crates/bds-core/src/render/site.rs +++ b/crates/bds-core/src/render/site.rs @@ -391,7 +391,12 @@ pub fn prepare_site_render_context( let linked_media_by_post_id = build_linked_media_by_post_id(&posts, &linked_media_by_source_post); let post_data_json_by_id = build_post_data_json_by_id(&posts, &linked_media_by_post_id); - let menu_items = build_menu_items(data_dir, &language, &main_language)?; + let menu_items = build_menu_items( + data_dir, + &language, + &main_language, + &category_settings, + )?; let canonical_post_path_by_slug = canonical_post_path_by_slug(&posts, &language, &main_language); let taxonomy = build_taxonomy_context(&posts, &tags); @@ -619,7 +624,13 @@ pub fn build_preview_response( let bundle = load_template_bundle(conn, data_dir, project_id)?; let language = language_from_path(&normalized, metadata); - let menu_items = build_menu_items(data_dir, &language, main_language(metadata))?; + let category_settings = queries_category_settings(data_dir)?; + let menu_items = build_menu_items( + data_dir, + &language, + main_language(metadata), + &category_settings, + )?; let html = render_not_found_route(&bundle, metadata, &language, &normalized, &menu_items)?; Ok(PreviewRenderResult { status_code: 404, @@ -1314,22 +1325,37 @@ fn build_menu_items( data_dir: &Path, language: &str, main_language: &str, + category_settings: &HashMap, ) -> Result, Box> { let items = menu::read_menu(data_dir)?; Ok(items .iter() - .map(|item| menu_item_context(item, language, main_language)) + .map(|item| menu_item_context(item, language, main_language, category_settings)) .collect()) } -fn menu_item_context(item: &menu::MenuItem, language: &str, main_language: &str) -> Value { +fn menu_item_context( + item: &menu::MenuItem, + language: &str, + main_language: &str, + category_settings: &HashMap, +) -> Value { let children = item .children .iter() - .map(|child| menu_item_context(child, language, main_language)) + .map(|child| menu_item_context(child, language, main_language, category_settings)) .collect::>(); + let title = match item.kind { + MenuItemKind::CategoryArchive => item + .slug + .as_deref() + .and_then(|category| category_settings.get(category)) + .and_then(|settings| settings.title_for(language, main_language)) + .unwrap_or(item.label.as_str()), + _ => item.label.as_str(), + }; json!({ - "title": item.label, + "title": title, "href": menu_item_href(item, language, main_language), "has_children": !children.is_empty(), "children": children, @@ -2202,15 +2228,34 @@ mod menu_tests { ) .unwrap(); - let rendered = build_menu_items(dir.path(), "en", "en").unwrap(); + crate::engine::meta::write_category_meta_json( + dir.path(), + &HashMap::from([( + "Über Öl".into(), + CategorySettings { + list_template_slug: None, + post_template_slug: None, + render_in_lists: true, + show_title: true, + title: Some("Oil".into()), + titles: BTreeMap::from([("de".into(), "Öl".into())]), + }, + )]), + ) + .unwrap(); + + let category_settings = queries_category_settings(dir.path()).unwrap(); + let rendered = build_menu_items(dir.path(), "en", "en", &category_settings).unwrap(); assert_eq!(rendered[0]["href"], "/"); assert_eq!(rendered[1]["href"], "/about/"); assert_eq!(rendered[2]["children"][0]["href"], "/category/uber-ol/"); - let translated = build_menu_items(dir.path(), "de", "en").unwrap(); + assert_eq!(rendered[2]["children"][0]["title"], "Oil"); + let translated = build_menu_items(dir.path(), "de", "en", &category_settings).unwrap(); assert_eq!(translated[1]["href"], "/de/about/"); assert_eq!( translated[2]["children"][0]["href"], "/de/category/uber-ol/" ); + assert_eq!(translated[2]["children"][0]["title"], "Öl"); } } diff --git a/specs/rendering.allium b/specs/rendering.allium index e8a0456..7647923 100644 --- a/specs/rendering.allium +++ b/specs/rendering.allium @@ -229,6 +229,12 @@ invariant CategoryArchiveTitleLanguage { -- category name. } +invariant CategoryMenuTitleLanguage { + -- A category-archive menu item uses the title configured for its render + -- language at every nesting level. A missing title falls back to the menu + -- item's persisted label. +} + invariant SharedRenderPathForPreviewAndGeneration { -- Preview and generation produce identical HTML for the same input because -- both build assigns through this subsystem and render via the same Liquid