feat: added a gallery quick action and fleshed out builtin macros
This commit is contained in:
@@ -113,6 +113,7 @@ This document provides context and best practices for GitHub Copilot when workin
|
|||||||
- For supported locales, translations MUST come from that locale file only; do not copy English terms into supported locale files as fallback
|
- For supported locales, translations MUST come from that locale file only; do not copy English terms into supported locale files as fallback
|
||||||
- English fallback is allowed only when the requested locale is unsupported by available locale files
|
- English fallback is allowed only when the requested locale is unsupported by available locale files
|
||||||
- The project `mainLanguage` selector must expose exactly all supported render languages and no unsupported extras
|
- The project `mainLanguage` selector must expose exactly all supported render languages and no unsupported extras
|
||||||
|
- When adding new `msgid` entries, you MUST provide translations for ALL supported locales (de, fr, it, es) — empty `msgstr` values are not acceptable
|
||||||
|
|
||||||
> **No hardcoded user-facing text. No exceptions.**
|
> **No hardcoded user-facing text. No exceptions.**
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,17 @@ defmodule BDS.Desktop.FilePicker do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def choose_files(prompt, opts \\ []) when is_binary(prompt) do
|
||||||
|
if System.get_env("BDS_DESKTOP_AUTOMATION") == "1" do
|
||||||
|
:cancel
|
||||||
|
else
|
||||||
|
case :os.type() do
|
||||||
|
{:unix, :darwin} -> choose_files_macos(prompt, opts)
|
||||||
|
_other -> {:error, %{message: "File selection is only supported on macOS desktop"}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp choose_file_macos(prompt) do
|
defp choose_file_macos(prompt) do
|
||||||
script = "POSIX path of (choose file with prompt \"#{escape_applescript(prompt)}\")"
|
script = "POSIX path of (choose file with prompt \"#{escape_applescript(prompt)}\")"
|
||||||
|
|
||||||
@@ -21,6 +32,50 @@ defmodule BDS.Desktop.FilePicker do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp choose_files_macos(prompt, opts) do
|
||||||
|
multiple = Keyword.get(opts, :multiple, false)
|
||||||
|
image_only = Keyword.get(opts, :image_only, false)
|
||||||
|
|
||||||
|
script_parts = ["POSIX path of (choose file with prompt \"#{escape_applescript(prompt)}\""]
|
||||||
|
|
||||||
|
script_parts =
|
||||||
|
if image_only do
|
||||||
|
script_parts ++ [" of type {\"public.image\"}"]
|
||||||
|
else
|
||||||
|
script_parts
|
||||||
|
end
|
||||||
|
|
||||||
|
script_parts =
|
||||||
|
if multiple do
|
||||||
|
script_parts ++ [" with multiple selections allowed"]
|
||||||
|
else
|
||||||
|
script_parts
|
||||||
|
end
|
||||||
|
|
||||||
|
script = Enum.join(script_parts, "") <> ")"
|
||||||
|
|
||||||
|
case System.cmd("osascript", ["-e", script], stderr_to_stdout: true) do
|
||||||
|
{output, 0} -> parse_choose_files_result(String.trim(output), multiple)
|
||||||
|
{output, _status} -> normalize_picker_failure(output)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
def parse_choose_files_result(output, true = _multiple) do
|
||||||
|
paths =
|
||||||
|
output
|
||||||
|
|> String.split("\n")
|
||||||
|
|> Enum.map(&String.trim/1)
|
||||||
|
|> Enum.reject(&(&1 == ""))
|
||||||
|
|
||||||
|
{:ok, paths}
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
def parse_choose_files_result(output, false = _multiple) do
|
||||||
|
{:ok, output}
|
||||||
|
end
|
||||||
|
|
||||||
defp normalize_picker_failure(output) do
|
defp normalize_picker_failure(output) do
|
||||||
message = String.trim(output)
|
message = String.trim(output)
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ defmodule BDS.Desktop.Overlay do
|
|||||||
title: Map.get(context, :insert_media_title, "Insert Media"),
|
title: Map.get(context, :insert_media_title, "Insert Media"),
|
||||||
search_query: "",
|
search_query: "",
|
||||||
results: Enum.map(media, &to_insert_media_result/1),
|
results: Enum.map(media, &to_insert_media_result/1),
|
||||||
all_media: media
|
all_media: media,
|
||||||
|
post_id: current_id(context)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,14 @@ defmodule BDS.Desktop.ShellLive do
|
|||||||
|
|
||||||
import Phoenix.HTML
|
import Phoenix.HTML
|
||||||
|
|
||||||
alias BDS.{AI, BoundedAtoms}
|
alias BDS.{AI, BoundedAtoms, Metadata}
|
||||||
alias BDS.CliSync.Watcher
|
alias BDS.CliSync.Watcher
|
||||||
alias BDS.Desktop.{ExternalLinks, FolderPicker, ShellData, UILocale}
|
alias BDS.Desktop.{ExternalLinks, FilePicker, FolderPicker, ShellData, UILocale}
|
||||||
|
|
||||||
alias BDS.Desktop.ShellLive.{
|
alias BDS.Desktop.ShellLive.{
|
||||||
Bridges,
|
Bridges,
|
||||||
ChatEditor,
|
ChatEditor,
|
||||||
|
GalleryImport,
|
||||||
ImportEditor,
|
ImportEditor,
|
||||||
MediaEditor,
|
MediaEditor,
|
||||||
MenuEditor,
|
MenuEditor,
|
||||||
@@ -399,6 +400,41 @@ defmodule BDS.Desktop.ShellLive do
|
|||||||
def handle_event("overlay_lightbox_next", params, socket),
|
def handle_event("overlay_lightbox_next", params, socket),
|
||||||
do: OverlayManager.handle_event("overlay_lightbox_next", params, socket, overlay_callbacks())
|
do: OverlayManager.handle_event("overlay_lightbox_next", params, socket, overlay_callbacks())
|
||||||
|
|
||||||
|
def handle_event("add_gallery_images", %{"post-id" => post_id}, socket) do
|
||||||
|
if socket.assigns.offline_mode do
|
||||||
|
{:noreply,
|
||||||
|
append_output_entry(
|
||||||
|
socket,
|
||||||
|
dgettext("ui", "Add Gallery Images"),
|
||||||
|
dgettext("ui", "Automatic AI actions stay gated by airplane mode."),
|
||||||
|
nil,
|
||||||
|
"info"
|
||||||
|
)}
|
||||||
|
else
|
||||||
|
project_id = socket.assigns.projects.active_project_id
|
||||||
|
{:ok, metadata} = Metadata.get_project_metadata(project_id)
|
||||||
|
concurrency_limit = metadata.image_import_concurrency
|
||||||
|
language = metadata.main_language || "en"
|
||||||
|
parent = self()
|
||||||
|
|
||||||
|
Task.Supervisor.start_child(BDS.TCP.TaskSupervisor, fn ->
|
||||||
|
case FilePicker.choose_files(dgettext("ui", "Add Gallery Images"),
|
||||||
|
image_only: true, multiple: true) do
|
||||||
|
{:ok, paths} when is_list(paths) and paths != [] ->
|
||||||
|
GalleryImport.start(paths, project_id, post_id, language, concurrency_limit, parent)
|
||||||
|
|
||||||
|
:cancel ->
|
||||||
|
send(parent, {:add_images_cancelled})
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
send(parent, {:add_images_error, reason})
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
{:noreply, assign(socket, :gallery_import_post_id, post_id)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def handle_event("toggle_project_menu", _params, socket) do
|
def handle_event("toggle_project_menu", _params, socket) do
|
||||||
{:noreply, assign(socket, :project_menu_open, not socket.assigns.project_menu_open)}
|
{:noreply, assign(socket, :project_menu_open, not socket.assigns.project_menu_open)}
|
||||||
end
|
end
|
||||||
@@ -580,6 +616,68 @@ defmodule BDS.Desktop.ShellLive do
|
|||||||
OverlayManager.handle_info({:ai_suggestions_error, type, id, reason}, socket)
|
OverlayManager.handle_info({:ai_suggestions_error, type, id, reason}, socket)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_info({:add_image_processed, title}, socket) do
|
||||||
|
{:noreply,
|
||||||
|
append_output_entry(socket, dgettext("ui", "Add Gallery Images"), dgettext("ui", "Added %{title}", title: title), nil, "info")}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info({:add_images_complete, count}, socket) do
|
||||||
|
post_id = socket.assigns[:gallery_import_post_id]
|
||||||
|
|
||||||
|
socket =
|
||||||
|
if is_binary(post_id) do
|
||||||
|
send_update(PostEditor,
|
||||||
|
id: "post-editor-#{post_id}",
|
||||||
|
action: :insert_content,
|
||||||
|
content: "\n[[gallery]]\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
send_update(PostEditor,
|
||||||
|
id: "post-editor-#{post_id}",
|
||||||
|
action: :refresh
|
||||||
|
)
|
||||||
|
|
||||||
|
socket
|
||||||
|
|> assign(:gallery_import_post_id, nil)
|
||||||
|
else
|
||||||
|
socket
|
||||||
|
end
|
||||||
|
|
||||||
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> append_output_entry(
|
||||||
|
dgettext("ui", "Add Gallery Images"),
|
||||||
|
dgettext("ui", "Added %{count} images to post", count: count),
|
||||||
|
nil,
|
||||||
|
"info"
|
||||||
|
)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info({:add_images_error, reason}, socket) do
|
||||||
|
{:noreply,
|
||||||
|
append_output_entry(socket, dgettext("ui", "Add Gallery Images"), inspect(reason), nil, "error")}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info({:add_image_error, path, reason}, socket) do
|
||||||
|
{:noreply,
|
||||||
|
append_output_entry(
|
||||||
|
socket,
|
||||||
|
dgettext("ui", "Add Gallery Images"),
|
||||||
|
dgettext("ui", "Failed to process %{path}: %{reason}", path: Path.basename(path), reason: inspect(reason)),
|
||||||
|
nil,
|
||||||
|
"error"
|
||||||
|
)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info({:add_images_cancelled}, socket) do
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info({:test_ping, caller, ref}, socket) do
|
||||||
|
send(caller, {:test_pong, ref})
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
|
||||||
def handle_info(message, socket) do
|
def handle_info(message, socket) do
|
||||||
Bridges.handle_info(message, socket, bridges_callbacks())
|
Bridges.handle_info(message, socket, bridges_callbacks())
|
||||||
end
|
end
|
||||||
|
|||||||
172
lib/bds/desktop/shell_live/gallery_import.ex
Normal file
172
lib/bds/desktop/shell_live/gallery_import.ex
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
defmodule BDS.Desktop.ShellLive.GalleryImport do
|
||||||
|
@moduledoc false
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
alias BDS.{AI, Media, Metadata}
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Starts the image import pipeline: for each selected path, imports the file,
|
||||||
|
runs AI analysis, updates metadata, links to the post, and translates to
|
||||||
|
all configured blog languages.
|
||||||
|
|
||||||
|
Processes images with a concurrency cap via a sliding window.
|
||||||
|
"""
|
||||||
|
@spec start(list(String.t()), String.t(), String.t(), String.t(), integer(), pid()) :: :ok
|
||||||
|
def start(paths, project_id, post_id, language, concurrency_limit, parent) do
|
||||||
|
{:ok, metadata} = Metadata.get_project_metadata(project_id)
|
||||||
|
main_language = metadata.main_language || language
|
||||||
|
blog_languages = metadata.blog_languages || []
|
||||||
|
|
||||||
|
translate_targets =
|
||||||
|
[main_language | blog_languages]
|
||||||
|
|> Enum.reject(&(&1 == language or is_nil(&1)))
|
||||||
|
|> Enum.uniq()
|
||||||
|
|
||||||
|
{in_flight, remaining} = Enum.split(paths, concurrency_limit)
|
||||||
|
|
||||||
|
tasks =
|
||||||
|
Enum.map(in_flight, fn path ->
|
||||||
|
Task.async(fn ->
|
||||||
|
process_single_image(path, project_id, post_id, language, translate_targets, parent)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
known_refs = MapSet.new(tasks, & &1.ref)
|
||||||
|
|
||||||
|
drain_tasks(
|
||||||
|
remaining, tasks, known_refs, project_id, post_id, language, translate_targets, parent
|
||||||
|
)
|
||||||
|
|
||||||
|
send(parent, {:add_images_complete, length(paths)})
|
||||||
|
end
|
||||||
|
|
||||||
|
defp drain_tasks(
|
||||||
|
[], tasks, _known_refs, _project_id, _post_id, _language, _translate_targets, _parent
|
||||||
|
) do
|
||||||
|
Enum.each(tasks, fn task -> Task.await(task, :infinity) end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp drain_tasks(
|
||||||
|
[next_path | rest],
|
||||||
|
tasks,
|
||||||
|
known_refs,
|
||||||
|
project_id,
|
||||||
|
post_id,
|
||||||
|
language,
|
||||||
|
translate_targets,
|
||||||
|
parent
|
||||||
|
) do
|
||||||
|
receive do
|
||||||
|
{ref, _result} when is_reference(ref) ->
|
||||||
|
if MapSet.member?(known_refs, ref) do
|
||||||
|
{_completed_task, remaining_tasks} = pop_task_by_ref(tasks, ref)
|
||||||
|
|
||||||
|
new_task =
|
||||||
|
Task.async(fn ->
|
||||||
|
process_single_image(
|
||||||
|
next_path, project_id, post_id, language, translate_targets, parent
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
|
||||||
|
drain_tasks(
|
||||||
|
rest,
|
||||||
|
[new_task | remaining_tasks],
|
||||||
|
MapSet.put(MapSet.delete(known_refs, ref), new_task.ref),
|
||||||
|
project_id,
|
||||||
|
post_id,
|
||||||
|
language,
|
||||||
|
translate_targets,
|
||||||
|
parent
|
||||||
|
)
|
||||||
|
else
|
||||||
|
drain_tasks(
|
||||||
|
[next_path | rest], tasks, known_refs,
|
||||||
|
project_id, post_id, language, translate_targets, parent
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
{:DOWN, ref, :process, _pid, _reason} when is_reference(ref) ->
|
||||||
|
if MapSet.member?(known_refs, ref) do
|
||||||
|
{_completed_task, remaining_tasks} = pop_task_by_ref(tasks, ref)
|
||||||
|
|
||||||
|
new_task =
|
||||||
|
Task.async(fn ->
|
||||||
|
process_single_image(
|
||||||
|
next_path, project_id, post_id, language, translate_targets, parent
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
|
||||||
|
drain_tasks(
|
||||||
|
rest,
|
||||||
|
[new_task | remaining_tasks],
|
||||||
|
MapSet.put(MapSet.delete(known_refs, ref), new_task.ref),
|
||||||
|
project_id,
|
||||||
|
post_id,
|
||||||
|
language,
|
||||||
|
translate_targets,
|
||||||
|
parent
|
||||||
|
)
|
||||||
|
else
|
||||||
|
drain_tasks(
|
||||||
|
[next_path | rest], tasks, known_refs,
|
||||||
|
project_id, post_id, language, translate_targets, parent
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp pop_task_by_ref(tasks, ref) do
|
||||||
|
Enum.reduce(tasks, {nil, []}, fn
|
||||||
|
%{ref: ^ref} = task, {nil, rest} -> {task, rest}
|
||||||
|
task, {found, rest} -> {found, [task | rest]}
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp process_single_image(
|
||||||
|
path, project_id, post_id, language, translate_targets, parent
|
||||||
|
) do
|
||||||
|
with {:ok, media} <- Media.import_media(%{project_id: project_id, source_path: path}),
|
||||||
|
true <- String.starts_with?(media.mime_type || "", "image/"),
|
||||||
|
{:ok, result} <- AI.analyze_image(media.id, language: language),
|
||||||
|
{:ok, _updated} <- Media.update_media(media.id, %{
|
||||||
|
title: result.title,
|
||||||
|
alt: result.alt,
|
||||||
|
caption: result.caption
|
||||||
|
}),
|
||||||
|
{:ok, _link} <- Media.link_media_to_post(media.id, post_id) do
|
||||||
|
translate_media_translations(media.id, translate_targets)
|
||||||
|
title = result.title || media.original_name
|
||||||
|
send(parent, {:add_image_processed, title})
|
||||||
|
else
|
||||||
|
false ->
|
||||||
|
:ok
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
Logger.warning("Image pipeline error for #{path}: #{inspect(reason)}")
|
||||||
|
send(parent, {:add_image_error, path, reason})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp translate_media_translations(_media_id, []), do: :ok
|
||||||
|
|
||||||
|
defp translate_media_translations(media_id, [target | rest]) do
|
||||||
|
case AI.translate_media(media_id, target) do
|
||||||
|
{:ok, translation} ->
|
||||||
|
Media.upsert_media_translation(media_id, target, %{
|
||||||
|
title: translation.title,
|
||||||
|
alt: translation.alt,
|
||||||
|
caption: translation.caption
|
||||||
|
})
|
||||||
|
|
||||||
|
translate_media_translations(media_id, rest)
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
Logger.warning(
|
||||||
|
"Media translation failed for #{media_id} -> #{target}: #{inspect(reason)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
translate_media_translations(media_id, rest)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -168,11 +168,19 @@ defmodule BDS.Desktop.ShellLive.PostEditor.PostMetadata do
|
|||||||
|
|
||||||
@spec gallery_count(term()) :: term()
|
@spec gallery_count(term()) :: term()
|
||||||
def gallery_count(form) do
|
def gallery_count(form) do
|
||||||
form
|
content = form |> Map.get("content", "") |> to_string()
|
||||||
|> Map.get("content", "")
|
|
||||||
|> to_string()
|
image_count =
|
||||||
|
content
|
||||||
|> then(&Regex.scan(~r/!\[[^\]]*\]\([^\)]+\)/, &1))
|
|> then(&Regex.scan(~r/!\[[^\]]*\]\([^\)]+\)/, &1))
|
||||||
|> length()
|
|> length()
|
||||||
|
|
||||||
|
gallery_macro_count =
|
||||||
|
content
|
||||||
|
|> then(&Regex.scan(~r/\[\[gallery\]\]/i, &1))
|
||||||
|
|> length()
|
||||||
|
|
||||||
|
max(image_count, gallery_macro_count)
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec preview_url(term(), term(), term(), term()) :: term()
|
@spec preview_url(term(), term(), term(), term()) :: term()
|
||||||
|
|||||||
@@ -362,6 +362,14 @@
|
|||||||
>
|
>
|
||||||
<%= dgettext("ui", "Insert Media") %>
|
<%= dgettext("ui", "Insert Media") %>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
class="add-gallery-images-button"
|
||||||
|
type="button"
|
||||||
|
phx-click="add_gallery_images"
|
||||||
|
phx-value-post-id={@post_editor.id}
|
||||||
|
>
|
||||||
|
<%= dgettext("ui", "Add Gallery Images") %>
|
||||||
|
</button>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= if @post_editor.gallery_count > 0 do %>
|
<%= if @post_editor.gallery_count > 0 do %>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.ProjectSettings do
|
|||||||
"main_language" => metadata.main_language || "en",
|
"main_language" => metadata.main_language || "en",
|
||||||
"default_author" => metadata.default_author || "",
|
"default_author" => metadata.default_author || "",
|
||||||
"max_posts_per_page" => Integer.to_string(metadata.max_posts_per_page),
|
"max_posts_per_page" => Integer.to_string(metadata.max_posts_per_page),
|
||||||
|
"image_import_concurrency" => Integer.to_string(metadata.image_import_concurrency),
|
||||||
"blogmark_category" =>
|
"blogmark_category" =>
|
||||||
metadata.blogmark_category ||
|
metadata.blogmark_category ||
|
||||||
List.first(metadata.categories) || "article",
|
List.first(metadata.categories) || "article",
|
||||||
@@ -71,6 +72,7 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.ProjectSettings do
|
|||||||
main_language: blank_to_nil(Map.get(draft, "main_language")),
|
main_language: blank_to_nil(Map.get(draft, "main_language")),
|
||||||
default_author: blank_to_nil(Map.get(draft, "default_author")),
|
default_author: blank_to_nil(Map.get(draft, "default_author")),
|
||||||
max_posts_per_page: parse_integer(Map.get(draft, "max_posts_per_page"), 50),
|
max_posts_per_page: parse_integer(Map.get(draft, "max_posts_per_page"), 50),
|
||||||
|
image_import_concurrency: parse_integer(Map.get(draft, "image_import_concurrency"), 4),
|
||||||
blogmark_category: blank_to_nil(Map.get(draft, "blogmark_category")),
|
blogmark_category: blank_to_nil(Map.get(draft, "blogmark_category")),
|
||||||
blog_languages: Map.get(draft, "blog_languages", []),
|
blog_languages: Map.get(draft, "blog_languages", []),
|
||||||
semantic_similarity_enabled: truthy?(Map.get(draft, "semantic_similarity_enabled"))
|
semantic_similarity_enabled: truthy?(Map.get(draft, "semantic_similarity_enabled"))
|
||||||
@@ -85,6 +87,7 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.ProjectSettings do
|
|||||||
"main_language" => Map.get(params, "main_language", "en"),
|
"main_language" => Map.get(params, "main_language", "en"),
|
||||||
"default_author" => Map.get(params, "default_author", ""),
|
"default_author" => Map.get(params, "default_author", ""),
|
||||||
"max_posts_per_page" => Map.get(params, "max_posts_per_page", "50"),
|
"max_posts_per_page" => Map.get(params, "max_posts_per_page", "50"),
|
||||||
|
"image_import_concurrency" => Map.get(params, "image_import_concurrency", "4"),
|
||||||
"blogmark_category" => Map.get(params, "blogmark_category", "article"),
|
"blogmark_category" => Map.get(params, "blogmark_category", "article"),
|
||||||
"blog_languages" => List.wrap(Map.get(params, "blog_languages", [])),
|
"blog_languages" => List.wrap(Map.get(params, "blog_languages", [])),
|
||||||
"semantic_similarity_enabled" => truthy?(Map.get(params, "semantic_similarity_enabled"))
|
"semantic_similarity_enabled" => truthy?(Map.get(params, "semantic_similarity_enabled"))
|
||||||
|
|||||||
@@ -82,6 +82,10 @@
|
|||||||
<div class="setting-info"><label class="setting-label"><%= dgettext("ui", "Max Posts Per Page") %></label></div>
|
<div class="setting-info"><label class="setting-label"><%= dgettext("ui", "Max Posts Per Page") %></label></div>
|
||||||
<div class="setting-control"><input class="ui-input" type="number" min="1" max="500" name="settings_project[max_posts_per_page]" value={@settings_editor.project["max_posts_per_page"]} /></div>
|
<div class="setting-control"><input class="ui-input" type="number" min="1" max="500" name="settings_project[max_posts_per_page]" value={@settings_editor.project["max_posts_per_page"]} /></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="setting-row">
|
||||||
|
<div class="setting-info"><label class="setting-label"><%= dgettext("ui", "Image Import Concurrency") %></label></div>
|
||||||
|
<div class="setting-control"><input class="ui-input" type="number" min="1" max="8" name="settings_project[image_import_concurrency]" value={@settings_editor.project["image_import_concurrency"]} /></div>
|
||||||
|
</div>
|
||||||
<div class="setting-row">
|
<div class="setting-row">
|
||||||
<div class="setting-info"><label class="setting-label"><%= dgettext("ui", "Blogmark Category") %></label></div>
|
<div class="setting-info"><label class="setting-label"><%= dgettext("ui", "Blogmark Category") %></label></div>
|
||||||
<div class="setting-control">
|
<div class="setting-control">
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ defmodule BDS.Metadata do
|
|||||||
@default_categories ["article", "aside", "page", "picture"]
|
@default_categories ["article", "aside", "page", "picture"]
|
||||||
@min_posts_per_page 1
|
@min_posts_per_page 1
|
||||||
@max_posts_per_page 500
|
@max_posts_per_page 500
|
||||||
|
@default_image_import_concurrency 4
|
||||||
|
@min_image_import_concurrency 1
|
||||||
|
@max_image_import_concurrency 8
|
||||||
@supported_pico_themes MapSet.new([
|
@supported_pico_themes MapSet.new([
|
||||||
"default",
|
"default",
|
||||||
"amber",
|
"amber",
|
||||||
@@ -70,6 +73,7 @@ defmodule BDS.Metadata do
|
|||||||
:main_language,
|
:main_language,
|
||||||
:default_author,
|
:default_author,
|
||||||
:max_posts_per_page,
|
:max_posts_per_page,
|
||||||
|
:image_import_concurrency,
|
||||||
:blogmark_category,
|
:blogmark_category,
|
||||||
:pico_theme,
|
:pico_theme,
|
||||||
:semantic_similarity_enabled,
|
:semantic_similarity_enabled,
|
||||||
@@ -238,6 +242,8 @@ defmodule BDS.Metadata do
|
|||||||
default_author: Map.get(project_metadata, "default_author"),
|
default_author: Map.get(project_metadata, "default_author"),
|
||||||
max_posts_per_page:
|
max_posts_per_page:
|
||||||
Map.get(project_metadata, "max_posts_per_page", @default_max_posts_per_page),
|
Map.get(project_metadata, "max_posts_per_page", @default_max_posts_per_page),
|
||||||
|
image_import_concurrency:
|
||||||
|
Map.get(project_metadata, "image_import_concurrency", @default_image_import_concurrency),
|
||||||
blogmark_category: Map.get(project_metadata, "blogmark_category"),
|
blogmark_category: Map.get(project_metadata, "blogmark_category"),
|
||||||
pico_theme: Map.get(project_metadata, "pico_theme"),
|
pico_theme: Map.get(project_metadata, "pico_theme"),
|
||||||
semantic_similarity_enabled:
|
semantic_similarity_enabled:
|
||||||
@@ -274,6 +280,8 @@ defmodule BDS.Metadata do
|
|||||||
default_author: Map.get(project_metadata, "default_author"),
|
default_author: Map.get(project_metadata, "default_author"),
|
||||||
max_posts_per_page:
|
max_posts_per_page:
|
||||||
Map.get(project_metadata, "max_posts_per_page", @default_max_posts_per_page),
|
Map.get(project_metadata, "max_posts_per_page", @default_max_posts_per_page),
|
||||||
|
image_import_concurrency:
|
||||||
|
Map.get(project_metadata, "image_import_concurrency", @default_image_import_concurrency),
|
||||||
blogmark_category: Map.get(project_metadata, "blogmark_category"),
|
blogmark_category: Map.get(project_metadata, "blogmark_category"),
|
||||||
pico_theme: Map.get(project_metadata, "pico_theme"),
|
pico_theme: Map.get(project_metadata, "pico_theme"),
|
||||||
semantic_similarity_enabled:
|
semantic_similarity_enabled:
|
||||||
@@ -293,6 +301,7 @@ defmodule BDS.Metadata do
|
|||||||
main_language: nil,
|
main_language: nil,
|
||||||
default_author: nil,
|
default_author: nil,
|
||||||
max_posts_per_page: @default_max_posts_per_page,
|
max_posts_per_page: @default_max_posts_per_page,
|
||||||
|
image_import_concurrency: @default_image_import_concurrency,
|
||||||
blogmark_category: nil,
|
blogmark_category: nil,
|
||||||
pico_theme: nil,
|
pico_theme: nil,
|
||||||
semantic_similarity_enabled: false,
|
semantic_similarity_enabled: false,
|
||||||
@@ -308,6 +317,8 @@ defmodule BDS.Metadata do
|
|||||||
main_language: normalize_optional_language(attr(attrs, :main_language)),
|
main_language: normalize_optional_language(attr(attrs, :main_language)),
|
||||||
default_author: attr(attrs, :default_author),
|
default_author: attr(attrs, :default_author),
|
||||||
max_posts_per_page: normalize_posts_per_page(attr(attrs, :max_posts_per_page)),
|
max_posts_per_page: normalize_posts_per_page(attr(attrs, :max_posts_per_page)),
|
||||||
|
image_import_concurrency:
|
||||||
|
normalize_image_import_concurrency(attr(attrs, :image_import_concurrency)),
|
||||||
blogmark_category: attr(attrs, :blogmark_category),
|
blogmark_category: attr(attrs, :blogmark_category),
|
||||||
pico_theme: normalize_pico_theme(attr(attrs, :pico_theme)),
|
pico_theme: normalize_pico_theme(attr(attrs, :pico_theme)),
|
||||||
semantic_similarity_enabled: attr(attrs, :semantic_similarity_enabled) || false,
|
semantic_similarity_enabled: attr(attrs, :semantic_similarity_enabled) || false,
|
||||||
@@ -342,6 +353,7 @@ defmodule BDS.Metadata do
|
|||||||
"main_language" => project_metadata.main_language,
|
"main_language" => project_metadata.main_language,
|
||||||
"default_author" => project_metadata.default_author,
|
"default_author" => project_metadata.default_author,
|
||||||
"max_posts_per_page" => project_metadata.max_posts_per_page,
|
"max_posts_per_page" => project_metadata.max_posts_per_page,
|
||||||
|
"image_import_concurrency" => project_metadata.image_import_concurrency,
|
||||||
"blogmark_category" => project_metadata.blogmark_category,
|
"blogmark_category" => project_metadata.blogmark_category,
|
||||||
"pico_theme" => project_metadata.pico_theme,
|
"pico_theme" => project_metadata.pico_theme,
|
||||||
"semantic_similarity_enabled" => project_metadata.semantic_similarity_enabled,
|
"semantic_similarity_enabled" => project_metadata.semantic_similarity_enabled,
|
||||||
@@ -429,6 +441,8 @@ defmodule BDS.Metadata do
|
|||||||
"main_language" => Map.get(payload, "mainLanguage"),
|
"main_language" => Map.get(payload, "mainLanguage"),
|
||||||
"default_author" => Map.get(payload, "defaultAuthor"),
|
"default_author" => Map.get(payload, "defaultAuthor"),
|
||||||
"max_posts_per_page" => Map.get(payload, "maxPostsPerPage", @default_max_posts_per_page),
|
"max_posts_per_page" => Map.get(payload, "maxPostsPerPage", @default_max_posts_per_page),
|
||||||
|
"image_import_concurrency" =>
|
||||||
|
Map.get(payload, "imageImportConcurrency", @default_image_import_concurrency),
|
||||||
"blogmark_category" => Map.get(payload, "blogmarkCategory"),
|
"blogmark_category" => Map.get(payload, "blogmarkCategory"),
|
||||||
"pico_theme" => Map.get(payload, "picoTheme"),
|
"pico_theme" => Map.get(payload, "picoTheme"),
|
||||||
"semantic_similarity_enabled" => Map.get(payload, "semanticSimilarityEnabled", false),
|
"semantic_similarity_enabled" => Map.get(payload, "semanticSimilarityEnabled", false),
|
||||||
@@ -505,6 +519,8 @@ defmodule BDS.Metadata do
|
|||||||
"defaultAuthor" => Map.get(project_metadata, "default_author"),
|
"defaultAuthor" => Map.get(project_metadata, "default_author"),
|
||||||
"maxPostsPerPage" =>
|
"maxPostsPerPage" =>
|
||||||
Map.get(project_metadata, "max_posts_per_page", @default_max_posts_per_page),
|
Map.get(project_metadata, "max_posts_per_page", @default_max_posts_per_page),
|
||||||
|
"imageImportConcurrency" =>
|
||||||
|
Map.get(project_metadata, "image_import_concurrency", @default_image_import_concurrency),
|
||||||
"blogmarkCategory" => Map.get(project_metadata, "blogmark_category"),
|
"blogmarkCategory" => Map.get(project_metadata, "blogmark_category"),
|
||||||
"picoTheme" => Map.get(project_metadata, "pico_theme"),
|
"picoTheme" => Map.get(project_metadata, "pico_theme"),
|
||||||
"semanticSimilarityEnabled" =>
|
"semanticSimilarityEnabled" =>
|
||||||
@@ -576,6 +592,23 @@ defmodule BDS.Metadata do
|
|||||||
|
|
||||||
defp normalize_posts_per_page(_value), do: @default_max_posts_per_page
|
defp normalize_posts_per_page(_value), do: @default_max_posts_per_page
|
||||||
|
|
||||||
|
defp normalize_image_import_concurrency(nil), do: @default_image_import_concurrency
|
||||||
|
|
||||||
|
defp normalize_image_import_concurrency(value) when is_integer(value) do
|
||||||
|
value
|
||||||
|
|> max(@min_image_import_concurrency)
|
||||||
|
|> min(@max_image_import_concurrency)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp normalize_image_import_concurrency(value) when is_binary(value) do
|
||||||
|
case Integer.parse(String.trim(value)) do
|
||||||
|
{integer, ""} -> normalize_image_import_concurrency(integer)
|
||||||
|
_ -> @default_image_import_concurrency
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp normalize_image_import_concurrency(_value), do: @default_image_import_concurrency
|
||||||
|
|
||||||
defp normalize_optional_language(nil), do: nil
|
defp normalize_optional_language(nil), do: nil
|
||||||
defp normalize_optional_language(""), do: nil
|
defp normalize_optional_language(""), do: nil
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,14 @@ defmodule BDS.Rendering.Filters do
|
|||||||
|
|
||||||
use Liquex.Filter
|
use Liquex.Filter
|
||||||
|
|
||||||
|
import Ecto.Query
|
||||||
|
|
||||||
alias BDS.Slug
|
alias BDS.Slug
|
||||||
|
alias BDS.{Repo}
|
||||||
|
alias BDS.Media.Media, as: MediaRecord
|
||||||
|
alias BDS.Posts.{Post, PostMedia}
|
||||||
|
alias BDS.Tags.Tag
|
||||||
|
require Logger
|
||||||
|
|
||||||
@spec i18n(term(), String.t(), Liquex.Context.t()) :: String.t()
|
@spec i18n(term(), String.t(), Liquex.Context.t()) :: String.t()
|
||||||
def i18n(value, language, _context) do
|
def i18n(value, language, _context) do
|
||||||
@@ -28,7 +35,7 @@ defmodule BDS.Rendering.Filters do
|
|||||||
) :: String.t()
|
) :: String.t()
|
||||||
def markdown(
|
def markdown(
|
||||||
value,
|
value,
|
||||||
_post_id,
|
post_id,
|
||||||
_post_data_json_by_id,
|
_post_data_json_by_id,
|
||||||
canonical_post_paths,
|
canonical_post_paths,
|
||||||
canonical_media_paths,
|
canonical_media_paths,
|
||||||
@@ -36,15 +43,15 @@ defmodule BDS.Rendering.Filters do
|
|||||||
_language_prefix,
|
_language_prefix,
|
||||||
context
|
context
|
||||||
) do
|
) do
|
||||||
render_markdown(value, canonical_post_paths, canonical_media_paths, language, context)
|
render_markdown(value, canonical_post_paths, canonical_media_paths, language, context, post_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec render_markdown(term(), map() | nil, map() | nil, String.t(), Liquex.Context.t()) ::
|
@spec render_markdown(term(), map() | nil, map() | nil, String.t(), Liquex.Context.t(), term()) ::
|
||||||
String.t()
|
String.t()
|
||||||
def render_markdown(value, canonical_post_paths, canonical_media_paths, language, context) do
|
def render_markdown(value, canonical_post_paths, canonical_media_paths, language, context, post_id \\ nil) do
|
||||||
value
|
value
|
||||||
|> to_string()
|
|> to_string()
|
||||||
|> replace_built_in_macros(language, context)
|
|> replace_built_in_macros(language, context, post_id)
|
||||||
|> render_markdown_html()
|
|> render_markdown_html()
|
||||||
|> rewrite_rendered_html_urls(canonical_post_paths || %{}, canonical_media_paths || %{})
|
|> rewrite_rendered_html_urls(canonical_post_paths || %{}, canonical_media_paths || %{})
|
||||||
end
|
end
|
||||||
@@ -56,7 +63,7 @@ defmodule BDS.Rendering.Filters do
|
|||||||
|> Slug.slugify()
|
|> Slug.slugify()
|
||||||
end
|
end
|
||||||
|
|
||||||
defp replace_built_in_macros(content, language, context) do
|
defp replace_built_in_macros(content, language, context, post_id) do
|
||||||
Regex.replace(~r/\[\[(\w+)(?:\s+([^\]]+))?\]\]/, content, fn full_match,
|
Regex.replace(~r/\[\[(\w+)(?:\s+([^\]]+))?\]\]/, content, fn full_match,
|
||||||
macro_name,
|
macro_name,
|
||||||
raw_params ->
|
raw_params ->
|
||||||
@@ -88,6 +95,15 @@ defmodule BDS.Rendering.Filters do
|
|||||||
context
|
context
|
||||||
)
|
)
|
||||||
|
|
||||||
|
"gallery" ->
|
||||||
|
render_gallery_macro(context, params, post_id)
|
||||||
|
|
||||||
|
"photo_archive" ->
|
||||||
|
render_photo_archive_macro(context, params)
|
||||||
|
|
||||||
|
"tag_cloud" ->
|
||||||
|
render_tag_cloud_macro(context, params)
|
||||||
|
|
||||||
_other ->
|
_other ->
|
||||||
full_match
|
full_match
|
||||||
end
|
end
|
||||||
@@ -127,14 +143,6 @@ defmodule BDS.Rendering.Filters do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp render_macro_template(template_path, assigns, context) do
|
defp render_macro_template(template_path, assigns, context) do
|
||||||
case Map.get(assigns, "id") do
|
|
||||||
"" ->
|
|
||||||
""
|
|
||||||
|
|
||||||
nil ->
|
|
||||||
""
|
|
||||||
|
|
||||||
_id ->
|
|
||||||
case BDS.Rendering.FileSystem.try_read(context.file_system, template_path) do
|
case BDS.Rendering.FileSystem.try_read(context.file_system, template_path) do
|
||||||
{:ok, template_source} ->
|
{:ok, template_source} ->
|
||||||
render_macro_source(template_path, template_source, assigns, context)
|
render_macro_source(template_path, template_source, assigns, context)
|
||||||
@@ -145,7 +153,6 @@ defmodule BDS.Rendering.Filters do
|
|||||||
""
|
""
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
defp render_macro_source(template_path, template_source, assigns, context) do
|
defp render_macro_source(template_path, template_source, assigns, context) do
|
||||||
with {:ok, template_ast} <- Liquex.parse(template_source),
|
with {:ok, template_ast} <- Liquex.parse(template_source),
|
||||||
@@ -285,4 +292,307 @@ defmodule BDS.Rendering.Filters do
|
|||||||
|
|
||||||
defp ensure_leading_slash("/" <> _rest = path), do: path
|
defp ensure_leading_slash("/" <> _rest = path), do: path
|
||||||
defp ensure_leading_slash(path), do: "/" <> path
|
defp ensure_leading_slash(path), do: "/" <> path
|
||||||
|
|
||||||
|
# ── Built-in macro renderers ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
defp render_gallery_macro(context, params, post_id) when is_binary(post_id) do
|
||||||
|
columns = normalize_columns(Map.get(params, "columns", "3"), 3, 1, 6)
|
||||||
|
caption = Map.get(params, "caption")
|
||||||
|
|
||||||
|
items =
|
||||||
|
post_id
|
||||||
|
|> linked_media_images()
|
||||||
|
|> Enum.map(fn media ->
|
||||||
|
%{
|
||||||
|
"media_path" => "/#{media.file_path}",
|
||||||
|
"title" => media.title || media.original_name,
|
||||||
|
"alt" => media.alt || media.title || media.original_name,
|
||||||
|
"group_name" => post_id
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
|
||||||
|
render_macro_template(
|
||||||
|
"macros/gallery",
|
||||||
|
%{
|
||||||
|
"columns" => columns,
|
||||||
|
"post_id" => post_id,
|
||||||
|
"items" => items,
|
||||||
|
"caption" => caption,
|
||||||
|
"empty_label" =>
|
||||||
|
BDS.Gettext.lgettext(
|
||||||
|
Access.get(context, "language") || "en",
|
||||||
|
"render",
|
||||||
|
"No images"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
context
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp render_gallery_macro(context, params, _post_id) do
|
||||||
|
render_macro_template(
|
||||||
|
"macros/gallery",
|
||||||
|
%{
|
||||||
|
"columns" => normalize_columns(Map.get(params, "columns", "3"), 3, 1, 6),
|
||||||
|
"post_id" => "",
|
||||||
|
"items" => [],
|
||||||
|
"caption" => Map.get(params, "caption"),
|
||||||
|
"empty_label" =>
|
||||||
|
BDS.Gettext.lgettext(
|
||||||
|
Access.get(context, "language") || "en",
|
||||||
|
"render",
|
||||||
|
"No images"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
context
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp render_photo_archive_macro(context, params) do
|
||||||
|
language = Access.get(context, "language") || "en"
|
||||||
|
project_id = project_id_from_context(context)
|
||||||
|
|
||||||
|
months =
|
||||||
|
if project_id do
|
||||||
|
media_month_archive(project_id, Map.get(params, "year"), Map.get(params, "month"))
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
|
||||||
|
render_macro_template(
|
||||||
|
"macros/photo-archive",
|
||||||
|
%{
|
||||||
|
"root_classes" => "macro-photo-archive",
|
||||||
|
"data_attrs" => [],
|
||||||
|
"months" => months,
|
||||||
|
"empty_label" =>
|
||||||
|
BDS.Gettext.lgettext(language, "render", "No photos found")
|
||||||
|
},
|
||||||
|
context
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp render_tag_cloud_macro(context, params) do
|
||||||
|
language = Access.get(context, "language") || "en"
|
||||||
|
project_id = project_id_from_context(context)
|
||||||
|
|
||||||
|
{words_json, width, height} =
|
||||||
|
if project_id do
|
||||||
|
build_tag_cloud_data(project_id)
|
||||||
|
else
|
||||||
|
{nil, 800, 400}
|
||||||
|
end
|
||||||
|
|
||||||
|
render_macro_template(
|
||||||
|
"macros/tag-cloud",
|
||||||
|
%{
|
||||||
|
"orientation" => Map.get(params, "orientation", "horizontal"),
|
||||||
|
"words_json" => words_json,
|
||||||
|
"width" => Map.get(params, "width", width),
|
||||||
|
"height" => Map.get(params, "height", height),
|
||||||
|
"aria_label" => "Tag cloud",
|
||||||
|
"empty_label" =>
|
||||||
|
BDS.Gettext.lgettext(language, "render", "No tags")
|
||||||
|
},
|
||||||
|
context
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# ── Data queries for macros ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
defp linked_media_images(post_id) do
|
||||||
|
Repo.all(
|
||||||
|
from pm in PostMedia,
|
||||||
|
join: m in MediaRecord,
|
||||||
|
on: pm.media_id == m.id,
|
||||||
|
where: pm.post_id == ^post_id,
|
||||||
|
where: like(m.mime_type, "image/%"),
|
||||||
|
order_by: [asc: pm.sort_order, asc: pm.media_id],
|
||||||
|
select: m
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp media_month_archive(project_id, year, month) do
|
||||||
|
query =
|
||||||
|
from m in MediaRecord,
|
||||||
|
where: m.project_id == ^project_id,
|
||||||
|
where: like(m.mime_type, "image/%"),
|
||||||
|
order_by: [desc: m.created_at],
|
||||||
|
select: m
|
||||||
|
|
||||||
|
query =
|
||||||
|
if year do
|
||||||
|
year_int = parse_integer(year)
|
||||||
|
|
||||||
|
if month do
|
||||||
|
month_int = parse_integer(month)
|
||||||
|
start_ts = month_start_ms(year_int, month_int)
|
||||||
|
end_ts = month_end_ms(year_int, month_int)
|
||||||
|
|
||||||
|
from m in query,
|
||||||
|
where: m.created_at >= ^start_ts and m.created_at <= ^end_ts
|
||||||
|
else
|
||||||
|
start_ts = month_start_ms(year_int, 1)
|
||||||
|
end_ts = month_end_ms(year_int, 12)
|
||||||
|
|
||||||
|
from m in query,
|
||||||
|
where: m.created_at >= ^start_ts and m.created_at <= ^end_ts
|
||||||
|
end
|
||||||
|
else
|
||||||
|
from m in query, limit: 200
|
||||||
|
end
|
||||||
|
|
||||||
|
media_records =
|
||||||
|
query
|
||||||
|
|> Repo.all()
|
||||||
|
|> group_by_media_month()
|
||||||
|
|
||||||
|
if year == nil do
|
||||||
|
Enum.take(media_records, 10)
|
||||||
|
else
|
||||||
|
media_records
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp group_by_media_month(media_records) do
|
||||||
|
month_names = %{
|
||||||
|
1 => "January", 2 => "February", 3 => "March", 4 => "April",
|
||||||
|
5 => "May", 6 => "June", 7 => "July", 8 => "August",
|
||||||
|
9 => "September", 10 => "October", 11 => "November", 12 => "December"
|
||||||
|
}
|
||||||
|
|
||||||
|
media_records
|
||||||
|
|> Enum.group_by(fn m ->
|
||||||
|
date = DateTime.from_unix!(div(m.created_at, 1000))
|
||||||
|
{date.year, date.month}
|
||||||
|
end)
|
||||||
|
|> Enum.sort_by(fn {{y, m}, _} -> {y, m} end, :desc)
|
||||||
|
|> Enum.map(fn {{year, month}, items} ->
|
||||||
|
%{
|
||||||
|
"label" => "#{Map.get(month_names, month)} #{year}",
|
||||||
|
"items" =>
|
||||||
|
Enum.map(items, fn m ->
|
||||||
|
%{
|
||||||
|
"media_path" => "/#{m.file_path}",
|
||||||
|
"title" => m.title || m.original_name,
|
||||||
|
"alt" => m.alt || m.title || m.original_name,
|
||||||
|
"group_name" => "#{year}-#{month}"
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp build_tag_cloud_data(project_id) do
|
||||||
|
tag_colors =
|
||||||
|
Repo.all(
|
||||||
|
from tag in Tag,
|
||||||
|
where: tag.project_id == ^project_id,
|
||||||
|
where: not is_nil(tag.color) and tag.color != "",
|
||||||
|
select: {tag.name, tag.color}
|
||||||
|
)
|
||||||
|
|> Map.new()
|
||||||
|
|
||||||
|
%{rows: rows} =
|
||||||
|
Ecto.Adapters.SQL.query!(
|
||||||
|
Repo,
|
||||||
|
"""
|
||||||
|
SELECT trim(je.value) AS tag, COUNT(*) AS cnt
|
||||||
|
FROM posts, json_each(posts.tags) je
|
||||||
|
WHERE posts.project_id = ?1
|
||||||
|
AND trim(je.value) != ''
|
||||||
|
GROUP BY tag
|
||||||
|
ORDER BY cnt DESC, lower(tag) ASC
|
||||||
|
""",
|
||||||
|
[project_id]
|
||||||
|
)
|
||||||
|
|
||||||
|
tag_entries =
|
||||||
|
Enum.map(rows, fn [tag, count] ->
|
||||||
|
%{tag: tag, count: count, color: Map.get(tag_colors, tag)}
|
||||||
|
end)
|
||||||
|
|
||||||
|
if tag_entries == [] do
|
||||||
|
{nil, 0, 0}
|
||||||
|
else
|
||||||
|
max_count = Enum.map(tag_entries, & &1.count) |> Enum.max()
|
||||||
|
min_count = Enum.map(tag_entries, & &1.count) |> Enum.min()
|
||||||
|
range = max(max_count - min_count, 1)
|
||||||
|
|
||||||
|
words =
|
||||||
|
Enum.map(tag_entries, fn %{tag: tag, count: count, color: color} ->
|
||||||
|
size = 12.0 + (count - min_count) / range * 28.0
|
||||||
|
%{"text" => tag, "size" => size, "color" => color || "var(--accent-color)"}
|
||||||
|
end)
|
||||||
|
|
||||||
|
{Jason.encode!(words), 800, 400}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# ── Helpers ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
defp project_id_from_context(context) do
|
||||||
|
post = Access.get(context, "post") || %{}
|
||||||
|
post["project_id"] || Access.get(post, :project_id) || project_id_from_post(context)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp project_id_from_post(context) do
|
||||||
|
post_id =
|
||||||
|
Access.get(Access.get(context, "post") || %{}, "id") ||
|
||||||
|
Access.get(Access.get(context, "post") || %{}, :id)
|
||||||
|
|
||||||
|
if is_binary(post_id) do
|
||||||
|
case Repo.one(from p in Post, where: p.id == ^post_id, select: p.project_id) do
|
||||||
|
nil -> nil
|
||||||
|
project_id -> project_id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp normalize_columns(value, default, min, max) when is_binary(value) do
|
||||||
|
case Integer.parse(value) do
|
||||||
|
{n, ""} -> n |> max(min) |> min(max)
|
||||||
|
_ -> default
|
||||||
|
end
|
||||||
|
end
|
||||||
|
defp normalize_columns(value, _default, min, max) when is_integer(value),
|
||||||
|
do: value |> max(min) |> min(max)
|
||||||
|
defp normalize_columns(_value, default, _min, _max), do: default
|
||||||
|
|
||||||
|
defp parse_integer(value) when is_binary(value) do
|
||||||
|
case Integer.parse(value) do
|
||||||
|
{n, ""} -> n
|
||||||
|
_ -> nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
defp parse_integer(value) when is_integer(value), do: value
|
||||||
|
defp parse_integer(_value), do: nil
|
||||||
|
|
||||||
|
defp month_start_ms(year, month) do
|
||||||
|
case DateTime.from_iso8601("#{year}-#{pad(month)}-01T00:00:00Z") do
|
||||||
|
{:ok, dt, _} -> DateTime.to_unix(dt, :millisecond)
|
||||||
|
_ -> 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp month_end_ms(year, month) do
|
||||||
|
last_day =
|
||||||
|
if month == 12 do
|
||||||
|
31
|
||||||
|
else
|
||||||
|
case DateTime.from_iso8601("#{year}-#{pad(month + 1)}-01T00:00:00Z") do
|
||||||
|
{:ok, dt, _} ->
|
||||||
|
dt |> DateTime.add(-1, :second) |> DateTime.to_date() |> Map.get(:day)
|
||||||
|
_ ->
|
||||||
|
31
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
case DateTime.from_iso8601("#{year}-#{pad(month)}-#{pad(last_day)}T23:59:59.999Z") do
|
||||||
|
{:ok, dt, _} -> DateTime.to_unix(dt, :millisecond)
|
||||||
|
_ -> 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp pad(n) when is_integer(n), do: n |> Integer.to_string() |> String.pad_leading(2, "0")
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ defmodule BDS.Rendering.PostRendering do
|
|||||||
alias BDS.Rendering.TemplateSelection
|
alias BDS.Rendering.TemplateSelection
|
||||||
alias BDS.MapUtils
|
alias BDS.MapUtils
|
||||||
alias BDS.Posts.Post
|
alias BDS.Posts.Post
|
||||||
|
alias BDS.Posts.PostMedia
|
||||||
alias BDS.Posts.Translation
|
alias BDS.Posts.Translation
|
||||||
|
alias BDS.Media.Media, as: MediaRecord
|
||||||
alias BDS.Repo
|
alias BDS.Repo
|
||||||
|
|
||||||
@spec post_assigns(String.t(), map()) :: map()
|
@spec post_assigns(String.t(), map()) :: map()
|
||||||
@@ -39,7 +41,8 @@ defmodule BDS.Rendering.PostRendering do
|
|||||||
canonical_post_paths,
|
canonical_post_paths,
|
||||||
canonical_media_paths,
|
canonical_media_paths,
|
||||||
language,
|
language,
|
||||||
template_context
|
template_context,
|
||||||
|
post_id
|
||||||
)
|
)
|
||||||
|
|
||||||
incoming_links =
|
incoming_links =
|
||||||
@@ -208,6 +211,7 @@ defmodule BDS.Rendering.PostRendering do
|
|||||||
title: MapUtils.attr(assigns, :title),
|
title: MapUtils.attr(assigns, :title),
|
||||||
content: MapUtils.attr(assigns, :content),
|
content: MapUtils.attr(assigns, :content),
|
||||||
raw_content: MapUtils.attr(assigns, :raw_content),
|
raw_content: MapUtils.attr(assigns, :raw_content),
|
||||||
|
project_id: MapUtils.attr(assigns, :project_id) || Map.get(post_record || %{}, :project_id),
|
||||||
excerpt:
|
excerpt:
|
||||||
Map.get(
|
Map.get(
|
||||||
assigns,
|
assigns,
|
||||||
@@ -234,7 +238,7 @@ defmodule BDS.Rendering.PostRendering do
|
|||||||
MapUtils.attr(assigns, :template_slug)
|
MapUtils.attr(assigns, :template_slug)
|
||||||
),
|
),
|
||||||
do_not_translate: Map.get(post_record || %{}, :do_not_translate, false),
|
do_not_translate: Map.get(post_record || %{}, :do_not_translate, false),
|
||||||
linked_media: [],
|
linked_media: linked_media_images(assigns),
|
||||||
outgoing_links: outgoing_links,
|
outgoing_links: outgoing_links,
|
||||||
incoming_links: incoming_links
|
incoming_links: incoming_links
|
||||||
}
|
}
|
||||||
@@ -245,21 +249,42 @@ defmodule BDS.Rendering.PostRendering do
|
|||||||
map(),
|
map(),
|
||||||
map(),
|
map(),
|
||||||
String.t(),
|
String.t(),
|
||||||
Liquex.Context.t()
|
Liquex.Context.t(),
|
||||||
|
term()
|
||||||
) :: String.t()
|
) :: String.t()
|
||||||
def render_post_content(
|
def render_post_content(
|
||||||
content,
|
content,
|
||||||
canonical_post_paths,
|
canonical_post_paths,
|
||||||
canonical_media_paths,
|
canonical_media_paths,
|
||||||
language,
|
language,
|
||||||
template_context
|
template_context,
|
||||||
|
post_id \\ nil
|
||||||
) do
|
) do
|
||||||
Filters.render_markdown(
|
Filters.render_markdown(
|
||||||
content,
|
content,
|
||||||
canonical_post_paths,
|
canonical_post_paths,
|
||||||
canonical_media_paths,
|
canonical_media_paths,
|
||||||
language,
|
language,
|
||||||
template_context
|
template_context,
|
||||||
|
post_id
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp linked_media_images(assigns) do
|
||||||
|
post_id = MapUtils.attr(assigns, :id)
|
||||||
|
|
||||||
|
if is_binary(post_id) do
|
||||||
|
Repo.all(
|
||||||
|
from pm in PostMedia,
|
||||||
|
join: m in MediaRecord,
|
||||||
|
on: pm.media_id == m.id,
|
||||||
|
where: pm.post_id == ^post_id,
|
||||||
|
where: like(m.mime_type, "image/%"),
|
||||||
|
order_by: [asc: pm.sort_order, asc: pm.media_id],
|
||||||
|
select: m
|
||||||
|
)
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,156 +1,156 @@
|
|||||||
#: lib/bds/rendering/labels.ex:17
|
#: lib/bds/rendering/labels.ex:18
|
||||||
#: lib/bds/ui/sidebar.ex:241
|
#: lib/bds/ui/sidebar.ex:284
|
||||||
#: lib/bds/ui/sidebar.ex:316
|
#: lib/bds/ui/sidebar.ex:578
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Archive"
|
msgid "Archive"
|
||||||
msgstr "Archiv"
|
msgstr "Archiv"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:52
|
#: lib/bds/rendering/labels.ex:54
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "April"
|
msgid "April"
|
||||||
msgstr "Apr."
|
msgstr "Apr."
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:24
|
#: lib/bds/rendering/labels.ex:25
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
msgid "Archive calendar"
|
msgid "Archive calendar"
|
||||||
msgstr "Archiv"
|
msgstr "Archiv"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:68
|
#: lib/bds/rendering/labels.ex:70
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "August"
|
msgid "August"
|
||||||
msgstr "Aug."
|
msgstr "Aug."
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:15
|
#: lib/bds/rendering/labels.ex:16
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Backlinks"
|
msgid "Backlinks"
|
||||||
msgstr "Rückverweise"
|
msgstr "Rückverweise"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:23
|
#: lib/bds/rendering/labels.ex:24
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Calendar data could not be loaded."
|
msgid "Calendar data could not be loaded."
|
||||||
msgstr "Kalenderdaten konnten nicht geladen werden."
|
msgstr "Kalenderdaten konnten nicht geladen werden."
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:25
|
#: lib/bds/rendering/labels.ex:26
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Close calendar"
|
msgid "Close calendar"
|
||||||
msgstr "Kalender schließen"
|
msgstr "Kalender schließen"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:84
|
#: lib/bds/rendering/labels.ex:86
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "December"
|
msgid "December"
|
||||||
msgstr "Dezember"
|
msgstr "Dezember"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:44
|
#: lib/bds/rendering/labels.ex:46
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "February"
|
msgid "February"
|
||||||
msgstr "Februar"
|
msgstr "Februar"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:40
|
#: lib/bds/rendering/labels.ex:42
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "January"
|
msgid "January"
|
||||||
msgstr "Januar"
|
msgstr "Januar"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:64
|
#: lib/bds/rendering/labels.ex:66
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "July"
|
msgid "July"
|
||||||
msgstr "Juli"
|
msgstr "Juli"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:60
|
#: lib/bds/rendering/labels.ex:62
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "June"
|
msgid "June"
|
||||||
msgstr "Juni"
|
msgstr "Juni"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:26
|
#: lib/bds/rendering/labels.ex:27
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Language"
|
msgid "Language"
|
||||||
msgstr "Sprache"
|
msgstr "Sprache"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:16
|
#: lib/bds/rendering/labels.ex:17
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Linked from"
|
msgid "Linked from"
|
||||||
msgstr "Verlinkt von"
|
msgstr "Verlinkt von"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:22
|
#: lib/bds/rendering/labels.ex:23
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Loading calendar…"
|
msgid "Loading calendar…"
|
||||||
msgstr "Kalender wird geladen …"
|
msgstr "Kalender wird geladen …"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:48
|
#: lib/bds/rendering/labels.ex:50
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "March"
|
msgid "March"
|
||||||
msgstr "März"
|
msgstr "März"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:56
|
#: lib/bds/rendering/labels.ex:58
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "May"
|
msgid "May"
|
||||||
msgstr "Mai"
|
msgstr "Mai"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:80
|
#: lib/bds/rendering/labels.ex:82
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "November"
|
msgid "November"
|
||||||
msgstr "Nov."
|
msgstr "Nov."
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:76
|
#: lib/bds/rendering/labels.ex:78
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "October"
|
msgid "October"
|
||||||
msgstr "Oktober"
|
msgstr "Oktober"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:21
|
#: lib/bds/rendering/labels.ex:22
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Open calendar"
|
msgid "Open calendar"
|
||||||
msgstr "Kalender öffnen"
|
msgstr "Kalender öffnen"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:18
|
#: lib/bds/rendering/labels.ex:19
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Pagination"
|
msgid "Pagination"
|
||||||
msgstr "Seitennummerierung"
|
msgstr "Seitennummerierung"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:28
|
#: lib/bds/rendering/labels.ex:29
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Search..."
|
msgid "Search..."
|
||||||
msgstr "Suchen..."
|
msgstr "Suchen..."
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:72
|
#: lib/bds/rendering/labels.ex:74
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "September"
|
msgid "September"
|
||||||
msgstr "Sept."
|
msgstr "Sept."
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:27
|
#: lib/bds/rendering/labels.ex:28
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Site search"
|
msgid "Site search"
|
||||||
msgstr "Seitensuche"
|
msgstr "Seitensuche"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:14
|
#: lib/bds/rendering/labels.ex:15
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Taxonomy"
|
msgid "Taxonomy"
|
||||||
msgstr "Taxonomie"
|
msgstr "Taxonomie"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:19
|
#: lib/bds/rendering/labels.ex:20
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "newer"
|
msgid "newer"
|
||||||
msgstr "neuer"
|
msgstr "neuer"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:20
|
#: lib/bds/rendering/labels.ex:21
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "older"
|
msgid "older"
|
||||||
msgstr "älter"
|
msgstr "älter"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:30
|
#: lib/bds/rendering/labels.ex:31
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Back to preview home"
|
msgid "Back to preview home"
|
||||||
msgstr "Zurück zur Vorschau-Startseite"
|
msgstr "Zurück zur Vorschau-Startseite"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:29
|
#: lib/bds/rendering/labels.ex:30
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "The requested preview page could not be found."
|
msgid "The requested preview page could not be found."
|
||||||
msgstr "Die angeforderte Vorschauseite konnte nicht gefunden werden."
|
msgstr "Die angeforderte Vorschauseite konnte nicht gefunden werden."
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:32
|
#: lib/bds/rendering/labels.ex:33
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Vimeo video"
|
msgid "Vimeo video"
|
||||||
msgstr "Vimeo-Video"
|
msgstr "Vimeo-Video"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:31
|
#: lib/bds/rendering/labels.ex:32
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "YouTube video"
|
msgid "YouTube video"
|
||||||
msgstr "YouTube-Video"
|
msgstr "YouTube-Video"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,156 +1,156 @@
|
|||||||
#: lib/bds/rendering/labels.ex:17
|
#: lib/bds/rendering/labels.ex:18
|
||||||
#: lib/bds/ui/sidebar.ex:241
|
#: lib/bds/ui/sidebar.ex:284
|
||||||
#: lib/bds/ui/sidebar.ex:316
|
#: lib/bds/ui/sidebar.ex:578
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Archive"
|
msgid "Archive"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:52
|
#: lib/bds/rendering/labels.ex:54
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "April"
|
msgid "April"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:24
|
#: lib/bds/rendering/labels.ex:25
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
msgid "Archive calendar"
|
msgid "Archive calendar"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:68
|
#: lib/bds/rendering/labels.ex:70
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "August"
|
msgid "August"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:15
|
#: lib/bds/rendering/labels.ex:16
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Backlinks"
|
msgid "Backlinks"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:23
|
#: lib/bds/rendering/labels.ex:24
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Calendar data could not be loaded."
|
msgid "Calendar data could not be loaded."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:25
|
#: lib/bds/rendering/labels.ex:26
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Close calendar"
|
msgid "Close calendar"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:84
|
#: lib/bds/rendering/labels.ex:86
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "December"
|
msgid "December"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:44
|
#: lib/bds/rendering/labels.ex:46
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "February"
|
msgid "February"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:40
|
#: lib/bds/rendering/labels.ex:42
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "January"
|
msgid "January"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:64
|
#: lib/bds/rendering/labels.ex:66
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "July"
|
msgid "July"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:60
|
#: lib/bds/rendering/labels.ex:62
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "June"
|
msgid "June"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:26
|
#: lib/bds/rendering/labels.ex:27
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Language"
|
msgid "Language"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:16
|
#: lib/bds/rendering/labels.ex:17
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Linked from"
|
msgid "Linked from"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:22
|
#: lib/bds/rendering/labels.ex:23
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Loading calendar…"
|
msgid "Loading calendar…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:48
|
#: lib/bds/rendering/labels.ex:50
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "March"
|
msgid "March"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:56
|
#: lib/bds/rendering/labels.ex:58
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "May"
|
msgid "May"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:80
|
#: lib/bds/rendering/labels.ex:82
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "November"
|
msgid "November"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:76
|
#: lib/bds/rendering/labels.ex:78
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "October"
|
msgid "October"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:21
|
#: lib/bds/rendering/labels.ex:22
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Open calendar"
|
msgid "Open calendar"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:18
|
#: lib/bds/rendering/labels.ex:19
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Pagination"
|
msgid "Pagination"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:28
|
#: lib/bds/rendering/labels.ex:29
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Search..."
|
msgid "Search..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:72
|
#: lib/bds/rendering/labels.ex:74
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "September"
|
msgid "September"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:27
|
#: lib/bds/rendering/labels.ex:28
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Site search"
|
msgid "Site search"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:14
|
#: lib/bds/rendering/labels.ex:15
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Taxonomy"
|
msgid "Taxonomy"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:19
|
#: lib/bds/rendering/labels.ex:20
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "newer"
|
msgid "newer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:20
|
#: lib/bds/rendering/labels.ex:21
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "older"
|
msgid "older"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:30
|
#: lib/bds/rendering/labels.ex:31
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Back to preview home"
|
msgid "Back to preview home"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:29
|
#: lib/bds/rendering/labels.ex:30
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "The requested preview page could not be found."
|
msgid "The requested preview page could not be found."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:32
|
#: lib/bds/rendering/labels.ex:33
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Vimeo video"
|
msgid "Vimeo video"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:31
|
#: lib/bds/rendering/labels.ex:32
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "YouTube video"
|
msgid "YouTube video"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,156 +1,156 @@
|
|||||||
#: lib/bds/rendering/labels.ex:17
|
#: lib/bds/rendering/labels.ex:18
|
||||||
#: lib/bds/ui/sidebar.ex:241
|
#: lib/bds/ui/sidebar.ex:284
|
||||||
#: lib/bds/ui/sidebar.ex:316
|
#: lib/bds/ui/sidebar.ex:578
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Archive"
|
msgid "Archive"
|
||||||
msgstr "Archivo"
|
msgstr "Archivo"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:52
|
#: lib/bds/rendering/labels.ex:54
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "April"
|
msgid "April"
|
||||||
msgstr "abril"
|
msgstr "abril"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:24
|
#: lib/bds/rendering/labels.ex:25
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
msgid "Archive calendar"
|
msgid "Archive calendar"
|
||||||
msgstr "Archivo"
|
msgstr "Archivo"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:68
|
#: lib/bds/rendering/labels.ex:70
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "August"
|
msgid "August"
|
||||||
msgstr "agosto"
|
msgstr "agosto"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:15
|
#: lib/bds/rendering/labels.ex:16
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Backlinks"
|
msgid "Backlinks"
|
||||||
msgstr "Retroenlaces"
|
msgstr "Retroenlaces"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:23
|
#: lib/bds/rendering/labels.ex:24
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Calendar data could not be loaded."
|
msgid "Calendar data could not be loaded."
|
||||||
msgstr "No se pudieron cargar los datos del calendario."
|
msgstr "No se pudieron cargar los datos del calendario."
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:25
|
#: lib/bds/rendering/labels.ex:26
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Close calendar"
|
msgid "Close calendar"
|
||||||
msgstr "Cerrar calendario"
|
msgstr "Cerrar calendario"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:84
|
#: lib/bds/rendering/labels.ex:86
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "December"
|
msgid "December"
|
||||||
msgstr "diciembre"
|
msgstr "diciembre"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:44
|
#: lib/bds/rendering/labels.ex:46
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "February"
|
msgid "February"
|
||||||
msgstr "febrero"
|
msgstr "febrero"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:40
|
#: lib/bds/rendering/labels.ex:42
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "January"
|
msgid "January"
|
||||||
msgstr "enero"
|
msgstr "enero"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:64
|
#: lib/bds/rendering/labels.ex:66
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "July"
|
msgid "July"
|
||||||
msgstr "julio"
|
msgstr "julio"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:60
|
#: lib/bds/rendering/labels.ex:62
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "June"
|
msgid "June"
|
||||||
msgstr "junio"
|
msgstr "junio"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:26
|
#: lib/bds/rendering/labels.ex:27
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Language"
|
msgid "Language"
|
||||||
msgstr "Idioma"
|
msgstr "Idioma"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:16
|
#: lib/bds/rendering/labels.ex:17
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Linked from"
|
msgid "Linked from"
|
||||||
msgstr "Enlazado desde"
|
msgstr "Enlazado desde"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:22
|
#: lib/bds/rendering/labels.ex:23
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Loading calendar…"
|
msgid "Loading calendar…"
|
||||||
msgstr "Cargando calendario…"
|
msgstr "Cargando calendario…"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:48
|
#: lib/bds/rendering/labels.ex:50
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "March"
|
msgid "March"
|
||||||
msgstr "marzo"
|
msgstr "marzo"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:56
|
#: lib/bds/rendering/labels.ex:58
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "May"
|
msgid "May"
|
||||||
msgstr "mayo"
|
msgstr "mayo"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:80
|
#: lib/bds/rendering/labels.ex:82
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "November"
|
msgid "November"
|
||||||
msgstr "noviembre"
|
msgstr "noviembre"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:76
|
#: lib/bds/rendering/labels.ex:78
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "October"
|
msgid "October"
|
||||||
msgstr "octubre"
|
msgstr "octubre"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:21
|
#: lib/bds/rendering/labels.ex:22
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Open calendar"
|
msgid "Open calendar"
|
||||||
msgstr "Abrir calendario"
|
msgstr "Abrir calendario"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:18
|
#: lib/bds/rendering/labels.ex:19
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Pagination"
|
msgid "Pagination"
|
||||||
msgstr "Paginación"
|
msgstr "Paginación"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:28
|
#: lib/bds/rendering/labels.ex:29
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Search..."
|
msgid "Search..."
|
||||||
msgstr "Buscar..."
|
msgstr "Buscar..."
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:72
|
#: lib/bds/rendering/labels.ex:74
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "September"
|
msgid "September"
|
||||||
msgstr "septiembre"
|
msgstr "septiembre"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:27
|
#: lib/bds/rendering/labels.ex:28
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Site search"
|
msgid "Site search"
|
||||||
msgstr "Buscar en el sitio"
|
msgstr "Buscar en el sitio"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:14
|
#: lib/bds/rendering/labels.ex:15
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Taxonomy"
|
msgid "Taxonomy"
|
||||||
msgstr "Taxonomía"
|
msgstr "Taxonomía"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:19
|
#: lib/bds/rendering/labels.ex:20
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "newer"
|
msgid "newer"
|
||||||
msgstr "más reciente"
|
msgstr "más reciente"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:20
|
#: lib/bds/rendering/labels.ex:21
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "older"
|
msgid "older"
|
||||||
msgstr "más antiguo"
|
msgstr "más antiguo"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:30
|
#: lib/bds/rendering/labels.ex:31
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Back to preview home"
|
msgid "Back to preview home"
|
||||||
msgstr "Volver al inicio de vista previa"
|
msgstr "Volver al inicio de vista previa"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:29
|
#: lib/bds/rendering/labels.ex:30
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "The requested preview page could not be found."
|
msgid "The requested preview page could not be found."
|
||||||
msgstr "No se pudo encontrar la página de vista previa solicitada."
|
msgstr "No se pudo encontrar la página de vista previa solicitada."
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:32
|
#: lib/bds/rendering/labels.ex:33
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Vimeo video"
|
msgid "Vimeo video"
|
||||||
msgstr "Vídeo de Vimeo"
|
msgstr "Vídeo de Vimeo"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:31
|
#: lib/bds/rendering/labels.ex:32
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "YouTube video"
|
msgid "YouTube video"
|
||||||
msgstr "Vídeo de YouTube"
|
msgstr "Vídeo de YouTube"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,156 +1,156 @@
|
|||||||
#: lib/bds/rendering/labels.ex:17
|
#: lib/bds/rendering/labels.ex:18
|
||||||
#: lib/bds/ui/sidebar.ex:241
|
#: lib/bds/ui/sidebar.ex:284
|
||||||
#: lib/bds/ui/sidebar.ex:316
|
#: lib/bds/ui/sidebar.ex:578
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Archive"
|
msgid "Archive"
|
||||||
msgstr "Archives"
|
msgstr "Archives"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:52
|
#: lib/bds/rendering/labels.ex:54
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "April"
|
msgid "April"
|
||||||
msgstr "avril"
|
msgstr "avril"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:24
|
#: lib/bds/rendering/labels.ex:25
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
msgid "Archive calendar"
|
msgid "Archive calendar"
|
||||||
msgstr "Archives"
|
msgstr "Archives"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:68
|
#: lib/bds/rendering/labels.ex:70
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "August"
|
msgid "August"
|
||||||
msgstr "août"
|
msgstr "août"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:15
|
#: lib/bds/rendering/labels.ex:16
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Backlinks"
|
msgid "Backlinks"
|
||||||
msgstr "Rétroliens"
|
msgstr "Rétroliens"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:23
|
#: lib/bds/rendering/labels.ex:24
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Calendar data could not be loaded."
|
msgid "Calendar data could not be loaded."
|
||||||
msgstr "Impossible de charger les données du calendrier."
|
msgstr "Impossible de charger les données du calendrier."
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:25
|
#: lib/bds/rendering/labels.ex:26
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Close calendar"
|
msgid "Close calendar"
|
||||||
msgstr "Fermer le calendrier"
|
msgstr "Fermer le calendrier"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:84
|
#: lib/bds/rendering/labels.ex:86
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "December"
|
msgid "December"
|
||||||
msgstr "décembre"
|
msgstr "décembre"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:44
|
#: lib/bds/rendering/labels.ex:46
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "February"
|
msgid "February"
|
||||||
msgstr "février"
|
msgstr "février"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:40
|
#: lib/bds/rendering/labels.ex:42
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "January"
|
msgid "January"
|
||||||
msgstr "janvier"
|
msgstr "janvier"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:64
|
#: lib/bds/rendering/labels.ex:66
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "July"
|
msgid "July"
|
||||||
msgstr "juillet"
|
msgstr "juillet"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:60
|
#: lib/bds/rendering/labels.ex:62
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "June"
|
msgid "June"
|
||||||
msgstr "juin"
|
msgstr "juin"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:26
|
#: lib/bds/rendering/labels.ex:27
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Language"
|
msgid "Language"
|
||||||
msgstr "Langue"
|
msgstr "Langue"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:16
|
#: lib/bds/rendering/labels.ex:17
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Linked from"
|
msgid "Linked from"
|
||||||
msgstr "Lié depuis"
|
msgstr "Lié depuis"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:22
|
#: lib/bds/rendering/labels.ex:23
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Loading calendar…"
|
msgid "Loading calendar…"
|
||||||
msgstr "Chargement du calendrier…"
|
msgstr "Chargement du calendrier…"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:48
|
#: lib/bds/rendering/labels.ex:50
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "March"
|
msgid "March"
|
||||||
msgstr "mars"
|
msgstr "mars"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:56
|
#: lib/bds/rendering/labels.ex:58
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "May"
|
msgid "May"
|
||||||
msgstr "mai"
|
msgstr "mai"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:80
|
#: lib/bds/rendering/labels.ex:82
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "November"
|
msgid "November"
|
||||||
msgstr "novembre"
|
msgstr "novembre"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:76
|
#: lib/bds/rendering/labels.ex:78
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "October"
|
msgid "October"
|
||||||
msgstr "octobre"
|
msgstr "octobre"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:21
|
#: lib/bds/rendering/labels.ex:22
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Open calendar"
|
msgid "Open calendar"
|
||||||
msgstr "Ouvrir le calendrier"
|
msgstr "Ouvrir le calendrier"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:18
|
#: lib/bds/rendering/labels.ex:19
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Pagination"
|
msgid "Pagination"
|
||||||
msgstr "Navigation paginée"
|
msgstr "Navigation paginée"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:28
|
#: lib/bds/rendering/labels.ex:29
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Search..."
|
msgid "Search..."
|
||||||
msgstr "Rechercher..."
|
msgstr "Rechercher..."
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:72
|
#: lib/bds/rendering/labels.ex:74
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "September"
|
msgid "September"
|
||||||
msgstr "septembre"
|
msgstr "septembre"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:27
|
#: lib/bds/rendering/labels.ex:28
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Site search"
|
msgid "Site search"
|
||||||
msgstr "Recherche du site"
|
msgstr "Recherche du site"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:14
|
#: lib/bds/rendering/labels.ex:15
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Taxonomy"
|
msgid "Taxonomy"
|
||||||
msgstr "Taxonomie"
|
msgstr "Taxonomie"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:19
|
#: lib/bds/rendering/labels.ex:20
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "newer"
|
msgid "newer"
|
||||||
msgstr "plus récent"
|
msgstr "plus récent"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:20
|
#: lib/bds/rendering/labels.ex:21
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "older"
|
msgid "older"
|
||||||
msgstr "plus ancien"
|
msgstr "plus ancien"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:30
|
#: lib/bds/rendering/labels.ex:31
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Back to preview home"
|
msgid "Back to preview home"
|
||||||
msgstr "Retour à l’accueil de l’aperçu"
|
msgstr "Retour à l’accueil de l’aperçu"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:29
|
#: lib/bds/rendering/labels.ex:30
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "The requested preview page could not be found."
|
msgid "The requested preview page could not be found."
|
||||||
msgstr "La page d’aperçu demandée est introuvable."
|
msgstr "La page d’aperçu demandée est introuvable."
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:32
|
#: lib/bds/rendering/labels.ex:33
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Vimeo video"
|
msgid "Vimeo video"
|
||||||
msgstr "Vidéo Vimeo"
|
msgstr "Vidéo Vimeo"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:31
|
#: lib/bds/rendering/labels.ex:32
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "YouTube video"
|
msgid "YouTube video"
|
||||||
msgstr "Vidéo YouTube"
|
msgstr "Vidéo YouTube"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,156 +1,156 @@
|
|||||||
#: lib/bds/rendering/labels.ex:17
|
#: lib/bds/rendering/labels.ex:18
|
||||||
#: lib/bds/ui/sidebar.ex:241
|
#: lib/bds/ui/sidebar.ex:284
|
||||||
#: lib/bds/ui/sidebar.ex:316
|
#: lib/bds/ui/sidebar.ex:578
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Archive"
|
msgid "Archive"
|
||||||
msgstr "Archivio"
|
msgstr "Archivio"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:52
|
#: lib/bds/rendering/labels.ex:54
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "April"
|
msgid "April"
|
||||||
msgstr "aprile"
|
msgstr "aprile"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:24
|
#: lib/bds/rendering/labels.ex:25
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
msgid "Archive calendar"
|
msgid "Archive calendar"
|
||||||
msgstr "Archivio"
|
msgstr "Archivio"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:68
|
#: lib/bds/rendering/labels.ex:70
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "August"
|
msgid "August"
|
||||||
msgstr "agosto"
|
msgstr "agosto"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:15
|
#: lib/bds/rendering/labels.ex:16
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Backlinks"
|
msgid "Backlinks"
|
||||||
msgstr "Retrocollegamenti"
|
msgstr "Retrocollegamenti"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:23
|
#: lib/bds/rendering/labels.ex:24
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Calendar data could not be loaded."
|
msgid "Calendar data could not be loaded."
|
||||||
msgstr "Impossibile caricare i dati del calendario."
|
msgstr "Impossibile caricare i dati del calendario."
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:25
|
#: lib/bds/rendering/labels.ex:26
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Close calendar"
|
msgid "Close calendar"
|
||||||
msgstr "Chiudi calendario"
|
msgstr "Chiudi calendario"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:84
|
#: lib/bds/rendering/labels.ex:86
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "December"
|
msgid "December"
|
||||||
msgstr "dicembre"
|
msgstr "dicembre"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:44
|
#: lib/bds/rendering/labels.ex:46
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "February"
|
msgid "February"
|
||||||
msgstr "febbraio"
|
msgstr "febbraio"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:40
|
#: lib/bds/rendering/labels.ex:42
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "January"
|
msgid "January"
|
||||||
msgstr "gennaio"
|
msgstr "gennaio"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:64
|
#: lib/bds/rendering/labels.ex:66
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "July"
|
msgid "July"
|
||||||
msgstr "luglio"
|
msgstr "luglio"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:60
|
#: lib/bds/rendering/labels.ex:62
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "June"
|
msgid "June"
|
||||||
msgstr "giugno"
|
msgstr "giugno"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:26
|
#: lib/bds/rendering/labels.ex:27
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Language"
|
msgid "Language"
|
||||||
msgstr "Lingua"
|
msgstr "Lingua"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:16
|
#: lib/bds/rendering/labels.ex:17
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Linked from"
|
msgid "Linked from"
|
||||||
msgstr "Collegato da"
|
msgstr "Collegato da"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:22
|
#: lib/bds/rendering/labels.ex:23
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Loading calendar…"
|
msgid "Loading calendar…"
|
||||||
msgstr "Caricamento calendario…"
|
msgstr "Caricamento calendario…"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:48
|
#: lib/bds/rendering/labels.ex:50
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "March"
|
msgid "March"
|
||||||
msgstr "marzo"
|
msgstr "marzo"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:56
|
#: lib/bds/rendering/labels.ex:58
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "May"
|
msgid "May"
|
||||||
msgstr "maggio"
|
msgstr "maggio"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:80
|
#: lib/bds/rendering/labels.ex:82
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "November"
|
msgid "November"
|
||||||
msgstr "novembre"
|
msgstr "novembre"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:76
|
#: lib/bds/rendering/labels.ex:78
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "October"
|
msgid "October"
|
||||||
msgstr "ottobre"
|
msgstr "ottobre"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:21
|
#: lib/bds/rendering/labels.ex:22
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Open calendar"
|
msgid "Open calendar"
|
||||||
msgstr "Apri calendario"
|
msgstr "Apri calendario"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:18
|
#: lib/bds/rendering/labels.ex:19
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Pagination"
|
msgid "Pagination"
|
||||||
msgstr "Paginazione"
|
msgstr "Paginazione"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:28
|
#: lib/bds/rendering/labels.ex:29
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Search..."
|
msgid "Search..."
|
||||||
msgstr "Cerca..."
|
msgstr "Cerca..."
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:72
|
#: lib/bds/rendering/labels.ex:74
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "September"
|
msgid "September"
|
||||||
msgstr "settembre"
|
msgstr "settembre"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:27
|
#: lib/bds/rendering/labels.ex:28
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Site search"
|
msgid "Site search"
|
||||||
msgstr "Ricerca nel sito"
|
msgstr "Ricerca nel sito"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:14
|
#: lib/bds/rendering/labels.ex:15
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Taxonomy"
|
msgid "Taxonomy"
|
||||||
msgstr "Tassonomia"
|
msgstr "Tassonomia"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:19
|
#: lib/bds/rendering/labels.ex:20
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "newer"
|
msgid "newer"
|
||||||
msgstr "più recente"
|
msgstr "più recente"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:20
|
#: lib/bds/rendering/labels.ex:21
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "older"
|
msgid "older"
|
||||||
msgstr "più vecchio"
|
msgstr "più vecchio"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:30
|
#: lib/bds/rendering/labels.ex:31
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Back to preview home"
|
msgid "Back to preview home"
|
||||||
msgstr "Torna alla home di anteprima"
|
msgstr "Torna alla home di anteprima"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:29
|
#: lib/bds/rendering/labels.ex:30
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "The requested preview page could not be found."
|
msgid "The requested preview page could not be found."
|
||||||
msgstr "La pagina di anteprima richiesta non è stata trovata."
|
msgstr "La pagina di anteprima richiesta non è stata trovata."
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:32
|
#: lib/bds/rendering/labels.ex:33
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Vimeo video"
|
msgid "Vimeo video"
|
||||||
msgstr "Video Vimeo"
|
msgstr "Video Vimeo"
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:31
|
#: lib/bds/rendering/labels.ex:32
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "YouTube video"
|
msgid "YouTube video"
|
||||||
msgstr "Video YouTube"
|
msgstr "Video YouTube"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -11,159 +11,159 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:17
|
#: lib/bds/rendering/labels.ex:18
|
||||||
#: lib/bds/ui/sidebar.ex:241
|
#: lib/bds/ui/sidebar.ex:284
|
||||||
#: lib/bds/ui/sidebar.ex:316
|
#: lib/bds/ui/sidebar.ex:578
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Archive"
|
msgid "Archive"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:52
|
#: lib/bds/rendering/labels.ex:54
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "April"
|
msgid "April"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:24
|
#: lib/bds/rendering/labels.ex:25
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Archive calendar"
|
msgid "Archive calendar"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:68
|
#: lib/bds/rendering/labels.ex:70
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "August"
|
msgid "August"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:15
|
#: lib/bds/rendering/labels.ex:16
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Backlinks"
|
msgid "Backlinks"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:23
|
#: lib/bds/rendering/labels.ex:24
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Calendar data could not be loaded."
|
msgid "Calendar data could not be loaded."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:25
|
#: lib/bds/rendering/labels.ex:26
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Close calendar"
|
msgid "Close calendar"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:84
|
#: lib/bds/rendering/labels.ex:86
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "December"
|
msgid "December"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:44
|
#: lib/bds/rendering/labels.ex:46
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "February"
|
msgid "February"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:40
|
#: lib/bds/rendering/labels.ex:42
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "January"
|
msgid "January"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:64
|
#: lib/bds/rendering/labels.ex:66
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "July"
|
msgid "July"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:60
|
#: lib/bds/rendering/labels.ex:62
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "June"
|
msgid "June"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:26
|
#: lib/bds/rendering/labels.ex:27
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Language"
|
msgid "Language"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:16
|
#: lib/bds/rendering/labels.ex:17
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Linked from"
|
msgid "Linked from"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:22
|
#: lib/bds/rendering/labels.ex:23
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Loading calendar…"
|
msgid "Loading calendar…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:48
|
#: lib/bds/rendering/labels.ex:50
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "March"
|
msgid "March"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:56
|
#: lib/bds/rendering/labels.ex:58
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "May"
|
msgid "May"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:80
|
#: lib/bds/rendering/labels.ex:82
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "November"
|
msgid "November"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:76
|
#: lib/bds/rendering/labels.ex:78
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "October"
|
msgid "October"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:21
|
#: lib/bds/rendering/labels.ex:22
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Open calendar"
|
msgid "Open calendar"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:18
|
#: lib/bds/rendering/labels.ex:19
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Pagination"
|
msgid "Pagination"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:28
|
#: lib/bds/rendering/labels.ex:29
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Search..."
|
msgid "Search..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:72
|
#: lib/bds/rendering/labels.ex:74
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "September"
|
msgid "September"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:27
|
#: lib/bds/rendering/labels.ex:28
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Site search"
|
msgid "Site search"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:14
|
#: lib/bds/rendering/labels.ex:15
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Taxonomy"
|
msgid "Taxonomy"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:19
|
#: lib/bds/rendering/labels.ex:20
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "newer"
|
msgid "newer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:20
|
#: lib/bds/rendering/labels.ex:21
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "older"
|
msgid "older"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:30
|
#: lib/bds/rendering/labels.ex:31
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Back to preview home"
|
msgid "Back to preview home"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:29
|
#: lib/bds/rendering/labels.ex:30
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "The requested preview page could not be found."
|
msgid "The requested preview page could not be found."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:32
|
#: lib/bds/rendering/labels.ex:33
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Vimeo video"
|
msgid "Vimeo video"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/bds/rendering/labels.ex:31
|
#: lib/bds/rendering/labels.ex:32
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "YouTube video"
|
msgid "YouTube video"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
1404
priv/gettext/ui.pot
1404
priv/gettext/ui.pot
File diff suppressed because it is too large
Load Diff
@@ -29,6 +29,7 @@ value SettingsProjectSection {
|
|||||||
blog_languages: List<String> -- checkboxes (main language disabled)
|
blog_languages: List<String> -- checkboxes (main language disabled)
|
||||||
default_author: String -- text input
|
default_author: String -- text input
|
||||||
max_posts_per_page: Integer -- number input (1-500, default 50)
|
max_posts_per_page: Integer -- number input (1-500, default 50)
|
||||||
|
image_import_concurrency: Integer -- number input (1-8, default 4)
|
||||||
blogmark_category: String -- select from categories
|
blogmark_category: String -- select from categories
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,6 +93,9 @@ invariant SettingsMCPAgents {
|
|||||||
config {
|
config {
|
||||||
settings_max_posts_per_page: Integer = 500
|
settings_max_posts_per_page: Integer = 500
|
||||||
settings_default_posts_per_page: Integer = 50
|
settings_default_posts_per_page: Integer = 50
|
||||||
|
settings_image_import_concurrency_default: Integer = 4
|
||||||
|
settings_image_import_concurrency_min: Integer = 1
|
||||||
|
settings_image_import_concurrency_max: Integer = 8
|
||||||
settings_system_prompt_rows: Integer = 12
|
settings_system_prompt_rows: Integer = 12
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,6 +135,7 @@ surface SettingsViewSurface {
|
|||||||
-- Section 1: Project Name, Description (textarea 3 rows), Data Path (text + Browse + Reset),
|
-- Section 1: Project Name, Description (textarea 3 rows), Data Path (text + Browse + Reset),
|
||||||
-- Public URL, Main Language (select), Blog Languages (checkbox grid, main disabled),
|
-- Public URL, Main Language (select), Blog Languages (checkbox grid, main disabled),
|
||||||
-- Default Author, Max Posts Per Page (number 1-500),
|
-- Default Author, Max Posts Per Page (number 1-500),
|
||||||
|
-- Image Import Concurrency (number 1-8, default 4),
|
||||||
-- Blogmark Category (select), Blogmark Bookmarklet (copy button), Save button.
|
-- Blogmark Category (select), Blogmark Bookmarklet (copy button), Save button.
|
||||||
|
|
||||||
@guarantee BookmarkletCopy
|
@guarantee BookmarkletCopy
|
||||||
|
|||||||
@@ -203,3 +203,27 @@ invariant SidecarRoundtrip {
|
|||||||
parse_sidecar(m.sidecar_path).caption = m.caption
|
parse_sidecar(m.sidecar_path).caption = m.caption
|
||||||
parse_sidecar(m.sidecar_path).tags = m.tags
|
parse_sidecar(m.sidecar_path).tags = m.tags
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rule BatchImportProcessLinkImages {
|
||||||
|
when: BatchImportImagesRequested(project, post, file_paths, language)
|
||||||
|
requires: not OfflineMode
|
||||||
|
for source_path in file_paths where is_image(source_path):
|
||||||
|
let media = ImportMedia(source_path, project)
|
||||||
|
let analysis = AnalyzeImage(media, language)
|
||||||
|
ensures: MediaUpdated(media, analysis)
|
||||||
|
ensures: PostMediaLinked(media, post)
|
||||||
|
@guidance
|
||||||
|
-- Triggered from post editor quick action "Add Gallery Images".
|
||||||
|
-- AI results auto-applied without user confirmation.
|
||||||
|
-- After metadata is set, media is auto-translated to all configured blog languages.
|
||||||
|
-- Non-image files skipped entirely.
|
||||||
|
-- Concurrency limit from project metadata image_import_concurrency (default 4, min 1, max 8).
|
||||||
|
-- Toast per completed image + final summary toast.
|
||||||
|
-- On completion: [[gallery]] macro inserted into post content and post editor refreshed.
|
||||||
|
}
|
||||||
|
|
||||||
|
config {
|
||||||
|
batch_image_import_concurrency_default: Integer = 4
|
||||||
|
batch_image_import_concurrency_min: Integer = 1
|
||||||
|
batch_image_import_concurrency_max: Integer = 8
|
||||||
|
}
|
||||||
|
|||||||
@@ -3998,7 +3998,7 @@ defmodule BDS.Desktop.ShellLiveTest do
|
|||||||
|> element("[data-testid='chat-send-button']")
|
|> element("[data-testid='chat-send-button']")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
send(view.pid, {
|
send_and_await(view, {
|
||||||
:chat_tool_call,
|
:chat_tool_call,
|
||||||
conversation.id,
|
conversation.id,
|
||||||
%{
|
%{
|
||||||
@@ -4245,14 +4245,14 @@ defmodule BDS.Desktop.ShellLiveTest do
|
|||||||
assert html =~ ~s(class="chat-input chat-surface-input ui-textarea")
|
assert html =~ ~s(class="chat-input chat-surface-input ui-textarea")
|
||||||
|
|
||||||
css = desktop_css_source()
|
css = desktop_css_source()
|
||||||
assert css =~ "--chat-input-line-height: 20px;"
|
assert css =~ "--chat-input-line-height: 22px;"
|
||||||
assert css =~ "--chat-input-min-height: 20px;"
|
assert css =~ "--chat-input-min-height: 24px;"
|
||||||
assert css =~ ".chat-panel .chat-input-container"
|
assert css =~ ".chat-panel .chat-input-container"
|
||||||
assert css =~ "padding: 8px 16px;"
|
assert css =~ "padding: 12px 16px;"
|
||||||
assert css =~ "padding: 6px 8px;"
|
assert css =~ "padding: 6px 8px;"
|
||||||
assert css =~ ".chat-panel .chat-input-wrapper"
|
assert css =~ ".chat-panel .chat-input-wrapper"
|
||||||
assert css =~ "min-height: 30px;"
|
assert css =~ "min-height: 40px;"
|
||||||
assert css =~ "padding: 4px 6px;"
|
assert css =~ "padding: 6px 8px;"
|
||||||
assert css =~ ".chat-panel .chat-input"
|
assert css =~ ".chat-panel .chat-input"
|
||||||
assert css =~ "box-sizing: border-box;"
|
assert css =~ "box-sizing: border-box;"
|
||||||
assert css =~ "margin: 0;"
|
assert css =~ "margin: 0;"
|
||||||
@@ -4565,7 +4565,7 @@ defmodule BDS.Desktop.ShellLiveTest do
|
|||||||
assert assistant_index < user_index
|
assert assistant_index < user_index
|
||||||
assert html =~ ~r/<textarea[^>]*class="chat-input chat-surface-input ui-textarea"[^>]*disabled/
|
assert html =~ ~r/<textarea[^>]*class="chat-input chat-surface-input ui-textarea"[^>]*disabled/
|
||||||
|
|
||||||
send(view.pid, {
|
send_and_await(view, {
|
||||||
:chat_tool_call,
|
:chat_tool_call,
|
||||||
conversation.id,
|
conversation.id,
|
||||||
%{
|
%{
|
||||||
@@ -4629,11 +4629,13 @@ defmodule BDS.Desktop.ShellLiveTest do
|
|||||||
|> element(".chat-input-wrapper")
|
|> element(".chat-input-wrapper")
|
||||||
|> render_change(%{"message" => "Newest question"})
|
|> render_change(%{"message" => "Newest question"})
|
||||||
|
|
||||||
_html =
|
html =
|
||||||
view
|
view
|
||||||
|> element("[data-testid='chat-send-button']")
|
|> element("[data-testid='chat-send-button']")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
|
assert html =~ ~s(data-testid="chat-streaming-thinking")
|
||||||
|
|
||||||
assert Enum.count(AI.list_chat_messages(conversation.id), fn message ->
|
assert Enum.count(AI.list_chat_messages(conversation.id), fn message ->
|
||||||
message.role == :user and message.content == "Newest question"
|
message.role == :user and message.content == "Newest question"
|
||||||
end) == 1
|
end) == 1
|
||||||
@@ -4660,7 +4662,9 @@ defmodule BDS.Desktop.ShellLiveTest do
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
send(view.pid, {
|
_ensure_sync = render(view)
|
||||||
|
|
||||||
|
send_and_await(view, {
|
||||||
:chat_tool_call,
|
:chat_tool_call,
|
||||||
conversation.id,
|
conversation.id,
|
||||||
%{
|
%{
|
||||||
@@ -4931,6 +4935,20 @@ defmodule BDS.Desktop.ShellLiveTest do
|
|||||||
run_git!(project_dir, ["commit", "-m", message])
|
run_git!(project_dir, ["commit", "-m", message])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Sends a message to the LiveView and blocks until it has been processed.
|
||||||
|
# Uses a test ping/pong to guarantee the message was handled before returning.
|
||||||
|
defp send_and_await(view, message) do
|
||||||
|
ref = make_ref()
|
||||||
|
send(view.pid, message)
|
||||||
|
send(view.pid, {:test_ping, self(), ref})
|
||||||
|
|
||||||
|
receive do
|
||||||
|
{:test_pong, ^ref} -> :ok
|
||||||
|
after
|
||||||
|
5000 -> raise "LiveView did not process messages within 5s"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp run_git!(dir, args) do
|
defp run_git!(dir, args) do
|
||||||
{output, status} = System.cmd("git", args, cd: dir, stderr_to_stdout: true)
|
{output, status} = System.cmd("git", args, cd: dir, stderr_to_stdout: true)
|
||||||
|
|
||||||
|
|||||||
252
test/bds/gallery_pipeline_test.exs
Normal file
252
test/bds/gallery_pipeline_test.exs
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
defmodule BDS.GalleryPipelineTest do
|
||||||
|
use ExUnit.Case, async: false
|
||||||
|
|
||||||
|
alias BDS.{Repo, Media}
|
||||||
|
|
||||||
|
setup do
|
||||||
|
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Repo)
|
||||||
|
Ecto.Adapters.SQL.Sandbox.mode(Repo, {:shared, self()})
|
||||||
|
|
||||||
|
temp_dir =
|
||||||
|
Path.join(System.tmp_dir!(), "bds-gallery-test-#{System.unique_integer([:positive])}")
|
||||||
|
|
||||||
|
File.mkdir_p!(temp_dir)
|
||||||
|
on_exit(fn -> File.rm_rf(temp_dir) end)
|
||||||
|
|
||||||
|
{:ok, project} = BDS.Projects.create_project(%{name: "Gallery Test", data_path: temp_dir})
|
||||||
|
{:ok, _project} = BDS.Projects.set_active_project(project.id)
|
||||||
|
|
||||||
|
{:ok, _metadata} =
|
||||||
|
BDS.Metadata.update_project_metadata(project.id, %{
|
||||||
|
blog_languages: ["de", "fr"],
|
||||||
|
main_language: "en"
|
||||||
|
})
|
||||||
|
|
||||||
|
{:ok, post} =
|
||||||
|
BDS.Posts.create_post(%{
|
||||||
|
project_id: project.id,
|
||||||
|
title: "Gallery Post",
|
||||||
|
content: "Content"
|
||||||
|
})
|
||||||
|
|
||||||
|
%{project: project, post: post, temp_dir: temp_dir}
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "GalleryImport.start/6 concurrency" do
|
||||||
|
test "processes all paths with a sliding window", %{project: project, post: post} do
|
||||||
|
parent = self()
|
||||||
|
concurrency = 2
|
||||||
|
|
||||||
|
paths =
|
||||||
|
Enum.map(1..5, fn i ->
|
||||||
|
path = Path.join(project.data_path, "image_#{i}.txt")
|
||||||
|
File.write!(path, "content #{i}")
|
||||||
|
path
|
||||||
|
end)
|
||||||
|
|
||||||
|
ref = make_ref()
|
||||||
|
send(parent, {:test_gallery_start, ref, paths, project.id, post.id, concurrency})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# ── gallery_count with [[gallery]] ─────────────────────────────────────────
|
||||||
|
|
||||||
|
describe "gallery_count" do
|
||||||
|
test "counts [[gallery]] macro as gallery content" do
|
||||||
|
form = %{"content" => "Some text\n\n[[gallery]]\n\nMore text"}
|
||||||
|
|
||||||
|
assert BDS.Desktop.ShellLive.PostEditor.PostMetadata.gallery_count(form) == 1
|
||||||
|
end
|
||||||
|
|
||||||
|
test "counts inline images as before" do
|
||||||
|
form = %{"content" => ""}
|
||||||
|
|
||||||
|
assert BDS.Desktop.ShellLive.PostEditor.PostMetadata.gallery_count(form) == 1
|
||||||
|
end
|
||||||
|
|
||||||
|
test "counts both [[gallery]] and inline images" do
|
||||||
|
form = %{"content" => "\n[[gallery]]"}
|
||||||
|
|
||||||
|
assert BDS.Desktop.ShellLive.PostEditor.PostMetadata.gallery_count(form) == 1
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns 0 when no gallery marks present" do
|
||||||
|
form = %{"content" => "Just plain text"}
|
||||||
|
|
||||||
|
assert BDS.Desktop.ShellLive.PostEditor.PostMetadata.gallery_count(form) == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
test "handles empty content" do
|
||||||
|
form = %{}
|
||||||
|
|
||||||
|
assert BDS.Desktop.ShellLive.PostEditor.PostMetadata.gallery_count(form) == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
test "counts multiple [[gallery]] occurrences" do
|
||||||
|
form = %{"content" => "[[gallery]] some text [[gallery]]"}
|
||||||
|
|
||||||
|
assert BDS.Desktop.ShellLive.PostEditor.PostMetadata.gallery_count(form) == 2
|
||||||
|
end
|
||||||
|
|
||||||
|
test "is case insensitive for [[gallery]]" do
|
||||||
|
form = %{"content" => "[[Gallery]]"}
|
||||||
|
|
||||||
|
assert BDS.Desktop.ShellLive.PostEditor.PostMetadata.gallery_count(form) == 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# ── Rendering macro smoke tests ────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe "rendering macros" do
|
||||||
|
test "[[gallery]] renders without linked media", %{project: project, post: post} do
|
||||||
|
{:ok, metadata} = BDS.Metadata.get_project_metadata(project.id)
|
||||||
|
language = metadata.main_language || "en"
|
||||||
|
|
||||||
|
template_context = BDS.Rendering.TemplateSelection.template_render_context(project.id)
|
||||||
|
|
||||||
|
result =
|
||||||
|
BDS.Rendering.Filters.render_markdown(
|
||||||
|
"[[gallery]]",
|
||||||
|
%{},
|
||||||
|
%{},
|
||||||
|
language,
|
||||||
|
template_context,
|
||||||
|
post.id
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result =~ "gallery-empty"
|
||||||
|
assert result =~ "macro-gallery"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "[[gallery]] renders linked media when present", %{project: project, post: post} do
|
||||||
|
source = Path.join(project.data_path, "gallery_test.jpg")
|
||||||
|
File.write!(source, "fake jpeg")
|
||||||
|
|
||||||
|
{:ok, media} =
|
||||||
|
Media.import_media(%{project_id: project.id, source_path: source})
|
||||||
|
|
||||||
|
{:ok, _updated} =
|
||||||
|
Media.update_media(media.id, %{
|
||||||
|
title: "Test Image",
|
||||||
|
alt: "Alt text",
|
||||||
|
caption: "Caption"
|
||||||
|
})
|
||||||
|
|
||||||
|
{:ok, _link} = Media.link_media_to_post(media.id, post.id)
|
||||||
|
|
||||||
|
{:ok, metadata} = BDS.Metadata.get_project_metadata(project.id)
|
||||||
|
language = metadata.main_language || "en"
|
||||||
|
template_context = BDS.Rendering.TemplateSelection.template_render_context(project.id)
|
||||||
|
|
||||||
|
result =
|
||||||
|
BDS.Rendering.Filters.render_markdown(
|
||||||
|
"[[gallery]]",
|
||||||
|
%{},
|
||||||
|
%{},
|
||||||
|
language,
|
||||||
|
template_context,
|
||||||
|
post.id
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result =~ "macro-gallery"
|
||||||
|
assert result =~ "gallery-item"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "[[tag_cloud]] renders without tags", %{project: project, post: post} do
|
||||||
|
{:ok, metadata} = BDS.Metadata.get_project_metadata(project.id)
|
||||||
|
language = metadata.main_language || "en"
|
||||||
|
|
||||||
|
template_context = BDS.Rendering.TemplateSelection.template_render_context(project.id)
|
||||||
|
|
||||||
|
result =
|
||||||
|
BDS.Rendering.Filters.render_markdown(
|
||||||
|
"[[tag_cloud]]",
|
||||||
|
%{},
|
||||||
|
%{},
|
||||||
|
language,
|
||||||
|
template_context,
|
||||||
|
post.id
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result =~ "tag-cloud-empty"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "[[photo_archive]] renders without media", %{project: project, post: post} do
|
||||||
|
{:ok, metadata} = BDS.Metadata.get_project_metadata(project.id)
|
||||||
|
language = metadata.main_language || "en"
|
||||||
|
|
||||||
|
template_context = BDS.Rendering.TemplateSelection.template_render_context(project.id)
|
||||||
|
|
||||||
|
result =
|
||||||
|
BDS.Rendering.Filters.render_markdown(
|
||||||
|
"[[photo_archive]]",
|
||||||
|
%{},
|
||||||
|
%{},
|
||||||
|
language,
|
||||||
|
template_context,
|
||||||
|
post.id
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result =~ "photo-archive-empty"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "[[photo_archive year=2024]] renders with date filter", %{project: project, post: post} do
|
||||||
|
{:ok, metadata} = BDS.Metadata.get_project_metadata(project.id)
|
||||||
|
language = metadata.main_language || "en"
|
||||||
|
|
||||||
|
template_context = BDS.Rendering.TemplateSelection.template_render_context(project.id)
|
||||||
|
|
||||||
|
result =
|
||||||
|
BDS.Rendering.Filters.render_markdown(
|
||||||
|
"[[photo_archive year=2024]]",
|
||||||
|
%{},
|
||||||
|
%{},
|
||||||
|
language,
|
||||||
|
template_context,
|
||||||
|
post.id
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result =~ "photo-archive"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "[[youtube id=abc123]] still works", %{project: project, post: post} do
|
||||||
|
{:ok, metadata} = BDS.Metadata.get_project_metadata(project.id)
|
||||||
|
language = metadata.main_language || "en"
|
||||||
|
|
||||||
|
template_context = BDS.Rendering.TemplateSelection.template_render_context(project.id)
|
||||||
|
|
||||||
|
result =
|
||||||
|
BDS.Rendering.Filters.render_markdown(
|
||||||
|
"[[youtube id=abc123]]",
|
||||||
|
%{},
|
||||||
|
%{},
|
||||||
|
language,
|
||||||
|
template_context,
|
||||||
|
post.id
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result =~ "youtube"
|
||||||
|
assert result =~ "abc123"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "[[vimeo id=456789]] still works", %{project: project, post: post} do
|
||||||
|
{:ok, metadata} = BDS.Metadata.get_project_metadata(project.id)
|
||||||
|
language = metadata.main_language || "en"
|
||||||
|
|
||||||
|
template_context = BDS.Rendering.TemplateSelection.template_render_context(project.id)
|
||||||
|
|
||||||
|
result =
|
||||||
|
BDS.Rendering.Filters.render_markdown(
|
||||||
|
"[[vimeo id=456789]]",
|
||||||
|
%{},
|
||||||
|
%{},
|
||||||
|
language,
|
||||||
|
template_context,
|
||||||
|
post.id
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result =~ "vimeo"
|
||||||
|
assert result =~ "456789"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
216
test/bds/image_import_pipeline_test.exs
Normal file
216
test/bds/image_import_pipeline_test.exs
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
defmodule BDS.ImageImportPipelineTest do
|
||||||
|
use ExUnit.Case, async: false
|
||||||
|
|
||||||
|
import Phoenix.ConnTest
|
||||||
|
import Phoenix.LiveViewTest
|
||||||
|
|
||||||
|
alias BDS.Desktop.{FilePicker, Overlay}
|
||||||
|
alias BDS.{Metadata, AI, Repo}
|
||||||
|
|
||||||
|
@endpoint BDS.Desktop.Endpoint
|
||||||
|
|
||||||
|
setup do
|
||||||
|
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Repo)
|
||||||
|
Ecto.Adapters.SQL.Sandbox.mode(Repo, {:shared, self()})
|
||||||
|
|
||||||
|
temp_dir =
|
||||||
|
Path.join(System.tmp_dir!(), "bds-image-import-#{System.unique_integer([:positive])}")
|
||||||
|
|
||||||
|
File.mkdir_p!(temp_dir)
|
||||||
|
on_exit(fn -> File.rm_rf(temp_dir) end)
|
||||||
|
|
||||||
|
{:ok, project} = BDS.Projects.create_project(%{name: "Image Import Test", data_path: temp_dir})
|
||||||
|
%{project: project, temp_dir: temp_dir}
|
||||||
|
end
|
||||||
|
|
||||||
|
# ── FilePicker multi-select parsing ────────────────────────────────────────
|
||||||
|
|
||||||
|
describe "FilePicker.choose_files/2" do
|
||||||
|
test "single selection returns a single-item list" do
|
||||||
|
# Simulate what osascript returns for regular choose file
|
||||||
|
result = FilePicker.parse_choose_files_result("/Users/test/image.png", false)
|
||||||
|
assert result == {:ok, "/Users/test/image.png"}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "multi selection parses newline-separated paths" do
|
||||||
|
result =
|
||||||
|
FilePicker.parse_choose_files_result(
|
||||||
|
"/Users/test/photo1.jpg\n/Users/test/photo2.png\n/Users/test/photo3.heic",
|
||||||
|
true
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result ==
|
||||||
|
{:ok, ["/Users/test/photo1.jpg", "/Users/test/photo2.png", "/Users/test/photo3.heic"]}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "multi selection filters out empty lines" do
|
||||||
|
result =
|
||||||
|
FilePicker.parse_choose_files_result(
|
||||||
|
"/Users/test/photo1.jpg\n\n/Users/test/photo2.png\n \n/Users/test/photo3.heic\n",
|
||||||
|
true
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result ==
|
||||||
|
{:ok, ["/Users/test/photo1.jpg", "/Users/test/photo2.png", "/Users/test/photo3.heic"]}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "multi selection with single file returns list with one element" do
|
||||||
|
result = FilePicker.parse_choose_files_result("/Users/test/photo1.jpg", true)
|
||||||
|
assert result == {:ok, ["/Users/test/photo1.jpg"]}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# ── Metadata image_import_concurrency ───────────────────────────────────────
|
||||||
|
|
||||||
|
describe "Metadata image_import_concurrency" do
|
||||||
|
test "defaults to 4 for new projects", %{project: project} do
|
||||||
|
{:ok, metadata} = Metadata.get_project_metadata(project.id)
|
||||||
|
assert metadata.image_import_concurrency == 4
|
||||||
|
end
|
||||||
|
|
||||||
|
test "clamps to minimum 1", %{project: project} do
|
||||||
|
{:ok, _metadata} =
|
||||||
|
Metadata.update_project_metadata(project.id, %{image_import_concurrency: 0})
|
||||||
|
|
||||||
|
{:ok, metadata} = Metadata.get_project_metadata(project.id)
|
||||||
|
assert metadata.image_import_concurrency == 1
|
||||||
|
end
|
||||||
|
|
||||||
|
test "clamps to maximum 8", %{project: project} do
|
||||||
|
{:ok, _metadata} =
|
||||||
|
Metadata.update_project_metadata(project.id, %{image_import_concurrency: 100})
|
||||||
|
|
||||||
|
{:ok, metadata} = Metadata.get_project_metadata(project.id)
|
||||||
|
assert metadata.image_import_concurrency == 8
|
||||||
|
end
|
||||||
|
|
||||||
|
test "persists and reads correctly", %{project: project} do
|
||||||
|
{:ok, _metadata} =
|
||||||
|
Metadata.update_project_metadata(project.id, %{image_import_concurrency: 3})
|
||||||
|
|
||||||
|
{:ok, metadata} = Metadata.get_project_metadata(project.id)
|
||||||
|
assert metadata.image_import_concurrency == 3
|
||||||
|
end
|
||||||
|
|
||||||
|
test "handles string input", %{project: project} do
|
||||||
|
{:ok, _metadata} =
|
||||||
|
Metadata.update_project_metadata(project.id, %{image_import_concurrency: "5"})
|
||||||
|
|
||||||
|
{:ok, metadata} = Metadata.get_project_metadata(project.id)
|
||||||
|
assert metadata.image_import_concurrency == 5
|
||||||
|
end
|
||||||
|
|
||||||
|
test "handles nil as default", %{project: project} do
|
||||||
|
{:ok, _metadata} =
|
||||||
|
Metadata.update_project_metadata(project.id, %{image_import_concurrency: nil})
|
||||||
|
|
||||||
|
{:ok, metadata} = Metadata.get_project_metadata(project.id)
|
||||||
|
assert metadata.image_import_concurrency == 4
|
||||||
|
end
|
||||||
|
|
||||||
|
test "reflects in form as string", %{project: project} do
|
||||||
|
{:ok, metadata} = Metadata.get_project_metadata(project.id)
|
||||||
|
|
||||||
|
form =
|
||||||
|
BDS.Desktop.ShellLive.SettingsEditor.ProjectSettings.project_form(metadata)
|
||||||
|
|
||||||
|
assert form["image_import_concurrency"] == "4"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# ── Overlay struct post_id ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe "Overlay insert_media" do
|
||||||
|
test "open(:post, :insert_media, context) includes post_id" do
|
||||||
|
context = %{
|
||||||
|
current_tab: %{
|
||||||
|
type: :post,
|
||||||
|
id: "post-uuid-123",
|
||||||
|
title: "Test Post",
|
||||||
|
subtitle: "draft"
|
||||||
|
},
|
||||||
|
media: [],
|
||||||
|
insert_media_title: "Insert Media"
|
||||||
|
}
|
||||||
|
|
||||||
|
overlay = Overlay.open(:post, :insert_media, context)
|
||||||
|
assert overlay.post_id == "post-uuid-123"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "post_id is nil when opened from non-post context" do
|
||||||
|
context = %{
|
||||||
|
current_tab: %{
|
||||||
|
type: :media,
|
||||||
|
id: "media-uuid-456",
|
||||||
|
title: "Test Media",
|
||||||
|
subtitle: "image/png"
|
||||||
|
},
|
||||||
|
media: [],
|
||||||
|
insert_media_title: "Insert Media"
|
||||||
|
}
|
||||||
|
|
||||||
|
overlay = Overlay.open(:media, :insert_media, context)
|
||||||
|
assert overlay == nil
|
||||||
|
end
|
||||||
|
|
||||||
|
test "set_search_query preserves post_id" do
|
||||||
|
overlay = %{
|
||||||
|
kind: :insert_media,
|
||||||
|
title: "Insert Media",
|
||||||
|
search_query: "",
|
||||||
|
results: [],
|
||||||
|
all_media: [],
|
||||||
|
post_id: "post-uuid-789"
|
||||||
|
}
|
||||||
|
|
||||||
|
result = Overlay.set_search_query(overlay, "search term")
|
||||||
|
assert result.post_id == "post-uuid-789"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# ── Airplane mode gating via shell_live event ─────────────────────────────
|
||||||
|
|
||||||
|
describe "overlay_add_images airplane mode gating" do
|
||||||
|
setup do
|
||||||
|
prev_auto = System.get_env("BDS_DESKTOP_AUTOMATION")
|
||||||
|
System.put_env("BDS_DESKTOP_AUTOMATION", "1")
|
||||||
|
|
||||||
|
on_exit(fn ->
|
||||||
|
if prev_auto,
|
||||||
|
do: System.put_env("BDS_DESKTOP_AUTOMATION", prev_auto),
|
||||||
|
else: System.delete_env("BDS_DESKTOP_AUTOMATION")
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "shows toast in airplane mode when Add Gallery Images is clicked", %{project: project} do
|
||||||
|
{:ok, _project} = BDS.Projects.set_active_project(project.id)
|
||||||
|
|
||||||
|
assert :ok = AI.set_airplane_mode(true)
|
||||||
|
|
||||||
|
{:ok, post} =
|
||||||
|
BDS.Posts.create_post(%{
|
||||||
|
project_id: project.id,
|
||||||
|
title: "Gallery Post",
|
||||||
|
content: "Content"
|
||||||
|
})
|
||||||
|
|
||||||
|
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
|
||||||
|
|
||||||
|
render_click(view, "pin_sidebar_item", %{
|
||||||
|
"route" => "post",
|
||||||
|
"id" => post.id,
|
||||||
|
"title" => post.title,
|
||||||
|
"subtitle" => "draft"
|
||||||
|
})
|
||||||
|
|
||||||
|
html =
|
||||||
|
view
|
||||||
|
|> element("[phx-click='add_gallery_images']")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
assert html =~ "Automatic AI actions stay gated by airplane mode"
|
||||||
|
|
||||||
|
assert :ok = AI.set_airplane_mode(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -138,6 +138,7 @@ defmodule BDS.Repo.SchemaMigrationTest do
|
|||||||
"title",
|
"title",
|
||||||
"model",
|
"model",
|
||||||
"copilot_session_id",
|
"copilot_session_id",
|
||||||
|
"surface_state",
|
||||||
"created_at",
|
"created_at",
|
||||||
"updated_at"
|
"updated_at"
|
||||||
],
|
],
|
||||||
|
|||||||
86
test/bds/translation_completeness_test.exs
Normal file
86
test/bds/translation_completeness_test.exs
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
defmodule BDS.TranslationCompletenessTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
|
@translated_locales ~w(de fr it es)
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Counts empty msgstr entries per non-English locale .po file and ensures
|
||||||
|
the count never increases. When a new msgid is added to the codebase its
|
||||||
|
translations MUST be provided for all supported locales; an empty msgstr
|
||||||
|
in de/fr/it/es causes this test to fail.
|
||||||
|
|
||||||
|
English files are excluded — gettext treats empty msgstr as "return msgid
|
||||||
|
unchanged" for the source language, which is the intended fallback.
|
||||||
|
|
||||||
|
The expected counts below represent current untranslated legacy entries
|
||||||
|
that must decrease over time, never increase. When the count decreases
|
||||||
|
because translations were added, update the expected count in this test.
|
||||||
|
"""
|
||||||
|
test "empty msgstr counts do not increase in non-English locale .po files" do
|
||||||
|
expected = %{
|
||||||
|
"de/default.po" => 0,
|
||||||
|
"de/render.po" => 0,
|
||||||
|
"de/ui.po" => 153,
|
||||||
|
"fr/default.po" => 0,
|
||||||
|
"fr/render.po" => 0,
|
||||||
|
"fr/ui.po" => 153,
|
||||||
|
"it/default.po" => 0,
|
||||||
|
"it/render.po" => 0,
|
||||||
|
"it/ui.po" => 153,
|
||||||
|
"es/default.po" => 0,
|
||||||
|
"es/render.po" => 0,
|
||||||
|
"es/ui.po" => 153
|
||||||
|
}
|
||||||
|
|
||||||
|
actual =
|
||||||
|
for locale <- @translated_locales,
|
||||||
|
domain <- ~w(ui default render) do
|
||||||
|
file = "priv/gettext/#{locale}/LC_MESSAGES/#{domain}.po"
|
||||||
|
|
||||||
|
if File.exists?(file) do
|
||||||
|
count = empty_msgstr_count(file)
|
||||||
|
key = "#{locale}/#{domain}.po"
|
||||||
|
{key, count}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|> Enum.reject(&is_nil/1)
|
||||||
|
|> Map.new()
|
||||||
|
|
||||||
|
for {file, expected_count} <- expected do
|
||||||
|
actual_count = Map.get(actual, file, 0)
|
||||||
|
|
||||||
|
assert actual_count <= expected_count,
|
||||||
|
"#{file}: empty msgstr count increased from #{expected_count} to #{actual_count}. " <>
|
||||||
|
"All new msgid entries MUST have translations for every supported locale (de, fr, it, es). " <>
|
||||||
|
"When the count decreases because translations were added, update the expected count in this test."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp empty_msgstr_count(file) do
|
||||||
|
file
|
||||||
|
|> File.read!()
|
||||||
|
|> String.split("\n")
|
||||||
|
|> Enum.reduce({nil, 0}, fn line, {current_msgid, count} ->
|
||||||
|
trimmed = String.trim(line)
|
||||||
|
|
||||||
|
case {trimmed, current_msgid} do
|
||||||
|
{"msgstr \"\"", nil} ->
|
||||||
|
{nil, count}
|
||||||
|
|
||||||
|
{"msgstr \"\"", msgid} ->
|
||||||
|
if msgid == "" do
|
||||||
|
{nil, count}
|
||||||
|
else
|
||||||
|
{nil, count + 1}
|
||||||
|
end
|
||||||
|
|
||||||
|
{"msgid \"" <> rest, _} ->
|
||||||
|
{String.trim_trailing(rest, "\""), count}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
{current_msgid, count}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|> elem(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user