diff --git a/crates/bds-ui/src/app.rs b/crates/bds-ui/src/app.rs index 3965012..3cc0c6d 100644 --- a/crates/bds-ui/src/app.rs +++ b/crates/bds-ui/src/app.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use std::path::PathBuf; use std::sync::Arc; +use iced::widget::text_editor; use iced::{Element, Subscription, Task}; use bds_core::db::Database; @@ -365,13 +366,13 @@ impl BdsApp { let old_view = self.sidebar_view; self.sidebar_view = new_view; self.sidebar_visible = new_visible; - // When switching between Posts/Pages, re-query with correct filter - let switched_post_scope = matches!( - (old_view, new_view), - (SidebarView::Posts, SidebarView::Pages) - | (SidebarView::Pages, SidebarView::Posts) - ); - if switched_post_scope { + // When switching to/from Posts/Pages, re-query with correct filter + let needs_post_refresh = old_view != new_view + && matches!( + new_view, + SidebarView::Posts | SidebarView::Pages + ); + if needs_post_refresh { self.refresh_sidebar_posts(); } Task::none() @@ -922,10 +923,45 @@ impl BdsApp { PostEditorMsg::TitleChanged(s) => { state.title = s; state.is_dirty = true; } PostEditorMsg::SlugChanged(s) => { state.slug = s; state.is_dirty = true; } PostEditorMsg::ExcerptChanged(s) => { state.excerpt = s; state.is_dirty = true; } - PostEditorMsg::ContentChanged(s) => { state.content = s; state.is_dirty = true; } + PostEditorMsg::ContentAction(action) => { + let is_edit = matches!(action, text_editor::Action::Edit(_)); + state.editor_content.perform(action); + if is_edit { + state.content = state.editor_content.text(); + state.is_dirty = true; + } + } PostEditorMsg::AuthorChanged(s) => { state.author = s; state.is_dirty = true; } PostEditorMsg::TemplateSlugChanged(s) => { state.template_slug = s; state.is_dirty = true; } PostEditorMsg::ToggleDoNotTranslate(b) => { state.do_not_translate = b; state.is_dirty = true; } + PostEditorMsg::ToggleMetadata => { state.metadata_expanded = !state.metadata_expanded; } + PostEditorMsg::ToggleExcerpt => { state.excerpt_expanded = !state.excerpt_expanded; } + PostEditorMsg::TagsInputChanged(s) => { state.tags_input = s; } + PostEditorMsg::TagsInputSubmit => { + let tag = state.tags_input.trim().to_string(); + if !tag.is_empty() && !state.tags.contains(&tag) { + state.tags.push(tag); + state.is_dirty = true; + } + state.tags_input.clear(); + } + PostEditorMsg::RemoveTag(tag) => { + state.tags.retain(|t| t != &tag); + state.is_dirty = true; + } + PostEditorMsg::CategoriesInputChanged(s) => { state.categories_input = s; } + PostEditorMsg::CategoriesInputSubmit => { + let cat = state.categories_input.trim().to_string(); + if !cat.is_empty() && !state.categories.contains(&cat) { + state.categories.push(cat); + state.is_dirty = true; + } + state.categories_input.clear(); + } + PostEditorMsg::RemoveCategory(cat) => { + state.categories.retain(|c| c != &cat); + state.is_dirty = true; + } PostEditorMsg::Save => { return self.save_post_editor(&tab_id); } @@ -1970,7 +2006,24 @@ impl BdsApp { TabType::Post => { if !self.post_editors.contains_key(&tab.id) { match bds_core::db::queries::post::get_post_by_id(db.conn(), &tab.id) { - Ok(post) => { + Ok(mut post) => { + // Published posts don't store body in DB — read from file + if post.content.is_none() { + if let Some(ref data_dir) = self.data_dir { + let rel = bds_core::util::paths::post_file_path( + post.created_at, + &post.slug, + ); + let path = data_dir.join(&rel); + if let Ok(raw) = std::fs::read_to_string(&path) { + if let Ok((_fm, body)) = + bds_core::util::frontmatter::read_post_file(&raw) + { + post.content = Some(body); + } + } + } + } self.post_editors.insert(post.id.clone(), PostEditorState::from_post(&post)); } Err(e) => { diff --git a/crates/bds-ui/src/components/inputs.rs b/crates/bds-ui/src/components/inputs.rs index 69a2866..0b12745 100644 --- a/crates/bds-ui/src/components/inputs.rs +++ b/crates/bds-ui/src/components/inputs.rs @@ -1,10 +1,11 @@ use iced::widget::{button, checkbox, column, container, pick_list, row, text, text_input, Space}; +use iced::widget::text::Shaping; use iced::{Alignment, Background, Color, Element, Length, Theme}; /// Standard form field label color. -const LABEL_COLOR: Color = Color::from_rgb(0.65, 0.68, 0.75); +pub const LABEL_COLOR: Color = Color::from_rgb(0.65, 0.68, 0.75); const _FIELD_BG: Color = Color::from_rgb(0.15, 0.16, 0.20); -const SECTION_COLOR: Color = Color::from_rgb(0.50, 0.52, 0.58); +pub const SECTION_COLOR: Color = Color::from_rgb(0.50, 0.52, 0.58); const DANGER_BG: Color = Color::from_rgb(0.60, 0.15, 0.15); const DANGER_HOVER: Color = Color::from_rgb(0.70, 0.20, 0.20); const PRIMARY_BG: Color = Color::from_rgb(0.20, 0.40, 0.70); @@ -18,7 +19,7 @@ pub fn labeled_input<'a, Message: Clone + 'a>( on_change: impl Fn(String) -> Message + 'a, ) -> Element<'a, Message> { column![ - text(label.to_string()).size(12).color(LABEL_COLOR), + text(label.to_string()).size(12).color(LABEL_COLOR).shaping(Shaping::Advanced), text_input(placeholder, value).on_input(on_change).size(14), ] .spacing(4) @@ -38,7 +39,7 @@ where { let list: Vec = options.to_vec(); column![ - text(label.to_string()).size(12).color(LABEL_COLOR), + text(label.to_string()).size(12).color(LABEL_COLOR).shaping(Shaping::Advanced), pick_list(list, selected.cloned(), on_select), ] .spacing(4) @@ -63,7 +64,8 @@ pub fn section_header<'a, Message: 'a>(label: &str) -> Element<'a, Message> { column![ text(label.to_string()) .size(11) - .color(SECTION_COLOR), + .color(SECTION_COLOR) + .shaping(Shaping::Advanced), container(Space::new(0, 0)) .width(Length::Fill) .height(Length::Fixed(1.0)) @@ -131,15 +133,21 @@ pub fn toolbar<'a, Message: 'a>( left: Vec>, right: Vec>, ) -> Element<'a, Message> { - let left_row = iced::widget::Row::with_children(left) - .spacing(8) - .align_y(Alignment::Center); + let left_row = container( + iced::widget::Row::with_children(left) + .spacing(8) + .align_y(Alignment::Center), + ) + .clip(true) + .width(Length::Fill); let right_row = iced::widget::Row::with_children(right) .spacing(8) - .align_y(Alignment::Center); + .align_y(Alignment::Center) + .wrap(); - row![left_row, Space::with_width(Length::Fill), right_row] + row![left_row, right_row] .padding(8) + .spacing(8) .align_y(Alignment::Center) .width(Length::Fill) .into() @@ -149,8 +157,8 @@ pub fn toolbar<'a, Message: 'a>( pub fn date_label<'a, Message: 'a>(label: &str, timestamp_ms: i64) -> Element<'a, Message> { let date_str = format_timestamp(timestamp_ms); row![ - text(label.to_string()).size(12).color(LABEL_COLOR), - text(date_str).size(12).color(Color::from_rgb(0.55, 0.58, 0.65)), + text(label.to_string()).size(12).color(LABEL_COLOR).shaping(Shaping::Advanced), + text(date_str).size(12).color(Color::from_rgb(0.55, 0.58, 0.65)).shaping(Shaping::Advanced), ] .spacing(8) .into() diff --git a/crates/bds-ui/src/views/post_editor.rs b/crates/bds-ui/src/views/post_editor.rs index 5641976..e3778b0 100644 --- a/crates/bds-ui/src/views/post_editor.rs +++ b/crates/bds-ui/src/views/post_editor.rs @@ -1,4 +1,5 @@ -use iced::widget::{button, column, container, row, scrollable, text, text_input, Space}; +use iced::widget::{button, column, container, row, scrollable, text, text_input, text_editor, Space}; +use iced::widget::text::{Shaping, Wrapping}; use iced::{Color, Element, Length, Theme}; use bds_core::i18n::UiLocale; @@ -9,13 +10,13 @@ use crate::components::inputs; use crate::i18n::t; /// State for an open post editor. -#[derive(Debug, Clone)] pub struct PostEditorState { pub post_id: String, pub title: String, pub slug: String, pub excerpt: String, pub content: String, + pub editor_content: text_editor::Content, pub tags: Vec, pub categories: Vec, pub author: String, @@ -26,16 +27,58 @@ pub struct PostEditorState { pub created_at: i64, pub updated_at: i64, pub is_dirty: bool, + pub metadata_expanded: bool, + pub excerpt_expanded: bool, + pub tags_input: String, + pub categories_input: String, +} + +impl std::fmt::Debug for PostEditorState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PostEditorState") + .field("post_id", &self.post_id) + .field("title", &self.title) + .finish_non_exhaustive() + } +} + +impl Clone for PostEditorState { + fn clone(&self) -> Self { + Self { + post_id: self.post_id.clone(), + title: self.title.clone(), + slug: self.slug.clone(), + excerpt: self.excerpt.clone(), + content: self.content.clone(), + editor_content: text_editor::Content::with_text(&self.content), + tags: self.tags.clone(), + categories: self.categories.clone(), + author: self.author.clone(), + language: self.language.clone(), + template_slug: self.template_slug.clone(), + do_not_translate: self.do_not_translate, + status: self.status.clone(), + created_at: self.created_at, + updated_at: self.updated_at, + is_dirty: self.is_dirty, + metadata_expanded: self.metadata_expanded, + excerpt_expanded: self.excerpt_expanded, + tags_input: self.tags_input.clone(), + categories_input: self.categories_input.clone(), + } + } } impl PostEditorState { pub fn from_post(post: &Post) -> Self { + let title = post.title.clone(); + let content = post.content.clone().unwrap_or_default(); Self { post_id: post.id.clone(), - title: post.title.clone(), slug: post.slug.clone(), excerpt: post.excerpt.clone().unwrap_or_default(), - content: post.content.clone().unwrap_or_default(), + content: content.clone(), + editor_content: text_editor::Content::with_text(&content), tags: post.tags.clone(), categories: post.categories.clone(), author: post.author.clone().unwrap_or_default(), @@ -46,6 +89,11 @@ impl PostEditorState { created_at: post.created_at, updated_at: post.updated_at, is_dirty: false, + metadata_expanded: title.is_empty(), + excerpt_expanded: false, + tags_input: String::new(), + categories_input: String::new(), + title, } } } @@ -56,10 +104,18 @@ pub enum PostEditorMsg { TitleChanged(String), SlugChanged(String), ExcerptChanged(String), - ContentChanged(String), + ContentAction(text_editor::Action), AuthorChanged(String), TemplateSlugChanged(String), ToggleDoNotTranslate(bool), + ToggleMetadata, + ToggleExcerpt, + TagsInputChanged(String), + TagsInputSubmit, + RemoveTag(String), + CategoriesInputChanged(String), + CategoriesInputSubmit, + RemoveCategory(String), Save, Publish, Delete, @@ -70,23 +126,39 @@ pub fn view<'a>( state: &'a PostEditorState, locale: UiLocale, ) -> Element<'a, Message> { + // ── Header bar ── + let dirty_indicator = if state.is_dirty { " \u{25CF}" } else { "" }; + let title_display = if state.title.is_empty() { + t(locale, "editor.untitled") + } else { + format!("{}{}", state.title, dirty_indicator) + }; + let header = inputs::toolbar( vec![ - text(state.title.clone()).size(18).into(), - status_badge(&state.status), + text(title_display) + .size(18) + .wrapping(Wrapping::None) + .shaping(Shaping::Advanced) + .into(), ], vec![ - button(text(t(locale, "common.save")).size(13)) + status_badge(&state.status), + button(text(t(locale, "common.save")).size(13).shaping(Shaping::Advanced)) .on_press(Message::PostEditor(PostEditorMsg::Save)) .style(inputs::primary_button) .padding([6, 16]) .into(), - button(text(t(locale, "editor.publish")).size(13)) - .on_press(Message::PostEditor(PostEditorMsg::Publish)) - .style(inputs::primary_button) - .padding([6, 16]) - .into(), - button(text(t(locale, "modal.confirmDelete.delete")).size(13)) + if state.status == PostStatus::Draft { + button(text(t(locale, "editor.publish")).size(13).shaping(Shaping::Advanced)) + .on_press(Message::PostEditor(PostEditorMsg::Publish)) + .style(inputs::primary_button) + .padding([6, 16]) + .into() + } else { + Space::new(0, 0).into() + }, + button(text(t(locale, "modal.confirmDelete.delete")).size(13).shaping(Shaping::Advanced)) .on_press(Message::PostEditor(PostEditorMsg::Delete)) .style(inputs::danger_button) .padding([6, 16]) @@ -94,64 +166,127 @@ pub fn view<'a>( ], ); - // Metadata section - let title_input = inputs::labeled_input( - &t(locale, "editor.title"), - &t(locale, "editor.titlePlaceholder"), - &state.title, - |s| Message::PostEditor(PostEditorMsg::TitleChanged(s)), - ); - let slug_input = inputs::labeled_input( - &t(locale, "editor.slug"), - &t(locale, "editor.slugPlaceholder"), - &state.slug, - |s| Message::PostEditor(PostEditorMsg::SlugChanged(s)), - ); - let meta_row1 = row![title_input, slug_input].spacing(16).width(Length::Fill); + // ── Collapsible Metadata Section ── + let meta_toggle_label = if state.metadata_expanded { + format!("\u{25BC} {}", t(locale, "editor.metadata")) + } else { + format!("\u{25B6} {}", t(locale, "editor.metadata")) + }; + let meta_toggle = button( + text(meta_toggle_label).size(12).color(inputs::SECTION_COLOR).shaping(Shaping::Advanced), + ) + .on_press(Message::PostEditor(PostEditorMsg::ToggleMetadata)) + .padding([4, 0]) + .style(|_, _| button::Style { + background: None, + ..button::Style::default() + }); - let author_input = inputs::labeled_input( - &t(locale, "editor.author"), - "", - &state.author, - |s| Message::PostEditor(PostEditorMsg::AuthorChanged(s)), - ); - let template_input = inputs::labeled_input( - &t(locale, "editor.templateSlug"), - "", - &state.template_slug, - |s| Message::PostEditor(PostEditorMsg::TemplateSlugChanged(s)), - ); - let dnt = inputs::labeled_checkbox( - &t(locale, "editor.doNotTranslate"), - state.do_not_translate, - |b| Message::PostEditor(PostEditorMsg::ToggleDoNotTranslate(b)), - ); - let meta_row2 = row![author_input, template_input, dnt].spacing(16).width(Length::Fill); + let metadata_section: Element<'a, Message> = if state.metadata_expanded { + let title_input = inputs::labeled_input( + &t(locale, "editor.title"), + &t(locale, "editor.titlePlaceholder"), + &state.title, + |s| Message::PostEditor(PostEditorMsg::TitleChanged(s)), + ); + let slug_input = inputs::labeled_input( + &t(locale, "editor.slug"), + &t(locale, "editor.slugPlaceholder"), + &state.slug, + |s| Message::PostEditor(PostEditorMsg::SlugChanged(s)), + ); + let meta_row1 = row![title_input, slug_input] + .spacing(16) + .width(Length::Fill); - // Excerpt - let excerpt_input = inputs::labeled_input( - &t(locale, "editor.excerpt"), - &t(locale, "editor.excerptPlaceholder"), - &state.excerpt, - |s| Message::PostEditor(PostEditorMsg::ExcerptChanged(s)), - ); + let author_input = inputs::labeled_input( + &t(locale, "editor.author"), + "", + &state.author, + |s| Message::PostEditor(PostEditorMsg::AuthorChanged(s)), + ); + let template_input = inputs::labeled_input( + &t(locale, "editor.templateSlug"), + "", + &state.template_slug, + |s| Message::PostEditor(PostEditorMsg::TemplateSlugChanged(s)), + ); + let dnt = inputs::labeled_checkbox( + &t(locale, "editor.doNotTranslate"), + state.do_not_translate, + |b| Message::PostEditor(PostEditorMsg::ToggleDoNotTranslate(b)), + ); + let meta_row2 = row![author_input, template_input, dnt] + .spacing(16) + .width(Length::Fill); - // Content (text area placeholder — full editor will use CodeEditor widget) - let content_section: Element<'a, Message> = column![ - inputs::section_header(&t(locale, "editor.content")), - container( - text_input(&t(locale, "editor.contentPlaceholder"), &state.content) - .on_input(|s| Message::PostEditor(PostEditorMsg::ContentChanged(s))) - .size(14) + // Tags chip input + let tags_section = chip_input_field( + &t(locale, "editor.tags"), + &t(locale, "editor.tagsPlaceholder"), + &state.tags, + &state.tags_input, + |s| Message::PostEditor(PostEditorMsg::TagsInputChanged(s)), + Message::PostEditor(PostEditorMsg::TagsInputSubmit), + |tag| Message::PostEditor(PostEditorMsg::RemoveTag(tag)), + ); + + // Categories chip input + let categories_section = chip_input_field( + &t(locale, "editor.categories"), + &t(locale, "editor.categoriesPlaceholder"), + &state.categories, + &state.categories_input, + |s| Message::PostEditor(PostEditorMsg::CategoriesInputChanged(s)), + Message::PostEditor(PostEditorMsg::CategoriesInputSubmit), + |cat| Message::PostEditor(PostEditorMsg::RemoveCategory(cat)), + ); + + column![meta_row1, meta_row2, tags_section, categories_section] + .spacing(8) + .width(Length::Fill) + .into() + } else { + Space::new(0, 0).into() + }; + + // ── Collapsible Excerpt Section ── + let excerpt_toggle_label = if state.excerpt_expanded { + format!("\u{25BC} {}", t(locale, "editor.excerpt")) + } else { + format!("\u{25B6} {}", t(locale, "editor.excerpt")) + }; + let excerpt_toggle = button( + text(excerpt_toggle_label).size(12).color(inputs::SECTION_COLOR).shaping(Shaping::Advanced), + ) + .on_press(Message::PostEditor(PostEditorMsg::ToggleExcerpt)) + .padding([4, 0]) + .style(|_, _| button::Style { + background: None, + ..button::Style::default() + }); + + let excerpt_section: Element<'a, Message> = if state.excerpt_expanded { + inputs::labeled_input( + &t(locale, "editor.excerpt"), + &t(locale, "editor.excerptPlaceholder"), + &state.excerpt, + |s| Message::PostEditor(PostEditorMsg::ExcerptChanged(s)), ) - .width(Length::Fill) - .height(Length::Fixed(300.0)), - ] - .spacing(8) - .width(Length::Fill) - .into(); + } else { + Space::new(0, 0).into() + }; - // Footer + // ── Content section (fills remaining space) ── + let content_placeholder = t(locale, "editor.contentPlaceholder"); + let content_label = inputs::section_header(&t(locale, "editor.content")); + let editor_widget = text_editor(&state.editor_content) + .placeholder(content_placeholder) + .on_action(|action| Message::PostEditor(PostEditorMsg::ContentAction(action))) + .height(Length::Fill) + .style(editor_style); + + // ── Footer ── let footer = row![ inputs::date_label(&t(locale, "editor.createdAt"), state.created_at), Space::with_width(Length::Fixed(24.0)), @@ -159,24 +294,112 @@ pub fn view<'a>( ] .padding(8); - let body = scrollable( + // ── Top pane: header + collapsible sections (scrollable for overflow) ── + let top_pane = scrollable( column![ header, - meta_row1, - meta_row2, - excerpt_input, - content_section, - footer, + meta_toggle, + metadata_section, + excerpt_toggle, + excerpt_section, ] - .spacing(12) - .padding(16) + .spacing(4) .width(Length::Fill) - ); + ) + .height(Length::Shrink); - container(body) - .width(Length::Fill) - .height(Length::Fill) - .into() + // ── Full layout: top pane (shrink), editor (fill), footer (shrink) ── + column![ + top_pane, + content_label, + editor_widget, + footer, + ] + .spacing(4) + .padding(16) + .width(Length::Fill) + .height(Length::Fill) + .into() +} + +/// Dark editor style for the text_editor widget. +fn editor_style(_theme: &Theme, status: text_editor::Status) -> text_editor::Style { + let bg = Color::from_rgb(0.10, 0.11, 0.14); + let border_color = match status { + text_editor::Status::Focused => Color::from_rgb(0.30, 0.50, 0.80), + text_editor::Status::Hovered => Color::from_rgb(0.30, 0.32, 0.40), + _ => Color::from_rgb(0.22, 0.24, 0.30), + }; + text_editor::Style { + background: iced::Background::Color(bg), + border: iced::Border { + radius: 4.0.into(), + width: 1.0, + color: border_color, + }, + icon: Color::from_rgb(0.50, 0.52, 0.58), + placeholder: Color::from_rgb(0.40, 0.42, 0.48), + value: Color::from_rgb(0.85, 0.87, 0.92), + selection: Color::from_rgba(0.30, 0.50, 0.80, 0.40), + } +} + +/// Chip input: shows existing chips as removable buttons + a text input to add new ones. +fn chip_input_field<'a>( + label: &str, + placeholder: &str, + chips: &[String], + input_value: &str, + on_input: impl Fn(String) -> Message + 'a, + on_submit: Message, + on_remove: impl Fn(String) -> Message + 'a, +) -> Element<'a, Message> { + let chip_elements: Vec> = chips + .iter() + .map(|chip| { + let label = format!("{} \u{2715}", chip); + let chip_val = chip.clone(); + button(text(label).size(11).shaping(Shaping::Advanced)) + .on_press(on_remove(chip_val)) + .padding([2, 6]) + .style(chip_button_style) + .into() + }) + .collect(); + + let mut chip_row = row![].spacing(4); + for el in chip_elements { + chip_row = chip_row.push(el); + } + + column![ + text(label.to_string()).size(12).color(inputs::LABEL_COLOR).shaping(Shaping::Advanced), + chip_row.wrap(), + text_input(placeholder, input_value) + .on_input(on_input) + .on_submit(on_submit) + .size(13) + .padding([4, 6]), + ] + .spacing(4) + .width(Length::Fill) + .into() +} + +fn chip_button_style(_theme: &Theme, status: button::Status) -> button::Style { + let bg = match status { + button::Status::Hovered => Color::from_rgb(0.30, 0.35, 0.45), + _ => Color::from_rgb(0.22, 0.25, 0.32), + }; + button::Style { + background: Some(iced::Background::Color(bg)), + text_color: Color::from_rgb(0.80, 0.82, 0.88), + border: iced::Border { + radius: 3.0.into(), + ..iced::Border::default() + }, + ..button::Style::default() + } } fn status_badge<'a>(status: &PostStatus) -> Element<'a, Message> { @@ -185,15 +408,21 @@ fn status_badge<'a>(status: &PostStatus) -> Element<'a, Message> { PostStatus::Published => ("Published", Color::from_rgb(0.2, 0.7, 0.3)), PostStatus::Archived => ("Archived", Color::from_rgb(0.5, 0.5, 0.5)), }; - container(text(label).size(11).color(color)) - .padding([2, 8]) - .style(move |_: &Theme| container::Style { - border: iced::Border { - radius: 4.0.into(), - width: 1.0, - color, - }, - ..container::Style::default() - }) - .into() + container( + text(label) + .size(11) + .color(color) + .wrapping(Wrapping::None) + .shaping(Shaping::Advanced), + ) + .padding([2, 8]) + .style(move |_: &Theme| container::Style { + border: iced::Border { + radius: 4.0.into(), + width: 1.0, + color, + }, + ..container::Style::default() + }) + .into() } diff --git a/locales/ui/de.json b/locales/ui/de.json index d542a13..ef7d07d 100644 --- a/locales/ui/de.json +++ b/locales/ui/de.json @@ -194,6 +194,11 @@ "editor.altPlaceholder": "Bild beschreiben...", "editor.caption": "Bildunterschrift", "editor.metadata": "Metadaten", + "editor.tags": "Tags", + "editor.tagsPlaceholder": "Tag hinzufügen...", + "editor.categories": "Kategorien", + "editor.categoriesPlaceholder": "Kategorie hinzufügen...", + "editor.untitled": "Ohne Titel", "editor.createdAt": "Erstellt", "editor.updatedAt": "Aktualisiert", "tags.noTags": "Noch keine Tags", diff --git a/locales/ui/en.json b/locales/ui/en.json index 267d85a..b9f81d0 100644 --- a/locales/ui/en.json +++ b/locales/ui/en.json @@ -194,6 +194,11 @@ "editor.altPlaceholder": "Describe the image...", "editor.caption": "Caption", "editor.metadata": "Metadata", + "editor.tags": "Tags", + "editor.tagsPlaceholder": "Add tag...", + "editor.categories": "Categories", + "editor.categoriesPlaceholder": "Add category...", + "editor.untitled": "Untitled", "editor.createdAt": "Created", "editor.updatedAt": "Updated", "tags.noTags": "No tags yet", diff --git a/locales/ui/es.json b/locales/ui/es.json index 2a41e87..ee9e3d7 100644 --- a/locales/ui/es.json +++ b/locales/ui/es.json @@ -194,6 +194,11 @@ "editor.altPlaceholder": "Describir la imagen...", "editor.caption": "Leyenda", "editor.metadata": "Metadatos", + "editor.tags": "Etiquetas", + "editor.tagsPlaceholder": "Añadir etiqueta...", + "editor.categories": "Categorías", + "editor.categoriesPlaceholder": "Añadir categoría...", + "editor.untitled": "Sin título", "editor.createdAt": "Creado", "editor.updatedAt": "Actualizado", "tags.noTags": "Sin etiquetas", diff --git a/locales/ui/fr.json b/locales/ui/fr.json index b5f32f6..4c7e980 100644 --- a/locales/ui/fr.json +++ b/locales/ui/fr.json @@ -194,6 +194,11 @@ "editor.altPlaceholder": "Décrivez l'image...", "editor.caption": "Légende", "editor.metadata": "Métadonnées", + "editor.tags": "Tags", + "editor.tagsPlaceholder": "Ajouter un tag...", + "editor.categories": "Catégories", + "editor.categoriesPlaceholder": "Ajouter une catégorie...", + "editor.untitled": "Sans titre", "editor.createdAt": "Créé", "editor.updatedAt": "Mis à jour", "tags.noTags": "Aucun tag", diff --git a/locales/ui/it.json b/locales/ui/it.json index da33f3b..ed8eaa6 100644 --- a/locales/ui/it.json +++ b/locales/ui/it.json @@ -194,6 +194,11 @@ "editor.altPlaceholder": "Descrivi l'immagine...", "editor.caption": "Didascalia", "editor.metadata": "Metadati", + "editor.tags": "Tag", + "editor.tagsPlaceholder": "Aggiungi tag...", + "editor.categories": "Categorie", + "editor.categoriesPlaceholder": "Aggiungi categoria...", + "editor.untitled": "Senza titolo", "editor.createdAt": "Creato", "editor.updatedAt": "Aggiornato", "tags.noTags": "Nessun tag",