Localize category labels in blog menus.
This commit is contained in:
@@ -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.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
- 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`.
|
- 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`.
|
||||||
|
|||||||
@@ -391,7 +391,12 @@ pub fn prepare_site_render_context(
|
|||||||
let linked_media_by_post_id =
|
let linked_media_by_post_id =
|
||||||
build_linked_media_by_post_id(&posts, &linked_media_by_source_post);
|
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 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 =
|
let canonical_post_path_by_slug =
|
||||||
canonical_post_path_by_slug(&posts, &language, &main_language);
|
canonical_post_path_by_slug(&posts, &language, &main_language);
|
||||||
let taxonomy = build_taxonomy_context(&posts, &tags);
|
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 bundle = load_template_bundle(conn, data_dir, project_id)?;
|
||||||
let language = language_from_path(&normalized, metadata);
|
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)?;
|
let html = render_not_found_route(&bundle, metadata, &language, &normalized, &menu_items)?;
|
||||||
Ok(PreviewRenderResult {
|
Ok(PreviewRenderResult {
|
||||||
status_code: 404,
|
status_code: 404,
|
||||||
@@ -1314,22 +1325,37 @@ fn build_menu_items(
|
|||||||
data_dir: &Path,
|
data_dir: &Path,
|
||||||
language: &str,
|
language: &str,
|
||||||
main_language: &str,
|
main_language: &str,
|
||||||
|
category_settings: &HashMap<String, CategorySettings>,
|
||||||
) -> Result<Vec<Value>, Box<dyn Error + Send + Sync>> {
|
) -> Result<Vec<Value>, Box<dyn Error + Send + Sync>> {
|
||||||
let items = menu::read_menu(data_dir)?;
|
let items = menu::read_menu(data_dir)?;
|
||||||
Ok(items
|
Ok(items
|
||||||
.iter()
|
.iter()
|
||||||
.map(|item| menu_item_context(item, language, main_language))
|
.map(|item| menu_item_context(item, language, main_language, category_settings))
|
||||||
.collect())
|
.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<String, CategorySettings>,
|
||||||
|
) -> Value {
|
||||||
let children = item
|
let children = item
|
||||||
.children
|
.children
|
||||||
.iter()
|
.iter()
|
||||||
.map(|child| menu_item_context(child, language, main_language))
|
.map(|child| menu_item_context(child, language, main_language, category_settings))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
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!({
|
json!({
|
||||||
"title": item.label,
|
"title": title,
|
||||||
"href": menu_item_href(item, language, main_language),
|
"href": menu_item_href(item, language, main_language),
|
||||||
"has_children": !children.is_empty(),
|
"has_children": !children.is_empty(),
|
||||||
"children": children,
|
"children": children,
|
||||||
@@ -2202,15 +2228,34 @@ mod menu_tests {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.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[0]["href"], "/");
|
||||||
assert_eq!(rendered[1]["href"], "/about/");
|
assert_eq!(rendered[1]["href"], "/about/");
|
||||||
assert_eq!(rendered[2]["children"][0]["href"], "/category/uber-ol/");
|
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[1]["href"], "/de/about/");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
translated[2]["children"][0]["href"],
|
translated[2]["children"][0]["href"],
|
||||||
"/de/category/uber-ol/"
|
"/de/category/uber-ol/"
|
||||||
);
|
);
|
||||||
|
assert_eq!(translated[2]["children"][0]["title"], "Öl");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -229,6 +229,12 @@ invariant CategoryArchiveTitleLanguage {
|
|||||||
-- category name.
|
-- 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 {
|
invariant SharedRenderPathForPreviewAndGeneration {
|
||||||
-- Preview and generation produce identical HTML for the same input because
|
-- Preview and generation produce identical HTML for the same input because
|
||||||
-- both build assigns through this subsystem and render via the same Liquid
|
-- both build assigns through this subsystem and render via the same Liquid
|
||||||
|
|||||||
Reference in New Issue
Block a user