fix: some work on performance

This commit is contained in:
2026-04-05 21:00:10 +02:00
parent 16aefa675e
commit 8022013fcf
8 changed files with 309 additions and 86 deletions

View File

@@ -138,6 +138,20 @@ pub enum Message {
TemplateLoaded(Result<Template, String>), TemplateLoaded(Result<Template, String>),
ScriptLoaded(Result<Script, String>), ScriptLoaded(Result<Script, String>),
// Async sidebar data
SidebarPostsLoaded(Vec<Post>),
SidebarMediaLoaded {
items: Vec<Media>,
thumbs: HashMap<String, Option<std::path::PathBuf>>,
},
SidebarPostsAppended(Vec<Post>),
SidebarMediaAppended {
items: Vec<Media>,
thumbs: HashMap<String, Option<std::path::PathBuf>>,
},
LoadMorePosts,
LoadMoreMedia,
Noop, Noop,
InitMenuBar, InitMenuBar,
} }
@@ -165,6 +179,9 @@ pub struct BdsApp {
sidebar_media: Vec<Media>, sidebar_media: Vec<Media>,
sidebar_scripts: Vec<Script>, sidebar_scripts: Vec<Script>,
sidebar_templates: Vec<Template>, sidebar_templates: Vec<Template>,
sidebar_media_thumbs: HashMap<String, Option<std::path::PathBuf>>,
sidebar_posts_has_more: bool,
sidebar_media_has_more: bool,
// Sidebar filters (per sidebar_views.allium PostsView / MediaView) // Sidebar filters (per sidebar_views.allium PostsView / MediaView)
post_filter: PostFilter, post_filter: PostFilter,
@@ -313,6 +330,9 @@ impl BdsApp {
sidebar_media: Vec::new(), sidebar_media: Vec::new(),
sidebar_scripts: Vec::new(), sidebar_scripts: Vec::new(),
sidebar_templates: Vec::new(), sidebar_templates: Vec::new(),
sidebar_media_thumbs: HashMap::new(),
sidebar_posts_has_more: false,
sidebar_media_has_more: false,
post_filter: PostFilter::default(), post_filter: PostFilter::default(),
page_filter: PostFilter::default(), page_filter: PostFilter::default(),
media_filter: MediaFilter::default(), media_filter: MediaFilter::default(),
@@ -375,10 +395,11 @@ impl BdsApp {
SidebarView::Posts | SidebarView::Pages SidebarView::Posts | SidebarView::Pages
); );
if needs_post_refresh { if needs_post_refresh {
self.refresh_sidebar_posts(); self.refresh_sidebar_posts()
} } else {
Task::none() Task::none()
} }
}
Message::ToggleSidebar => { Message::ToggleSidebar => {
self.sidebar_visible = !self.sidebar_visible; self.sidebar_visible = !self.sidebar_visible;
Task::none() Task::none()
@@ -471,9 +492,9 @@ impl BdsApp {
} }
} }
} }
self.refresh_counts(); let sidebar_task = self.refresh_counts();
self.sync_menu_state(); self.sync_menu_state();
Task::none() sidebar_task
} }
Message::SwitchProject(project_id) => { Message::SwitchProject(project_id) => {
self.project_dropdown_open = false; self.project_dropdown_open = false;
@@ -767,9 +788,9 @@ impl BdsApp {
self.notify(ToastLevel::Error, &format!("{label} failed: {err}")); self.notify(ToastLevel::Error, &format!("{label} failed: {err}"));
} }
} }
self.refresh_counts(); let sidebar_task = self.refresh_counts();
self.refresh_task_snapshots(); self.refresh_task_snapshots();
Task::none() sidebar_task
} }
// ── Toast ── // ── Toast ──
@@ -793,8 +814,7 @@ impl BdsApp {
_ => &mut self.post_filter, _ => &mut self.post_filter,
}; };
filter.search_query = query; filter.search_query = query;
self.refresh_sidebar_posts(); self.refresh_sidebar_posts()
Task::none()
} }
Message::TogglePostFilterPanel => { Message::TogglePostFilterPanel => {
let filter = match self.sidebar_view { let filter = match self.sidebar_view {
@@ -811,8 +831,7 @@ impl BdsApp {
}; };
filter.calendar.selected_year = year; filter.calendar.selected_year = year;
filter.calendar.selected_month = None; filter.calendar.selected_month = None;
self.refresh_sidebar_posts(); self.refresh_sidebar_posts()
Task::none()
} }
Message::SetPostCalendarMonth(month) => { Message::SetPostCalendarMonth(month) => {
let filter = match self.sidebar_view { let filter = match self.sidebar_view {
@@ -820,8 +839,7 @@ impl BdsApp {
_ => &mut self.post_filter, _ => &mut self.post_filter,
}; };
filter.calendar.selected_month = month; filter.calendar.selected_month = month;
self.refresh_sidebar_posts(); self.refresh_sidebar_posts()
Task::none()
} }
Message::TogglePostTagFilter(tag) => { Message::TogglePostTagFilter(tag) => {
let filter = match self.sidebar_view { let filter = match self.sidebar_view {
@@ -833,8 +851,7 @@ impl BdsApp {
} else { } else {
filter.tag_filter.push(tag); filter.tag_filter.push(tag);
} }
self.refresh_sidebar_posts(); self.refresh_sidebar_posts()
Task::none()
} }
Message::TogglePostCategoryFilter(cat) => { Message::TogglePostCategoryFilter(cat) => {
let filter = match self.sidebar_view { let filter = match self.sidebar_view {
@@ -846,8 +863,7 @@ impl BdsApp {
} else { } else {
filter.category_filter.push(cat); filter.category_filter.push(cat);
} }
self.refresh_sidebar_posts(); self.refresh_sidebar_posts()
Task::none()
} }
Message::ClearPostFilters => { Message::ClearPostFilters => {
let filter = match self.sidebar_view { let filter = match self.sidebar_view {
@@ -855,13 +871,11 @@ impl BdsApp {
_ => &mut self.post_filter, _ => &mut self.post_filter,
}; };
filter.clear(); filter.clear();
self.refresh_sidebar_posts(); self.refresh_sidebar_posts()
Task::none()
} }
Message::MediaSearchChanged(query) => { Message::MediaSearchChanged(query) => {
self.media_filter.search_query = query; self.media_filter.search_query = query;
self.refresh_sidebar_media(); self.refresh_sidebar_media()
Task::none()
} }
Message::ToggleMediaFilterPanel => { Message::ToggleMediaFilterPanel => {
self.media_filter.filter_panel_visible = !self.media_filter.filter_panel_visible; self.media_filter.filter_panel_visible = !self.media_filter.filter_panel_visible;
@@ -870,13 +884,11 @@ impl BdsApp {
Message::SetMediaCalendarYear(year) => { Message::SetMediaCalendarYear(year) => {
self.media_filter.calendar.selected_year = year; self.media_filter.calendar.selected_year = year;
self.media_filter.calendar.selected_month = None; self.media_filter.calendar.selected_month = None;
self.refresh_sidebar_media(); self.refresh_sidebar_media()
Task::none()
} }
Message::SetMediaCalendarMonth(month) => { Message::SetMediaCalendarMonth(month) => {
self.media_filter.calendar.selected_month = month; self.media_filter.calendar.selected_month = month;
self.refresh_sidebar_media(); self.refresh_sidebar_media()
Task::none()
} }
Message::ToggleMediaTagFilter(tag) => { Message::ToggleMediaTagFilter(tag) => {
if let Some(pos) = self.media_filter.tag_filter.iter().position(|t| *t == tag) { if let Some(pos) = self.media_filter.tag_filter.iter().position(|t| *t == tag) {
@@ -884,14 +896,46 @@ impl BdsApp {
} else { } else {
self.media_filter.tag_filter.push(tag); self.media_filter.tag_filter.push(tag);
} }
self.refresh_sidebar_media(); self.refresh_sidebar_media()
Task::none()
} }
Message::ClearMediaFilters => { Message::ClearMediaFilters => {
self.media_filter.clear(); self.media_filter.clear();
self.refresh_sidebar_media(); self.refresh_sidebar_media()
}
// ── Async sidebar data ──
Message::SidebarPostsLoaded(mut posts) => {
self.sidebar_posts_has_more = posts.len() > Self::SIDEBAR_PAGE_SIZE as usize;
posts.truncate(Self::SIDEBAR_PAGE_SIZE as usize);
self.sidebar_posts = posts;
Task::none() Task::none()
} }
Message::SidebarMediaLoaded { mut items, thumbs } => {
self.sidebar_media_has_more = items.len() > Self::SIDEBAR_PAGE_SIZE as usize;
items.truncate(Self::SIDEBAR_PAGE_SIZE as usize);
self.sidebar_media = items;
self.sidebar_media_thumbs = thumbs;
Task::none()
}
Message::SidebarPostsAppended(mut posts) => {
self.sidebar_posts_has_more = posts.len() > Self::SIDEBAR_PAGE_SIZE as usize;
posts.truncate(Self::SIDEBAR_PAGE_SIZE as usize);
self.sidebar_posts.extend(posts);
Task::none()
}
Message::SidebarMediaAppended { mut items, thumbs } => {
self.sidebar_media_has_more = items.len() > Self::SIDEBAR_PAGE_SIZE as usize;
items.truncate(Self::SIDEBAR_PAGE_SIZE as usize);
self.sidebar_media.extend(items);
self.sidebar_media_thumbs.extend(thumbs);
Task::none()
}
Message::LoadMorePosts => {
self.load_more_sidebar_posts()
}
Message::LoadMoreMedia => {
self.load_more_sidebar_media()
}
// ── Modal ── // ── Modal ──
Message::ShowModal(state) => { Message::ShowModal(state) => {
@@ -1236,6 +1280,9 @@ impl BdsApp {
&self.sidebar_templates, &self.sidebar_templates,
active_post_filter, active_post_filter,
&self.media_filter, &self.media_filter,
&self.sidebar_media_thumbs,
self.sidebar_posts_has_more,
self.sidebar_media_has_more,
active_name, active_name,
&self.projects, &self.projects,
self.active_project.as_ref().map(|p| p.id.as_str()), self.active_project.as_ref().map(|p| p.id.as_str()),
@@ -1534,7 +1581,7 @@ impl BdsApp {
) )
} }
fn refresh_counts(&mut self) { fn refresh_counts(&mut self) -> Task<Message> {
if let (Some(db), Some(project)) = (&self.db, &self.active_project) { if let (Some(db), Some(project)) = (&self.db, &self.active_project) {
self.post_count = bds_core::db::queries::post::count_posts_by_project( self.post_count = bds_core::db::queries::post::count_posts_by_project(
db.conn(), db.conn(),
@@ -1568,17 +1615,27 @@ impl BdsApp {
} }
} }
// Refresh sidebar data with current filters (separate borrows to avoid borrow conflict) // Refresh sidebar data with current filters (async — off main thread)
self.refresh_sidebar_posts(); let t1 = self.refresh_sidebar_posts();
self.refresh_sidebar_media(); let t2 = self.refresh_sidebar_media();
self.refresh_filter_metadata(); self.refresh_filter_metadata();
Task::batch([t1, t2])
} }
/// Refresh only sidebar posts using current filter state. /// Number of items to load per sidebar page.
fn refresh_sidebar_posts(&mut self) { /// Matches the TypeScript app's limit of 500 for initial load.
if let (Some(db), Some(project)) = (&self.db, &self.active_project) { const SIDEBAR_PAGE_SIZE: i64 = 500;
use bds_core::db::queries::post::{PostFilterParams, list_posts_filtered};
/// Refresh only sidebar posts using current filter state (async).
fn refresh_sidebar_posts(&mut self) -> Task<Message> {
let (Some(_), Some(project)) = (&self.db, &self.active_project) else {
return Task::none();
};
use bds_core::db::queries::post::PostFilterParams;
let db_path = self.db_path.clone();
let project_id = project.id.clone();
let filter = match self.sidebar_view { let filter = match self.sidebar_view {
SidebarView::Pages => &self.page_filter, SidebarView::Pages => &self.page_filter,
_ => &self.post_filter, _ => &self.post_filter,
@@ -1595,16 +1652,33 @@ impl BdsApp {
pages_only: is_pages, pages_only: is_pages,
}; };
self.sidebar_posts = list_posts_filtered( Task::perform(
db.conn(), &project.id, &params, 500, 0, async move {
).unwrap_or_default(); tokio::task::spawn_blocking(move || {
} let db = Database::open(&db_path).ok();
db.and_then(|db| {
bds_core::db::queries::post::list_posts_filtered(
db.conn(), &project_id, &params,
Self::SIDEBAR_PAGE_SIZE + 1, 0,
).ok()
}).unwrap_or_default()
}).await.unwrap_or_default()
},
Message::SidebarPostsLoaded,
)
} }
/// Refresh only sidebar media using current filter state. /// Refresh only sidebar media using current filter state (async).
fn refresh_sidebar_media(&mut self) { fn refresh_sidebar_media(&mut self) -> Task<Message> {
if let (Some(db), Some(project)) = (&self.db, &self.active_project) { let (Some(_), Some(project)) = (&self.db, &self.active_project) else {
use bds_core::db::queries::media::{MediaFilterParams, list_media_filtered}; return Task::none();
};
use bds_core::db::queries::media::MediaFilterParams;
let db_path = self.db_path.clone();
let project_id = project.id.clone();
let data_dir = self.data_dir.clone();
let params = MediaFilterParams { let params = MediaFilterParams {
search_query: self.media_filter.search_query.clone(), search_query: self.media_filter.search_query.clone(),
@@ -1613,10 +1687,124 @@ impl BdsApp {
tags: self.media_filter.tag_filter.clone(), tags: self.media_filter.tag_filter.clone(),
}; };
self.sidebar_media = list_media_filtered( Task::perform(
db.conn(), &project.id, &params, 500, 0, async move {
).unwrap_or_default(); tokio::task::spawn_blocking(move || {
let db = Database::open(&db_path).ok();
let items = db.and_then(|db| {
bds_core::db::queries::media::list_media_filtered(
db.conn(), &project_id, &params,
Self::SIDEBAR_PAGE_SIZE + 1, 0,
).ok()
}).unwrap_or_default();
// Pre-resolve thumbnail paths off the main thread
let thumbs: HashMap<String, Option<std::path::PathBuf>> = items.iter().map(|m| {
let thumb = data_dir.as_ref().and_then(|dir| {
if !m.mime_type.starts_with("image/") { return None; }
let rel = bds_core::util::paths::thumbnail_path(&m.id, "small", "webp");
let full = dir.join(&rel);
if full.exists() { Some(full) } else { None }
});
(m.id.clone(), thumb)
}).collect();
(items, thumbs)
}).await.unwrap_or_default()
},
|(items, thumbs)| Message::SidebarMediaLoaded { items, thumbs },
)
} }
/// Load more posts (append to existing sidebar data).
fn load_more_sidebar_posts(&mut self) -> Task<Message> {
let (Some(_), Some(project)) = (&self.db, &self.active_project) else {
return Task::none();
};
use bds_core::db::queries::post::PostFilterParams;
let db_path = self.db_path.clone();
let project_id = project.id.clone();
let offset = self.sidebar_posts.len() as i64;
let filter = match self.sidebar_view {
SidebarView::Pages => &self.page_filter,
_ => &self.post_filter,
};
let is_pages = self.sidebar_view == SidebarView::Pages;
let params = PostFilterParams {
search_query: filter.search_query.clone(),
year: filter.calendar.selected_year,
month: filter.calendar.selected_month,
tags: filter.tag_filter.clone(),
categories: filter.category_filter.clone(),
exclude_pages: !is_pages,
pages_only: is_pages,
};
Task::perform(
async move {
tokio::task::spawn_blocking(move || {
let db = Database::open(&db_path).ok();
db.and_then(|db| {
bds_core::db::queries::post::list_posts_filtered(
db.conn(), &project_id, &params,
Self::SIDEBAR_PAGE_SIZE + 1, offset,
).ok()
}).unwrap_or_default()
}).await.unwrap_or_default()
},
Message::SidebarPostsAppended,
)
}
/// Load more media (append to existing sidebar data).
fn load_more_sidebar_media(&mut self) -> Task<Message> {
let (Some(_), Some(project)) = (&self.db, &self.active_project) else {
return Task::none();
};
use bds_core::db::queries::media::MediaFilterParams;
let db_path = self.db_path.clone();
let project_id = project.id.clone();
let offset = self.sidebar_media.len() as i64;
let data_dir = self.data_dir.clone();
let params = MediaFilterParams {
search_query: self.media_filter.search_query.clone(),
year: self.media_filter.calendar.selected_year,
month: self.media_filter.calendar.selected_month,
tags: self.media_filter.tag_filter.clone(),
};
Task::perform(
async move {
tokio::task::spawn_blocking(move || {
let db = Database::open(&db_path).ok();
let items = db.and_then(|db| {
bds_core::db::queries::media::list_media_filtered(
db.conn(), &project_id, &params,
Self::SIDEBAR_PAGE_SIZE + 1, offset,
).ok()
}).unwrap_or_default();
let thumbs: HashMap<String, Option<std::path::PathBuf>> = items.iter().map(|m| {
let thumb = data_dir.as_ref().and_then(|dir| {
if !m.mime_type.starts_with("image/") { return None; }
let rel = bds_core::util::paths::thumbnail_path(&m.id, "small", "webp");
let full = dir.join(&rel);
if full.exists() { Some(full) } else { None }
});
(m.id.clone(), thumb)
}).collect();
(items, thumbs)
}).await.unwrap_or_default()
},
|(items, thumbs)| Message::SidebarMediaAppended { items, thumbs },
)
} }
/// Refresh available tags, categories, and calendar data for filter widgets. /// Refresh available tags, categories, and calendar data for filter widgets.

View File

@@ -4,8 +4,6 @@ use iced::widget::{button, column, container, image, row, scrollable, text, text
use iced::widget::text::Shaping; use iced::widget::text::Shaping;
use iced::{Background, Border, Color, Element, Length, Theme}; use iced::{Background, Border, Color, Element, Length, Theme};
use bds_core::util::paths::thumbnail_path;
use bds_core::i18n::UiLocale; use bds_core::i18n::UiLocale;
use bds_core::model::{Media, Post, Script, Template}; use bds_core::model::{Media, Post, Script, Template};
@@ -112,18 +110,6 @@ fn format_file_size(size: i64) -> String {
} }
} }
/// Resolve the small thumbnail path for a media item on disk.
/// Returns Some(path) if the media is an image and the thumbnail file exists.
fn resolve_thumbnail_path(data_dir: Option<&Path>, media: &Media) -> Option<PathBuf> {
let data_dir = data_dir?;
if !media.mime_type.starts_with("image/") {
return None;
}
let rel = thumbnail_path(&media.id, "small", "webp");
let full = data_dir.join(&rel);
if full.exists() { Some(full) } else { None }
}
/// Truncate a media title to the max length, appending "..." if over limit. /// Truncate a media title to the max length, appending "..." if over limit.
/// Per sidebar_views.allium: JS hard limit of 60 chars on title (substring + "..."). /// Per sidebar_views.allium: JS hard limit of 60 chars on title (substring + "...").
fn truncate_media_title(title: &str) -> String { fn truncate_media_title(title: &str) -> String {
@@ -518,10 +504,13 @@ pub fn view(
templates: &[Template], templates: &[Template],
post_filter: &PostFilter, post_filter: &PostFilter,
media_filter: &MediaFilter, media_filter: &MediaFilter,
media_thumbs: &std::collections::HashMap<String, Option<PathBuf>>,
posts_has_more: bool,
media_has_more: bool,
width: f32, width: f32,
active_tab: Option<&str>, active_tab: Option<&str>,
locale: UiLocale, locale: UiLocale,
data_dir: Option<&Path>, _data_dir: Option<&Path>,
) -> Element<'static, Message> { ) -> Element<'static, Message> {
let header_text = t(locale, sidebar_view.i18n_key()); let header_text = t(locale, sidebar_view.i18n_key());
let muted = Color::from_rgb(0.50, 0.50, 0.55); let muted = Color::from_rgb(0.50, 0.50, 0.55);
@@ -670,6 +659,23 @@ pub fn view(
top_items.push(make_post_item(p)); top_items.push(make_post_item(p));
} }
} }
// "Load More" button
if posts_has_more {
top_items.push(Space::with_height(4.0).into());
top_items.push(
button(
text(t(locale, "sidebar.loadMore"))
.size(11)
.shaping(Shaping::Advanced)
)
.on_press(Message::LoadMorePosts)
.padding([4, 8])
.width(Length::Fill)
.style(filter_button_style)
.into()
);
}
} }
iced::widget::Column::with_children(top_items) iced::widget::Column::with_children(top_items)
@@ -746,8 +752,10 @@ pub fn view(
let display_name = truncate_to_fit(&display_name, text_px); let display_name = truncate_to_fit(&display_name, text_px);
let style_fn = if is_active { item_active_style } else { item_style }; let style_fn = if is_active { item_active_style } else { item_style };
// Left: 40x40 thumbnail or file icon // Left: 40x40 thumbnail or file icon (pre-resolved, no filesystem I/O)
let thumb_path = resolve_thumbnail_path(data_dir, m); let thumb_path = media_thumbs
.get(&m.id)
.and_then(|opt| opt.as_ref());
let left: Element<'static, Message> = if let Some(path) = thumb_path { let left: Element<'static, Message> = if let Some(path) = thumb_path {
container( container(
image(path.to_string_lossy().to_string()) image(path.to_string_lossy().to_string())
@@ -813,6 +821,23 @@ pub fn view(
}) })
.collect(); .collect();
top_items.extend(items); top_items.extend(items);
// "Load More" button
if media_has_more {
top_items.push(Space::with_height(4.0).into());
top_items.push(
button(
text(t(locale, "sidebar.loadMore"))
.size(11)
.shaping(Shaping::Advanced)
)
.on_press(Message::LoadMoreMedia)
.padding([4, 8])
.width(Length::Fill)
.style(filter_button_style)
.into()
);
}
} }
iced::widget::Column::with_children(top_items) iced::widget::Column::with_children(top_items)

View File

@@ -1,5 +1,5 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path; use std::path::{Path, PathBuf};
use iced::widget::{button, column, container, mouse_area, row, stack, text, Space}; use iced::widget::{button, column, container, mouse_area, row, stack, text, Space};
use iced::widget::text::Shaping; use iced::widget::text::Shaping;
@@ -74,6 +74,11 @@ pub fn view<'a>(
// Sidebar filters // Sidebar filters
post_filter: &'a PostFilter, post_filter: &'a PostFilter,
media_filter: &'a MediaFilter, media_filter: &'a MediaFilter,
// Pre-resolved media thumbnail paths
sidebar_media_thumbs: &'a HashMap<String, Option<PathBuf>>,
// Pagination
sidebar_posts_has_more: bool,
sidebar_media_has_more: bool,
// Status bar // Status bar
active_project_name: Option<&'a str>, active_project_name: Option<&'a str>,
projects: &'a [Project], projects: &'a [Project],
@@ -141,7 +146,7 @@ pub fn view<'a>(
let mut main_row = row![activity]; let mut main_row = row![activity];
if sidebar_visible { if sidebar_visible {
main_row = main_row.push(sidebar::view(sidebar_view, sidebar_posts, sidebar_media, sidebar_scripts, sidebar_templates, post_filter, media_filter, sidebar_width, active_tab, locale, data_dir)); main_row = main_row.push(sidebar::view(sidebar_view, sidebar_posts, sidebar_media, sidebar_scripts, sidebar_templates, post_filter, media_filter, sidebar_media_thumbs, sidebar_posts_has_more, sidebar_media_has_more, sidebar_width, active_tab, locale, data_dir));
// Resize drag handle: 4px wide strip between sidebar and content // Resize drag handle: 4px wide strip between sidebar and content
let handle = container(Space::new(0, 0)) let handle = container(Space::new(0, 0))

View File

@@ -112,6 +112,7 @@
"sidebar.drafts": "Entwürfe", "sidebar.drafts": "Entwürfe",
"sidebar.published": "Veröffentlicht", "sidebar.published": "Veröffentlicht",
"sidebar.archived": "Archiviert", "sidebar.archived": "Archiviert",
"sidebar.loadMore": "Mehr laden…",
"language.en": "Englisch", "language.en": "Englisch",
"language.de": "Deutsch", "language.de": "Deutsch",
"language.fr": "Französisch", "language.fr": "Französisch",

View File

@@ -112,6 +112,7 @@
"sidebar.drafts": "Drafts", "sidebar.drafts": "Drafts",
"sidebar.published": "Published", "sidebar.published": "Published",
"sidebar.archived": "Archived", "sidebar.archived": "Archived",
"sidebar.loadMore": "Load More…",
"language.en": "English", "language.en": "English",
"language.de": "German", "language.de": "German",
"language.fr": "French", "language.fr": "French",

View File

@@ -112,6 +112,7 @@
"sidebar.drafts": "Borradores", "sidebar.drafts": "Borradores",
"sidebar.published": "Publicados", "sidebar.published": "Publicados",
"sidebar.archived": "Archivados", "sidebar.archived": "Archivados",
"sidebar.loadMore": "Cargar más…",
"language.en": "Inglés", "language.en": "Inglés",
"language.de": "Alemán", "language.de": "Alemán",
"language.fr": "Francés", "language.fr": "Francés",

View File

@@ -112,6 +112,7 @@
"sidebar.drafts": "Brouillons", "sidebar.drafts": "Brouillons",
"sidebar.published": "Publiés", "sidebar.published": "Publiés",
"sidebar.archived": "Archivés", "sidebar.archived": "Archivés",
"sidebar.loadMore": "Charger plus…",
"language.en": "Anglais", "language.en": "Anglais",
"language.de": "Allemand", "language.de": "Allemand",
"language.fr": "Français", "language.fr": "Français",

View File

@@ -112,6 +112,7 @@
"sidebar.drafts": "Bozze", "sidebar.drafts": "Bozze",
"sidebar.published": "Pubblicati", "sidebar.published": "Pubblicati",
"sidebar.archived": "Archiviati", "sidebar.archived": "Archiviati",
"sidebar.loadMore": "Carica di più…",
"language.en": "Inglese", "language.en": "Inglese",
"language.de": "Tedesco", "language.de": "Tedesco",
"language.fr": "Francese", "language.fr": "Francese",