Compare commits

...

12 Commits

Author SHA1 Message Date
d03d033548 fix: fixed shutdown race 2026-05-29 16:16:33 +02:00
74ceaeb971 fix: force full re-embed on explicit rebuild and degrade gracefully when embedding model is unavailable 2026-05-29 15:49:53 +02:00
61ff2a77c0 perf: A1-14b replace O(n^2) embedding snapshot with hnswlib HNSW index and debounced persistence 2026-05-29 15:36:13 +02:00
744f7543d7 perf: batch CPU embedding inference and add A1-14c Apple GPU (EMLX) spec gap 2026-05-29 14:43:39 +02:00
a1004d72bf fix: A1-14 real neural embeddings via Bumblebee multilingual-e5-small with Float32 BLOB vector cache 2026-05-29 14:04:51 +02:00
489d787306 fix: A1-13 wire git sidebar to BDS.Git with branch, changes, history, and actions 2026-05-29 13:25:32 +02:00
babae1838d fix: A1-12 functional client-side search with real PagefindUI and fragment index 2026-05-29 10:29:42 +02:00
5b619f492a fix: A1-11 graceful preview shutdown drains inflight requests before stopping 2026-05-29 09:49:54 +02:00
b3434b3054 fix: A1-10 write template file to disk on create instead of leaving file_path empty 2026-05-29 09:43:18 +02:00
5b21dcb17d fix: A1-9 replace native color input with 17-preset colour picker popover + custom hex 2026-05-29 09:28:57 +02:00
1f645f6e5e fix: stabilize atom-leak test by checking specific keys instead of global atom count 2026-05-29 09:19:13 +02:00
99d36e6e2f fix: A1-8 add Liquid/Lua validation gates before template and script publish 2026-05-29 09:16:07 +02:00
71 changed files with 4743 additions and 1231 deletions

View File

@@ -17,13 +17,15 @@ Gap categories: **SC** = spec correct, fix code | **CS** = code correct, update
| A1-5 | ~~Auto-save after 3000ms idle~~ | editor_post.allium:183-188 | PostEditor schedules auto-save via parent timer on dirty change | **Resolved:** 3000ms idle auto-save timer in Bridges, tab-switch save in ShellLive, cancel on manual save, 3 tests added | | A1-5 | ~~Auto-save after 3000ms idle~~ | editor_post.allium:183-188 | PostEditor schedules auto-save via parent timer on dirty change | **Resolved:** 3000ms idle auto-save timer in Bridges, tab-switch save in ShellLive, cancel on manual save, 3 tests added |
| A1-6 | ~~On-demand rendering in preview server~~ | preview.allium:53-93 | `Preview.Router` matches post/archive/home/language routes and renders on-demand via `Rendering` | **Resolved:** `Preview.Router` implements on-demand template rendering for post, archive, home, date, tag, category, page, and language-prefixed routes; static file fallback retained for non-HTML assets (pagefind, feeds); 6 tests added | | A1-6 | ~~On-demand rendering in preview server~~ | preview.allium:53-93 | `Preview.Router` matches post/archive/home/language routes and renders on-demand via `Rendering` | **Resolved:** `Preview.Router` implements on-demand template rendering for post, archive, home, date, tag, category, page, and language-prefixed routes; static file fallback retained for non-HTML assets (pagefind, feeds); 6 tests added |
| A1-7 | ~~Template lookup must use all 4 levels (post→tag→category→default)~~ | template_context.allium:267-277 | `resolve_post_template_slug/3` implements tag→category cascade; all callers (preview, generation) updated | **Resolved:** `resolve_post_template_slug/3` in template_selection.ex, callers in preview.ex, router.ex, outputs.ex updated, 8 tests added | | A1-7 | ~~Template lookup must use all 4 levels (post→tag→category→default)~~ | template_context.allium:267-277 | `resolve_post_template_slug/3` implements tag→category cascade; all callers (preview, generation) updated | **Resolved:** `resolve_post_template_slug/3` in template_selection.ex, callers in preview.ex, router.ex, outputs.ex updated, 8 tests added |
| A1-8 | `ValidateLiquid`/`ValidateScript` before publish | template.allium:110, script.allium:165 | No validation gate before publish | Fix code: add validation step before publish | | A1-8 | ~~`ValidateLiquid`/`ValidateScript` before publish~~ | template.allium:110, script.allium:165 | `publish_template` validates Liquid via `Liquex.parse`, `publish_script` validates Lua via `BDS.Scripting.validate` | **Resolved:** validation gates added to `publish_template/1` and `publish_script/1`, invalid content returns `{:error, {:invalid_liquid|:invalid_script, reason}}`, 4 tests added |
| A1-9 | 17 preset colors + custom hex in tag picker | editor_tags.allium | Native `<input type="color">`, no preset palette | Fix code: implement preset color palette popover | | A1-9 | ~~17 preset colors + custom hex in tag picker~~ | editor_tags.allium | `ColourPicker` hook + popover with 17 preset swatches grid and custom hex input, wired to both create and edit forms | **Resolved:** replaced native `<input type="color">` with `ColourPickerPopover` component (17 presets, custom hex #RRGGBB, immediate selection), JS hook for click-away dismiss, 1 test added |
| A1-10 | Template file written on create | engine_side_effects.allium:151-153 | Draft templates have `file_path=""` | Fix code: write template file on create | | A1-10 | ~~Template file written on create~~ | engine_side_effects.allium:151-153 | `create_template` now computes `file_path` and writes template file with YAML frontmatter on create | **Resolved:** `create_template/1` writes `templates/{slug}.liquid` on create, `next_template_file_path` always computes path, 1 test added |
| A1-11 | Graceful shutdown with inflight request tracking | preview.allium:47-48 | Kills acceptor process, no inflight tracking | Fix code: track inflight requests, drain before shutdown | | A1-11 | ~~Graceful shutdown with inflight request tracking~~ | preview.allium:47-48 | `stop_preview` now closes the listener, parks the reply, and drains monitored inflight request tasks before reporting stopped | **Resolved:** acceptor transfers socket ownership to each request task; GenServer monitors inflight tasks, `begin_graceful_stop` stops accepting and finalizes via `:DOWN`/`:drain_timeout` (5s force-kill cap), 1 test added |
| A1-12 | Real Pagefind integration for search | generation.allium:208 | Stub only: `pagefind-ui.js` is one-liner, `PagefindUI` never defined, search-runtime.js silently bails, client-side search non-functional | Fix code: bundle real Pagefind, build proper fragment index, wire PagefindUI | | A1-12 | ~~Real Pagefind integration for search~~ | generation.allium:208 | Functional client-side search: `PagefindUI` defined in bundled `pagefind-ui.js`, fragment index records url/title/body-scoped text per page, search-runtime wires it up | **Resolved:** bundled real `PagefindUI` (fetch index, ranked full-text match, highlighted excerpts) + `pagefind-ui.css` as local assets read into `Pagefind`; index scoped to `data-pagefind-body` (unmarked pages excluded per PagefindHtmlMarking), title from `<title>`/`<h1>`; localized "No results found" label via `data-search-no-results` (de/fr/it/es); 3 unit tests added |
| A1-13 | Git sidebar shows only "Working tree" placeholder | sidebar_views.allium:651-770 | `sidebar.ex:782-798` returns single entity_list item; `BDS.Git` has full status/diff/commit/history/fetch/pull/push/prune_lfs but sidebar doesn't use it | Fix code: wire sidebar `git_view/0` to `BDS.Git` — render branch, ahead/behind, status file list, commit input, history entries, action buttons per spec | | A1-13 | ~~Git sidebar shows only "Working tree" placeholder~~ | sidebar_views.allium:651-770 | `git_view/1` now builds a full `layout: "git"` view from `BDS.Git` (repository/remote_state/status/history); `SidebarComponents` renders active + not_a_repo states | **Resolved:** `git_view/1` in sidebar.ex assembles branch/upstream/ahead/behind, status files, paginated history (20/page); `render_git_sidebar` renders branch header, sync legend, fetch/pull/push/prune-lfs buttons, commit form, clickable status files (open git_diff), history entries; shell_live wires `git_commit` (closes git_diff tabs), `git_fetch`/`git_pull`/`git_push`/`git_prune_lfs`, `git_initialize`; `BDS.Git.history` enriched with author/date, `BDS.Git.set_remote/2` added; i18n for de/fr/it/es; 3 shell tests + git author/date assertions added |
| A1-14 | Embedding uses TF-IDF hash projection instead of real neural model | embedding.allium:44-53, invariants ModelCaching/VectorCacheInDb | `backends/in_app.ex` hashes terms into sparse vectors via `:erlang.phash2`; no ONNX model, no `"query: "` prefix, no mean pooling, vectors stored as JSON text not Float32Array BLOB, snapshot-based neighbor lookup instead of USearch HNSW index | Fix code: (1) add Bumblebee + ONNX runtime deps to run `Xenova/multilingual-e5-small`, (2) implement lazy model download + cache in app data dir, (3) `"query: "` prefix + mean pooling + L2 norm in backend, (4) store vectors as binary BLOB (1536 bytes), (5) replace JSON snapshot with USearch HNSW index (cosine, M=16, ef=128/64, 5s debounce), (6) cross-language semantic similarity must work | | A1-14 | ~~Embedding uses TF-IDF hash projection instead of real neural model~~ | embedding.allium:44-53, invariants RealNeuralModel/ModelCaching/VectorCacheInDb | `Backends.Neural` runs `intfloat/multilingual-e5-small` (e5 weights behind the Xenova id) via Bumblebee+EXLA | **Resolved (core):** added bumblebee/nx/exla deps; `Backends.Neural` is a lazily-loaded GenServer that builds the Bumblebee text-embedding serving on first request (`"query: "` prefix + mean pooling + L2 norm), downloads+caches the model under the app data dir (ModelCaching), and is wired into the supervision tree when configured; vectors now persisted as packed little-endian Float32 BLOB (384×4=1536 bytes) instead of JSON text (VectorCacheInDb) with migration recreating `embedding_keys.vector` as BLOB; `InApp` demoted to documented offline/test stub; test config uses the stub so the suite stays offline; spec EmbeddingModel clarified (Xenova id ↔ intfloat weights via Bumblebee); batched inference via optional `embed_many/2` backend callback (configurable `batch_size`/`sequence_length`; rebuild/index/repair embed in chunks instead of one post at a time) + `NativeAcceleratedExecution` invariant added to spec; 4 tests added (BLOB round-trip, batched-rebuild, Neural model_info/behaviour). **Deferred:** A1-14b USearch HNSW index, A1-14c Apple GPU (EMLX). |
| A1-14b | ~~USearch HNSW ANN index + debounced persistence not implemented~~ | embedding.allium config/FindSimilar/DebouncedPersistence | `Embeddings.Index` is now an HNSW (hnswlib) ANN index with debounced persistence | **Resolved:** rewrote `Embeddings.Index` as a DB-free GenServer wrapping an hnswlib HNSW graph (cosine, M=16, efConstruction=128, efSearch=64) — O(n·log n) build, O(log n) queries, replacing the O(n²) JSON cosine snapshot; per-project in-memory index + `label→post_id` map; 5s debounced `save_index` + `.meta.json` sidecar, force-save on project switch (`set_active_project`) and shutdown (`terminate`), `forget/1` on project delete; lazy reload from disk with rebuild-from-DB self-heal on miss; `find_similar`/`find_duplicates`/`compute_similarities` rewired (no brute-force fallback); USearch has no Elixir binding so hnswlib provides the identical HNSW algorithm/params (spec reconciled); supervision + dialyzer PLT updated; tests updated for debounced/binary persistence + self-heal. Follow-up hardening: explicit rebuild now forces re-embedding regardless of content_hash (ReindexAll), and model-unavailable errors propagate cleanly (post saves degrade to unindexed + log; rebuild/index return `{:error, reason}` surfaced as a failed task with a user-facing message instead of crashing). |
| A1-14c | Embedding model runs on CPU only; no Apple GPU acceleration | embedding.allium invariant NativeAcceleratedExecution | `Backends.Neural` uses Bumblebee+EXLA; on Apple Silicon XLA has no Metal backend so inference is native CPU (batched). Apple GPU/Neural Engine unused | Fix code: spike an EMLX (Apple MLX) Nx backend so the model executes on the Apple Silicon GPU; gate by platform/availability with EXLA-CPU fallback; verify Bumblebee serving + defn compiler compatibility and benchmark vs CPU batching |
| A1-15 | ~~Preview vs generation content source strategy undocumented~~ | preview.allium (no invariant), generation.allium (no invariant) | Generation uses only published .md file content (`Generation.Data` snapshots set `content: nil`); preview includes published+draft posts and prefers DB content over file (`Preview.Router` queries `:published`/`:draft`, uses `editor_body`) | **Resolved:** added `PreviewDraftOverlay` invariant to preview.allium and `GenerationPublishedOnly` invariant to generation.allium; both cross-reference each other; code already correct, 3 tests added for draft-in-preview behavior | | A1-15 | ~~Preview vs generation content source strategy undocumented~~ | preview.allium (no invariant), generation.allium (no invariant) | Generation uses only published .md file content (`Generation.Data` snapshots set `content: nil`); preview includes published+draft posts and prefers DB content over file (`Preview.Router` queries `:published`/`:draft`, uses `editor_body`) | **Resolved:** added `PreviewDraftOverlay` invariant to preview.allium and `GenerationPublishedOnly` invariant to generation.allium; both cross-reference each other; code already correct, 3 tests added for draft-in-preview behavior |
### A2. Spec Should Update (code is normative) ### A2. Spec Should Update (code is normative)
@@ -184,7 +186,7 @@ All reconciled to follow code. Specs must be self-consistent and match code.
## Priority Order for Resolution ## Priority Order for Resolution
1. **A1-1 through A1-14** — code must follow spec (includes auto-save, on-demand preview, template lookup, validation gates, real Pagefind, graceful shutdown, real embedding model) 1. **A1-1 through A1-14c** — code must follow spec (includes auto-save, on-demand preview, template lookup, validation gates, real Pagefind, graceful shutdown, real embedding model, HNSW ANN index; only A1-14c = Apple GPU/EMLX acceleration still open)
2. **D1-1 through D1-18** — untested invariants/guarantees 2. **D1-1 through D1-18** — untested invariants/guarantees
3. **C-1 through C-3** — internal spec inconsistencies (reconcile to code) 3. **C-1 through C-3** — internal spec inconsistencies (reconcile to code)
4. **B1-1 through B1-6** — major code behaviors missing from spec 4. **B1-1 through B1-6** — major code behaviors missing from spec

View File

@@ -555,3 +555,88 @@
padding: 8px 12px; padding: 8px 12px;
} }
} }
/* Colour picker popover */
.colour-picker-wrap {
position: relative;
display: inline-flex;
}
.colour-picker-trigger {
width: 28px;
height: 28px;
border-radius: 4px;
border: 1px solid var(--vscode-input-border);
cursor: pointer;
padding: 0;
flex-shrink: 0;
}
.colour-picker-trigger:hover {
opacity: 0.85;
}
.colour-picker-popover {
position: absolute;
top: calc(100% + 4px);
left: 0;
z-index: 30;
padding: 8px;
border: 1px solid var(--vscode-dropdown-border, var(--vscode-panel-border));
border-radius: 6px;
background: var(--vscode-dropdown-background, var(--vscode-sideBar-background));
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
width: 196px;
}
.colour-picker-grid {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 4px;
}
.colour-picker-swatch {
width: 24px;
height: 24px;
border-radius: 4px;
border: 2px solid transparent;
cursor: pointer;
padding: 0;
transition: border-color 0.1s;
}
.colour-picker-swatch:hover {
border-color: var(--vscode-focusBorder);
}
.colour-picker-swatch.selected {
border-color: var(--vscode-focusBorder);
box-shadow: 0 0 0 1px var(--vscode-focusBorder);
}
.colour-picker-custom {
display: flex;
align-items: center;
gap: 6px;
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid var(--vscode-panel-border);
}
.colour-picker-custom label {
font-size: 11px;
color: var(--vscode-descriptionForeground);
white-space: nowrap;
}
.colour-picker-custom input {
flex: 1;
min-width: 0;
font-size: 12px;
padding: 2px 6px;
border: 1px solid var(--vscode-input-border);
border-radius: 3px;
background: var(--vscode-input-background);
color: var(--vscode-input-foreground);
font-family: monospace;
}

View File

@@ -0,0 +1,45 @@
export const ColourPicker = {
mounted() {
this._onClickAway = (e) => {
if (!this.el.contains(e.target)) {
this.el.querySelector(".colour-picker-popover")?.classList.add("hidden");
}
};
document.addEventListener("mousedown", this._onClickAway);
this._setupCustomInput();
},
updated() {
this._setupCustomInput();
},
destroyed() {
document.removeEventListener("mousedown", this._onClickAway);
},
_setupCustomInput() {
const input = this.el.querySelector(".colour-picker-custom input");
if (!input || input._cpBound) return;
input._cpBound = true;
const pushColor = () => {
let val = input.value.trim();
if (val && !val.startsWith("#")) val = "#" + val;
if (/^#[0-9a-fA-F]{6}$/.test(val)) {
const event = this.el.dataset.pickEvent;
this.pushEventTo(this.el.dataset.target, event, { color: val });
this.el.querySelector(".colour-picker-popover")?.classList.add("hidden");
}
};
input.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
e.preventDefault();
pushColor();
}
});
input.addEventListener("blur", pushColor);
}
};

View File

@@ -2,6 +2,7 @@ import { AppShell } from "./app_shell.js";
import { SidebarInteractions } from "./sidebar_interactions.js"; import { SidebarInteractions } from "./sidebar_interactions.js";
import { SettingsSectionScroll, TagsSectionScroll } from "./section_scroll.js"; import { SettingsSectionScroll, TagsSectionScroll } from "./section_scroll.js";
import { ChatSurface } from "./chat_surface.js"; import { ChatSurface } from "./chat_surface.js";
import { ColourPicker } from "./colour_picker.js";
import { MenuEditorTree } from "./menu_editor_tree.js"; import { MenuEditorTree } from "./menu_editor_tree.js";
import { MonacoEditor } from "./monaco_editor.js"; import { MonacoEditor } from "./monaco_editor.js";
import { MonacoDiffEditor } from "./monaco_diff_editor.js"; import { MonacoDiffEditor } from "./monaco_diff_editor.js";
@@ -12,6 +13,7 @@ export const Hooks = {
SettingsSectionScroll, SettingsSectionScroll,
TagsSectionScroll, TagsSectionScroll,
ChatSurface, ChatSurface,
ColourPicker,
MenuEditorTree, MenuEditorTree,
MonacoEditor, MonacoEditor,
MonacoDiffEditor MonacoDiffEditor

View File

@@ -61,9 +61,18 @@ config :bds, :scripting,
job_max_reductions: :none job_max_reductions: :none
config :bds, :embeddings, config :bds, :embeddings,
backend: BDS.Embeddings.Backends.InApp, backend: BDS.Embeddings.Backends.Neural,
model_id: "Xenova/multilingual-e5-small", model_id: "Xenova/multilingual-e5-small",
dimensions: 384 model_repo: "intfloat/multilingual-e5-small",
dimensions: 384,
# Inference is batched: batch_size texts per compiled run, truncated to
# sequence_length tokens. Tuning these trades throughput against memory.
batch_size: 16,
sequence_length: 256
# Cache downloaded model files under the app data directory so they persist
# across sessions (ModelCaching invariant). Overridden at runtime in prod.
config :bumblebee, :cache_dir, Path.expand("../priv/data/models", __DIR__)
config :logger, :console, config :logger, :console,
format: "$time $metadata[$level] $message\n", format: "$time $metadata[$level] $message\n",

View File

@@ -8,4 +8,9 @@ if config_env() == :prod do
config :bds, BDS.Repo, config :bds, BDS.Repo,
database: database_path, database: database_path,
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "1") pool_size: String.to_integer(System.get_env("POOL_SIZE") || "1")
# Persist downloaded embedding model files alongside the database data dir.
config :bumblebee, :cache_dir,
System.get_env("BDS_MODEL_CACHE_DIR") ||
Path.join(Path.dirname(Path.expand(database_path)), "models")
end end

View File

@@ -8,3 +8,13 @@ config :bds, BDS.Repo,
busy_timeout: 15_000 busy_timeout: 15_000
config :logger, level: :warning config :logger, level: :warning
# Tests use the deterministic lexical stub backend so the suite stays offline
# and never downloads the ~100 MB neural model.
config :bds, :embeddings,
backend: BDS.Embeddings.Backends.InApp,
model_id: "Xenova/multilingual-e5-small",
model_repo: "intfloat/multilingual-e5-small",
dimensions: 384,
batch_size: 16,
sequence_length: 256

View File

@@ -37,14 +37,24 @@ defmodule BDS.Application do
{Task.Supervisor, name: BDS.TCP.TaskSupervisor}, {Task.Supervisor, name: BDS.TCP.TaskSupervisor},
BDS.Scripting.JobStore, BDS.Scripting.JobStore,
{Task.Supervisor, name: BDS.Scripting.TaskSupervisor}, {Task.Supervisor, name: BDS.Scripting.TaskSupervisor},
BDS.Scripting.JobSupervisor BDS.Scripting.JobSupervisor,
| desktop_children(current_env()) BDS.Embeddings.Index
] ] ++ embedding_children() ++ desktop_children(current_env())
opts = [strategy: :one_for_one, name: BDS.Supervisor] opts = [strategy: :one_for_one, name: BDS.Supervisor]
Supervisor.start_link(children, opts) Supervisor.start_link(children, opts)
end end
# The neural embedding backend runs as a supervised, lazily-initialised
# GenServer (it loads the model only on the first embedding request). Only
# start it when it is the configured backend.
defp embedding_children do
case Application.get_env(:bds, :embeddings, [])[:backend] do
BDS.Embeddings.Backends.Neural -> [BDS.Embeddings.Backends.Neural]
_other -> []
end
end
defp current_env do defp current_env do
Application.get_env(:bds, :current_env_override) || @compiled_env Application.get_env(:bds, :current_env_override) || @compiled_env
end end

View File

@@ -120,16 +120,7 @@ defmodule BDS.Desktop.ShellCommands do
"rebuild_embedding_index", "rebuild_embedding_index",
"Rebuild Embedding Index", "Rebuild Embedding Index",
"Embeddings", "Embeddings",
fn report -> fn report -> rebuild_embedding_index_work(project, report) end
{:ok, rebuilt_post_ids} = Embeddings.rebuild_project(project.id, on_progress: report)
report.(1.0, "Embedding index rebuilt")
%{
project_id: project.id,
rebuilt_post_ids: rebuilt_post_ids,
rebuilt_count: length(rebuilt_post_ids)
}
end
) )
end end
@@ -524,8 +515,14 @@ defmodule BDS.Desktop.ShellCommands do
}, },
%{ %{
name: "Rebuild Embedding Index", name: "Rebuild Embedding Index",
work: fn report -> work: fn report -> rebuild_embedding_index_work(project, report) end
{:ok, rebuilt_post_ids} = Embeddings.rebuild_project(project.id, on_progress: report) }
]
end
defp rebuild_embedding_index_work(project, report) do
case Embeddings.rebuild_project(project.id, on_progress: report) do
{:ok, rebuilt_post_ids} ->
report.(1.0, "Embedding index rebuilt") report.(1.0, "Embedding index rebuilt")
%{ %{
@@ -533,9 +530,22 @@ defmodule BDS.Desktop.ShellCommands do
rebuilt_post_ids: rebuilt_post_ids, rebuilt_post_ids: rebuilt_post_ids,
rebuilt_count: length(rebuilt_post_ids) rebuilt_count: length(rebuilt_post_ids)
} }
{:error, reason} ->
{:error, embedding_error_message(reason)}
end end
} end
]
defp embedding_error_message(reason) do
detail =
case reason do
message when is_binary(message) -> message
{:embedding_backend_unavailable, _inner} -> "the embedding service did not start"
other -> inspect(other)
end
"Could not build the embedding index: #{detail}. The model is downloaded on first use, " <>
"so check your internet connection — or turn off semantic similarity in Settings."
end end
defp run_rebuild_sequence(_group_id, _attrs, []), do: :ok defp run_rebuild_sequence(_group_id, _attrs, []), do: :ok

View File

@@ -84,6 +84,8 @@ defmodule BDS.Desktop.ShellLive do
"load_more_sidebar" "load_more_sidebar"
] ]
@git_action_events ["git_fetch", "git_pull", "git_push", "git_prune_lfs"]
@layout_menu_actions MapSet.new([ @layout_menu_actions MapSet.new([
:toggle_sidebar, :toggle_sidebar,
:toggle_panel, :toggle_panel,
@@ -192,7 +194,8 @@ defmodule BDS.Desktop.ShellLive do
end end
def handle_event("toggle_assistant_sidebar", _params, socket) do def handle_event("toggle_assistant_sidebar", _params, socket) do
{:noreply, refresh_layout(socket, Workbench.toggle_assistant_sidebar(socket.assigns.workbench))} {:noreply,
refresh_layout(socket, Workbench.toggle_assistant_sidebar(socket.assigns.workbench))}
end end
def handle_event("select_view", %{"view" => view_id}, socket) do def handle_event("select_view", %{"view" => view_id}, socket) do
@@ -237,6 +240,20 @@ defmodule BDS.Desktop.ShellLive do
SidebarEvents.handle(socket, event, params, &refresh_sidebar/2) SidebarEvents.handle(socket, event, params, &refresh_sidebar/2)
end end
def handle_event(event, _params, socket) when event in @git_action_events do
{:noreply, run_git_action(socket, event)}
end
def handle_event("git_commit", params, socket) do
message = params |> get_in(["git", "message"]) |> to_string() |> String.trim()
{:noreply, commit_git(socket, message)}
end
def handle_event("git_initialize", params, socket) do
remote_url = params |> get_in(["git", "remote_url"]) |> normalize_git_remote_url()
{:noreply, initialize_git(socket, remote_url)}
end
def handle_event("create_sidebar_item", %{"kind" => kind}, socket) do def handle_event("create_sidebar_item", %{"kind" => kind}, socket) do
{:noreply, create_sidebar_item(socket, kind)} {:noreply, create_sidebar_item(socket, kind)}
end end
@@ -424,7 +441,9 @@ defmodule BDS.Desktop.ShellLive do
Task.Supervisor.start_child(BDS.TCP.TaskSupervisor, fn -> Task.Supervisor.start_child(BDS.TCP.TaskSupervisor, fn ->
case FilePicker.choose_files(dgettext("ui", "Add Gallery Images"), case FilePicker.choose_files(dgettext("ui", "Add Gallery Images"),
image_only: true, multiple: true) do image_only: true,
multiple: true
) do
{:ok, paths} when is_list(paths) and paths != [] -> {:ok, paths} when is_list(paths) and paths != [] ->
GalleryImport.start(paths, project_id, post_id, language, concurrency_limit, parent) GalleryImport.start(paths, project_id, post_id, language, concurrency_limit, parent)
@@ -623,7 +642,13 @@ defmodule BDS.Desktop.ShellLive do
def handle_info({:add_image_processed, title}, socket) do def handle_info({:add_image_processed, title}, socket) do
{:noreply, {:noreply,
append_output_entry(socket, dgettext("ui", "Add Gallery Images"), dgettext("ui", "Added %{title}", title: title), nil, "info")} append_output_entry(
socket,
dgettext("ui", "Add Gallery Images"),
dgettext("ui", "Added %{title}", title: title),
nil,
"info"
)}
end end
def handle_info({:add_images_complete, count}, socket) do def handle_info({:add_images_complete, count}, socket) do
@@ -660,7 +685,13 @@ defmodule BDS.Desktop.ShellLive do
def handle_info({:add_images_error, reason}, socket) do def handle_info({:add_images_error, reason}, socket) do
{:noreply, {:noreply,
append_output_entry(socket, dgettext("ui", "Add Gallery Images"), inspect(reason), nil, "error")} append_output_entry(
socket,
dgettext("ui", "Add Gallery Images"),
inspect(reason),
nil,
"error"
)}
end end
def handle_info({:add_image_error, path, reason}, socket) do def handle_info({:add_image_error, path, reason}, socket) do
@@ -668,7 +699,10 @@ defmodule BDS.Desktop.ShellLive do
append_output_entry( append_output_entry(
socket, socket,
dgettext("ui", "Add Gallery Images"), dgettext("ui", "Add Gallery Images"),
dgettext("ui", "Failed to process %{path}: %{reason}", path: Path.basename(path), reason: inspect(reason)), dgettext("ui", "Failed to process %{path}: %{reason}",
path: Path.basename(path),
reason: inspect(reason)
),
nil, nil,
"error" "error"
)} )}
@@ -696,13 +730,17 @@ defmodule BDS.Desktop.ShellLive do
defp refresh_layout(socket, workbench) do defp refresh_layout(socket, workbench) do
git_badge_count = socket.assigns[:git_badge_count] || 0 git_badge_count = socket.assigns[:git_badge_count] || 0
activity_buttons = Workbench.activity_buttons(workbench, git_badge_count) activity_buttons = Workbench.activity_buttons(workbench, git_badge_count)
task_status = socket.assigns[:task_status] || %{running_task_message: nil, running_task_overflow: nil}
task_status =
socket.assigns[:task_status] || %{running_task_message: nil, running_task_overflow: nil}
dashboard = socket.assigns[:dashboard] || BDS.UI.Dashboard.empty_snapshot() dashboard = socket.assigns[:dashboard] || BDS.UI.Dashboard.empty_snapshot()
page_language = socket.assigns[:page_language] || ShellData.ui_language() page_language = socket.assigns[:page_language] || ShellData.ui_language()
offline_mode = Map.get(socket.assigns, :offline_mode, true) offline_mode = Map.get(socket.assigns, :offline_mode, true)
sidebar_data = socket.assigns[:sidebar_data] || %{} sidebar_data = socket.assigns[:sidebar_data] || %{}
current_tab = current_tab(workbench) current_tab = current_tab(workbench)
prev_tab = socket.assigns[:current_tab] prev_tab = socket.assigns[:current_tab]
prev_panel_tab = prev_panel_tab =
case socket.assigns[:workbench] do case socket.assigns[:workbench] do
%Workbench{panel: %{active_tab: tab}} -> tab %Workbench{panel: %{active_tab: tab}} -> tab
@@ -1017,6 +1055,122 @@ defmodule BDS.Desktop.ShellLive do
|> push_url_state() |> push_url_state()
end end
defp run_git_action(socket, event) do
project_id = current_project_id(socket)
{label, result} =
case event do
"git_fetch" -> {dgettext("ui", "Fetch"), git_call(project_id, &BDS.Git.fetch/1)}
"git_pull" -> {dgettext("ui", "Pull"), git_call(project_id, &BDS.Git.pull/1)}
"git_push" -> {dgettext("ui", "Push"), git_call(project_id, &BDS.Git.push/1)}
"git_prune_lfs" -> {dgettext("ui", "Prune LFS"), prune_lfs(project_id)}
end
socket
|> append_git_result(label, result)
|> refresh_sidebar(socket.assigns.workbench)
end
defp commit_git(socket, "") do
socket
|> append_output_entry(
dgettext("ui", "Commit"),
dgettext("ui", "Commit message is required"),
nil,
"error"
)
|> refresh_sidebar(socket.assigns.workbench)
end
defp commit_git(socket, message) do
case git_call(current_project_id(socket), &BDS.Git.commit_all(&1, message)) do
{:ok, _result} ->
workbench = close_git_diff_tabs(socket.assigns.workbench)
tab_meta = TabHelpers.sync_tab_meta(workbench, socket.assigns[:tab_meta] || %{})
socket
|> assign(:tab_meta, tab_meta)
|> append_output_entry(dgettext("ui", "Commit"), message)
|> refresh_sidebar(workbench)
|> push_url_state()
{:error, reason} ->
socket
|> append_output_entry(dgettext("ui", "Commit"), format_git_error(reason), nil, "error")
|> refresh_sidebar(socket.assigns.workbench)
end
end
defp initialize_git(socket, remote_url) do
project_id = current_project_id(socket)
case git_call(project_id, &BDS.Git.initialize_repo/1) do
{:ok, _repo} ->
_ = maybe_set_git_remote(project_id, remote_url)
socket
|> append_output_entry(
dgettext("ui", "Initialize Git"),
dgettext("ui", "Repository initialized")
)
|> refresh_sidebar(socket.assigns.workbench)
{:error, reason} ->
socket
|> append_output_entry(
dgettext("ui", "Initialize Git"),
format_git_error(reason),
nil,
"error"
)
|> refresh_sidebar(socket.assigns.workbench)
end
end
defp git_call(nil, _fun), do: {:error, :no_project}
defp git_call("default", _fun), do: {:error, :no_project}
defp git_call(project_id, fun) when is_binary(project_id), do: fun.(project_id)
defp prune_lfs(nil), do: {:error, :no_project}
defp prune_lfs("default"), do: {:error, :no_project}
defp prune_lfs(project_id) when is_binary(project_id),
do: BDS.Git.prune_lfs_cache(project_id, 10)
defp maybe_set_git_remote(_project_id, nil), do: :ok
defp maybe_set_git_remote(project_id, remote_url),
do: BDS.Git.set_remote(project_id, remote_url)
defp append_git_result(socket, label, {:ok, _result}) do
append_output_entry(socket, label, dgettext("ui", "Done"))
end
defp append_git_result(socket, label, {:error, reason}) do
append_output_entry(socket, label, format_git_error(reason), nil, "error")
end
defp format_git_error(:no_project), do: dgettext("ui", "No active project")
defp format_git_error(%{message: message}) when is_binary(message), do: message
defp format_git_error(%{guidance: guidance}) when is_binary(guidance), do: guidance
defp format_git_error({:git_failed, message}) when is_binary(message), do: message
defp format_git_error(reason), do: inspect(reason)
defp close_git_diff_tabs(workbench) do
workbench.tabs
|> Enum.filter(&(&1.type == :git_diff))
|> Enum.reduce(workbench, fn tab, wb -> Workbench.close_tab(wb, :git_diff, tab.id) end)
end
defp current_project_id(socket), do: (socket.assigns[:projects] || %{})[:active_project_id]
defp normalize_git_remote_url(value) do
case value |> to_string() |> String.trim() do
"" -> nil
url -> url
end
end
defp sidebar_create_action(view), do: SidebarCreate.action(view) defp sidebar_create_action(view), do: SidebarCreate.action(view)
defp set_page_language(socket, language) do defp set_page_language(socket, language) do

View File

@@ -257,6 +257,7 @@ defmodule BDS.Desktop.ShellLive.SidebarComponents do
"media_grid" -> render_media_sidebar(assigns) "media_grid" -> render_media_sidebar(assigns)
"entity_list" -> render_entity_sidebar(assigns) "entity_list" -> render_entity_sidebar(assigns)
"nav_list" -> render_nav_sidebar(assigns) "nav_list" -> render_nav_sidebar(assigns)
"git" -> render_git_sidebar(assigns)
_other -> render_default_sidebar(assigns) _other -> render_default_sidebar(assigns)
end end
end end
@@ -483,6 +484,141 @@ defmodule BDS.Desktop.ShellLive.SidebarComponents do
""" """
end end
defp render_git_sidebar(assigns) do
assigns = assign(assigns, :git_state, Map.get(assigns.sidebar_data, :git_state, "not_a_repo"))
~H"""
<div class="git-sidebar">
<%= if @git_state == "active" do %>
<%= render_git_active(assigns) %>
<% else %>
<%= render_git_not_a_repo(assigns) %>
<% end %>
</div>
"""
end
defp render_git_not_a_repo(assigns) do
~H"""
<section class="git-section git-not-a-repo">
<p class="git-empty-hint"><%= dgettext("ui", "This project is not a Git repository yet.") %></p>
<form class="git-init-form flex flex-col gap-2" data-testid="git-init-form" phx-submit="git_initialize">
<input
type="text"
name="git[remote_url]"
placeholder={dgettext("ui", "Remote URL (optional)")}
value={Map.get(@sidebar_data, :remote_url) || ""}
/>
<button class="git-action-button" data-testid="git-initialize" type="submit">
<%= dgettext("ui", "Initialize Git") %>
</button>
</form>
</section>
"""
end
defp render_git_active(assigns) do
~H"""
<header class="git-header">
<div class="git-branch-row flex items-center gap-2">
<span class="git-branch-icon">⎇</span>
<span class="git-branch" data-testid="git-branch"><%= @sidebar_data.branch %></span>
<%= if @sidebar_data.upstream do %>
<span class="git-upstream" data-testid="git-upstream"><%= @sidebar_data.upstream %></span>
<% end %>
</div>
<div class="git-tracking flex items-center gap-3">
<span class="git-ahead" data-testid="git-ahead" title={dgettext("ui", "Ahead")}>↑ <%= @sidebar_data.ahead %></span>
<span class="git-behind" data-testid="git-behind" title={dgettext("ui", "Behind")}>↓ <%= @sidebar_data.behind %></span>
</div>
<div class="git-sync-legend flex items-center gap-3">
<span class="git-legend-item"><span class="git-sync-dot git-sync-synced"></span><%= dgettext("ui", "Synced") %></span>
<span class="git-legend-item"><span class="git-sync-dot git-sync-local_only"></span><%= dgettext("ui", "Local only") %></span>
<span class="git-legend-item"><span class="git-sync-dot git-sync-remote_only"></span><%= dgettext("ui", "Remote only") %></span>
</div>
</header>
<div class="git-actions flex items-center gap-2">
<button class="git-action-button" data-testid="git-action-fetch" type="button" phx-click="git_fetch" title={dgettext("ui", "Fetch")}><%= dgettext("ui", "Fetch") %></button>
<button class="git-action-button" data-testid="git-action-pull" type="button" phx-click="git_pull" title={dgettext("ui", "Pull")}><%= dgettext("ui", "Pull") %></button>
<button class="git-action-button" data-testid="git-action-push" type="button" phx-click="git_push" title={dgettext("ui", "Push")}><%= dgettext("ui", "Push") %></button>
<button class="git-action-button" data-testid="git-action-prune-lfs" type="button" phx-click="git_prune_lfs" title={dgettext("ui", "Prune LFS")}><%= dgettext("ui", "Prune LFS") %></button>
</div>
<section class="git-section git-changes">
<div class="git-section-title">
<span><%= dgettext("ui", "Changes") %></span>
<span class="git-section-count"><%= length(@sidebar_data.status_files) %></span>
</div>
<form class="git-commit-form flex flex-col gap-2" data-testid="git-commit-form" phx-submit="git_commit">
<input type="text" name="git[message]" placeholder={dgettext("ui", "Commit message")} />
<button class="git-action-button" data-testid="git-commit" type="submit"><%= dgettext("ui", "Commit") %></button>
</form>
<%= if Enum.any?(@sidebar_data.status_files) do %>
<div class="git-status-list flex flex-col">
<%= for file <- @sidebar_data.status_files do %>
<button
class="git-status-file flex items-center justify-between gap-2"
data-testid="git-status-file"
data-route="git_diff"
type="button"
title={"#{file.label}: #{file.path}"}
phx-click="open_sidebar_item"
phx-value-route="git_diff"
phx-value-id={"git-diff:" <> file.path}
phx-value-title={file.path}
phx-value-subtitle={file.label}
>
<span class="git-status-path"><%= file.path %></span>
<span class={"git-status-badge git-status-#{file.status}"}><%= file.code %></span>
</button>
<% end %>
</div>
<% else %>
<p class="git-empty-hint"><%= dgettext("ui", "No changes") %></p>
<% end %>
</section>
<section class="git-section git-history">
<div class="git-section-title">
<span><%= dgettext("ui", "History") %></span>
</div>
<%= if Enum.any?(@sidebar_data.history_entries) do %>
<div class="git-history-list flex flex-col">
<%= for entry <- @sidebar_data.history_entries do %>
<button
class="git-history-entry flex flex-col"
data-testid="git-history-entry"
data-route="git_diff"
type="button"
phx-click="open_sidebar_item"
phx-value-route="git_diff"
phx-value-id={"git-diff:commit:" <> entry.short_hash}
phx-value-title={entry.short_hash}
phx-value-subtitle={entry.subject || ""}
>
<span class="git-history-subject"><%= entry.subject %></span>
<span class="git-history-meta flex items-center gap-2">
<span class={"git-sync-dot git-sync-#{entry.sync_status}"}></span>
<span class="git-history-hash"><%= entry.short_hash %></span>
<%= if entry.author do %><span class="git-history-author"><%= entry.author %></span><% end %>
<%= if entry.date do %><span class="git-history-date"><%= entry.date %></span><% end %>
</span>
</button>
<% end %>
</div>
<%= if @sidebar_data.has_more_history do %>
<p class="git-history-more"><%= dgettext("ui", "Older history available") %></p>
<% end %>
<% else %>
<p class="git-empty-hint"><%= dgettext("ui", "No commits yet") %></p>
<% end %>
</section>
"""
end
defp render_default_sidebar(assigns) do defp render_default_sidebar(assigns) do
~H""" ~H"""
<%= for section <- Map.get(@sidebar_data, :sections, []) do %> <%= for section <- Map.get(@sidebar_data, :sections, []) do %>

View File

@@ -16,6 +16,16 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do
@tags_sections ~w(cloud manage merge) @tags_sections ~w(cloud manage merge)
@colour_presets ~w(
#ef4444 #f97316 #f59e0b #eab308 #84cc16
#22c55e #10b981 #14b8a6 #06b6d4 #0ea5e9
#3b82f6 #6366f1 #8b5cf6 #a855f7 #d946ef
#ec4899 #64748b
)
@spec colour_presets() :: [String.t()]
def colour_presets, do: @colour_presets
@spec update(map(), Phoenix.LiveView.Socket.t()) :: {:ok, Phoenix.LiveView.Socket.t()} @spec update(map(), Phoenix.LiveView.Socket.t()) :: {:ok, Phoenix.LiveView.Socket.t()}
@impl true @impl true
def update(%{action: :save} = assigns, socket) do def update(%{action: :save} = assigns, socket) do
@@ -107,6 +117,26 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do
{:noreply, assign(socket, :tags_editor, tags_editor)} {:noreply, assign(socket, :tags_editor, tags_editor)}
end end
def handle_event("pick_new_tag_color", %{"color" => color}, socket) do
tags_editor =
Map.put(socket.assigns.tags_editor, :new_tag, %{
socket.assigns.tags_editor.new_tag
| "color" => color
})
{:noreply, assign(socket, :tags_editor, tags_editor)}
end
def handle_event("pick_edit_tag_color", %{"color" => color}, socket) do
tags_editor =
Map.put(socket.assigns.tags_editor, :edit_draft, %{
socket.assigns.tags_editor.edit_draft
| "color" => color
})
{:noreply, assign(socket, :tags_editor, tags_editor)}
end
def handle_event("save_tag_editor", _params, socket) do def handle_event("save_tag_editor", _params, socket) do
{:noreply, do_save(socket)} {:noreply, do_save(socket)}
end end
@@ -241,6 +271,55 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do
end end
end end
attr :color, :string, default: nil
attr :presets, :list, required: true
attr :pick_event, :string, required: true
attr :target, :any, required: true
defp colour_picker(assigns) do
~H"""
<div
class="colour-picker-wrap"
id={"cp-#{@pick_event}"}
phx-hook="ColourPicker"
data-pick-event={@pick_event}
data-target={if @target, do: @target.cid}
>
<button
type="button"
class="colour-picker-trigger"
style={"background-color: #{if @color in [nil, ""], do: "#3b82f6", else: @color}"}
phx-click={Phoenix.LiveView.JS.toggle(to: "#cp-#{@pick_event} .colour-picker-popover")}
/>
<div class="colour-picker-popover hidden">
<div class="colour-picker-grid">
<%= for preset <- @presets do %>
<button
type="button"
class={"colour-picker-swatch#{if normalize_hex(@color) == normalize_hex(preset), do: " selected", else: ""}"}
style={"background-color: #{preset}"}
phx-click={Phoenix.LiveView.JS.push(@pick_event, value: %{color: preset}, target: if(@target, do: @target.cid)) |> Phoenix.LiveView.JS.add_class("hidden", to: "#cp-#{@pick_event} .colour-picker-popover")}
/>
<% end %>
</div>
<div class="colour-picker-custom">
<label>#</label>
<input
type="text"
maxlength="7"
placeholder="RRGGBB"
value={if @color in [nil, ""], do: "", else: @color}
/>
</div>
</div>
</div>
"""
end
defp normalize_hex(nil), do: nil
defp normalize_hex(""), do: nil
defp normalize_hex(hex), do: String.downcase(hex)
defp load_data(socket) do defp load_data(socket) do
project_id = socket.assigns.project_id project_id = socket.assigns.project_id
@@ -280,7 +359,8 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do
merge_target: merge_target:
Map.get(socket.assigns, :tags_editor, %{}) Map.get(socket.assigns, :tags_editor, %{})
|> Map.get(:merge_target, List.first(selected) || ""), |> Map.get(:merge_target, List.first(selected) || ""),
selected_section: selected_section selected_section: selected_section,
colour_presets: @colour_presets
} }
assign(socket, :tags_editor, data) assign(socket, :tags_editor, data)

View File

@@ -38,7 +38,13 @@
<form class="tag-create-form" phx-change="change_new_tag_editor" phx-target={@myself}> <form class="tag-create-form" phx-change="change_new_tag_editor" phx-target={@myself}>
<div class="tag-form-row flex flex-wrap items-center gap-3"> <div class="tag-form-row flex flex-wrap items-center gap-3">
<input class="ui-input" type="text" name="new_tag[name]" value={@tags_editor.new_tag["name"]} placeholder={dgettext("ui", "Tag name")} /> <input class="ui-input" type="text" name="new_tag[name]" value={@tags_editor.new_tag["name"]} placeholder={dgettext("ui", "Tag name")} />
<input type="color" name="new_tag[color]" value={if(@tags_editor.new_tag["color"] in [nil, ""], do: "#3b82f6", else: @tags_editor.new_tag["color"])} /> <input type="hidden" name="new_tag[color]" value={@tags_editor.new_tag["color"] || ""} />
<.colour_picker
color={@tags_editor.new_tag["color"]}
presets={@tags_editor.colour_presets}
pick_event="pick_new_tag_color"
target={@myself}
/>
<button class="primary ui-button ui-button-primary" type="button" phx-click="create_tag_editor" phx-target={@myself}><%= dgettext("ui", "Create") %></button> <button class="primary ui-button ui-button-primary" type="button" phx-click="create_tag_editor" phx-target={@myself}><%= dgettext("ui", "Create") %></button>
</div> </div>
</form> </form>
@@ -47,7 +53,13 @@
<form class="tag-edit-form" phx-change="change_edit_tag_editor" phx-target={@myself}> <form class="tag-edit-form" phx-change="change_edit_tag_editor" phx-target={@myself}>
<div class="tag-form-row flex flex-wrap items-center gap-3"> <div class="tag-form-row flex flex-wrap items-center gap-3">
<input class="ui-input" type="text" name="edit_tag[name]" value={@tags_editor.edit_draft["name"]} /> <input class="ui-input" type="text" name="edit_tag[name]" value={@tags_editor.edit_draft["name"]} />
<input type="color" name="edit_tag[color]" value={if(@tags_editor.edit_draft["color"] in [nil, ""], do: "#3b82f6", else: @tags_editor.edit_draft["color"])} /> <input type="hidden" name="edit_tag[color]" value={@tags_editor.edit_draft["color"] || ""} />
<.colour_picker
color={@tags_editor.edit_draft["color"]}
presets={@tags_editor.colour_presets}
pick_event="pick_edit_tag_color"
target={@myself}
/>
<select class="ui-input" name="edit_tag[post_template_slug]"> <select class="ui-input" name="edit_tag[post_template_slug]">
<option value=""><%= dgettext("ui", "No Template") %></option> <option value=""><%= dgettext("ui", "No Template") %></option>
<%= for template <- @tags_editor.templates do %> <%= for template <- @tags_editor.templates do %>

View File

@@ -61,9 +61,26 @@ defmodule BDS.Desktop.Shutdown do
def command_menu_selected(_event, _command_event), do: :ok def command_menu_selected(_event, _command_event), do: :ok
@doc """
Terminate the OS process directly with SIGKILL.
`Desktop.Window.quit/0` routes through `System.halt/1`, which calls the libc
`exit()` and runs the wxWidgets C++ static destructors on the way out. On
macOS that races the still-running wx event loop on the main thread and
segfaults (`wxMenu::~wxMenu` vs `wxAppBase::ProcessIdle`). A SIGKILL is a
kernel-level termination that skips those destructors entirely, so the app
exits cleanly without producing a crash report.
"""
@spec quit() :: :ok
def quit do
kill_heart()
kill_beam()
:ok
end
defp start_shutdown_task do defp start_shutdown_task do
Task.start(fn -> Task.start(fn ->
MainWindow.persist_now() persist_safely()
maybe_hide_window() maybe_hide_window()
Process.sleep(50) Process.sleep(50)
quit_module().quit() quit_module().quit()
@@ -72,6 +89,57 @@ defmodule BDS.Desktop.Shutdown do
:ok :ok
end end
defp persist_safely do
MainWindow.persist_now()
:ok
rescue
_error -> :ok
catch
:exit, _reason -> :ok
end
# heart, when present, would relaunch the app after we kill the BEAM, so it
# has to be terminated first. When the app is started without heart (e.g. via
# `mix`) there is nothing to do here.
defp kill_heart do
with heart when is_pid(heart) <- Process.whereis(:heart),
{:links, links} <- Process.info(heart, :links),
port when is_port(port) <- Enum.find(links, &is_port/1),
{:os_pid, heart_pid} <- Port.info(port, :os_pid) do
os_kill(heart_pid)
else
_ -> :ok
end
rescue
_error -> :ok
catch
:exit, _reason -> :ok
end
defp kill_beam do
os_kill(:os.getpid())
end
defp os_kill(os_pid) do
os_kill_fun().(os_pid)
:ok
rescue
_error -> :ok
catch
:exit, _reason -> :ok
end
defp os_kill_fun do
Application.get_env(:bds, :desktop_os_kill_fun, &__MODULE__.hard_kill/1)
end
@doc false
@spec hard_kill(charlist() | integer() | String.t()) :: :ok
def hard_kill(os_pid) do
System.cmd("kill", ["-9", to_string(os_pid)], stderr_to_stdout: true)
:ok
end
defp maybe_hide_window do defp maybe_hide_window do
module = window_module() module = window_module()
@@ -86,8 +154,10 @@ defmodule BDS.Desktop.Shutdown do
:exit, _reason -> :ok :exit, _reason -> :ok
end end
defp quit_module do @doc false
Application.get_env(:bds, :desktop_window_quit_module, Window) @spec quit_module() :: module()
def quit_module do
Application.get_env(:bds, :desktop_window_quit_module, __MODULE__)
end end
defp window_module do defp window_module do

View File

@@ -2,6 +2,7 @@ defmodule BDS.Embeddings do
@moduledoc false @moduledoc false
import Ecto.Query import Ecto.Query
require Logger
alias BDS.Persistence alias BDS.Persistence
alias BDS.Embeddings.DismissedDuplicatePair alias BDS.Embeddings.DismissedDuplicatePair
@@ -75,25 +76,16 @@ defmodule BDS.Embeddings do
) )
existing_keys = preload_keys_by_post_id(project_id, Enum.map(posts, & &1.id)) existing_keys = preload_keys_by_post_id(project_id, Enum.map(posts, & &1.id))
base_label = max_label_value()
{rows, _next_label} =
Enum.reduce(posts, {[], base_label + 1}, fn post, {acc, next_label} ->
existing_key = Map.get(existing_keys, post.id)
case compute_key_data(post, existing_key, next_label) do
:skip ->
{acc, next_label}
{:upsert, row} ->
bump = if existing_key, do: 0, else: 1
{[row | acc], next_label + bump}
end
end)
case build_key_rows(posts, existing_keys, max_label_value(), nil, false) do
{:ok, rows} ->
batch_upsert_keys(rows) batch_upsert_keys(rows)
:ok = rebuild_snapshot(project_id) :ok = rebuild_snapshot(project_id)
{:ok, Enum.map(posts, & &1.id)} {:ok, Enum.map(posts, & &1.id)}
{:error, _reason} = error ->
error
end
else else
{:ok, []} {:ok, []}
end end
@@ -113,9 +105,6 @@ defmodule BDS.Embeddings do
) )
post_ids = Enum.map(posts, & &1.id) post_ids = Enum.map(posts, & &1.id)
total_posts = length(posts)
:ok = report_rebuild_started(on_progress, total_posts, "embedding entries")
Repo.delete_all( Repo.delete_all(
from key in Key, from key in Key,
@@ -123,30 +112,19 @@ defmodule BDS.Embeddings do
) )
existing_keys = preload_keys_by_post_id(project_id) existing_keys = preload_keys_by_post_id(project_id)
base_label = max_label_value()
{rows, _next_label} =
posts
|> Enum.with_index(1)
|> Enum.reduce({[], base_label + 1}, fn {post, index}, {acc, next_label} ->
:ok = report_rebuild_progress(on_progress, index, total_posts, "embedding entries")
existing_key = Map.get(existing_keys, post.id)
case compute_key_data(post, existing_key, next_label) do
:skip ->
{acc, next_label}
{:upsert, row} ->
bump = if existing_key, do: 0, else: 1
{[row | acc], next_label + bump}
end
end)
# An explicit rebuild re-embeds every post from scratch (ReindexAll),
# ignoring the content_hash skip optimisation.
case build_key_rows(posts, existing_keys, max_label_value(), on_progress, true) do
{:ok, rows} ->
batch_upsert_keys(rows) batch_upsert_keys(rows)
:ok = report_rebuild_phase(on_progress, 0.99, "Persisting embedding snapshot") :ok = report_rebuild_phase(on_progress, 0.99, "Persisting embedding snapshot")
:ok = rebuild_snapshot(project_id) :ok = rebuild_snapshot(project_id)
{:ok, post_ids} {:ok, post_ids}
{:error, _reason} = error ->
error
end
else else
{:ok, []} {:ok, []}
end end
@@ -200,16 +178,15 @@ defmodule BDS.Embeddings do
case Repo.get_by(Key, post_id: post.id, project_id: post.project_id) do case Repo.get_by(Key, post_id: post.id, project_id: post.project_id) do
%Key{content_hash: ^content_hash} -> %Key{content_hash: ^content_hash} ->
if Keyword.get(opts, :refresh_index, true) and # Embedding is already current. The HNSW index self-heals on query
snapshot_content_hash(post.project_id, post.id) != content_hash do # (find_similar/find_duplicates rebuild when no index is loaded), so
:ok = rebuild_snapshot(post.project_id) # there is nothing to refresh here.
end
:ok :ok
existing_key -> existing_key ->
case embed_text(raw_text, post.language) do
{:ok, vector} ->
label = existing_key_label(existing_key) || next_label() label = existing_key_label(existing_key) || next_label()
{:ok, vector} = embed_text(raw_text, post.language)
(existing_key || %Key{}) (existing_key || %Key{})
|> Key.changeset(%{ |> Key.changeset(%{
@@ -217,7 +194,7 @@ defmodule BDS.Embeddings do
post_id: post.id, post_id: post.id,
project_id: post.project_id, project_id: post.project_id,
content_hash: content_hash, content_hash: content_hash,
vector: Jason.encode!(vector) vector: encode_vector(vector)
}) })
|> Repo.insert_or_update() |> Repo.insert_or_update()
@@ -226,6 +203,17 @@ defmodule BDS.Embeddings do
end end
:ok :ok
{:error, reason} ->
# Embedding is best-effort on post save: if the model is unavailable
# (e.g. offline first-use download), leave the post unindexed rather
# than failing the save. An explicit reindex surfaces the error.
Logger.warning(
"Embedding unavailable for post #{post.id}: #{inspect(reason)}; left unindexed"
)
:ok
end
end end
end end
@@ -246,17 +234,100 @@ defmodule BDS.Embeddings do
Repo.one(from key in Key, select: max(key.label)) || 0 Repo.one(from key in Key, select: max(key.label)) || 0
end end
defp compute_key_data(%Post{} = post, existing_key, next_label) do # Builds the upsert rows for a batch of posts. Unless `force?` is set, posts
body = resolve_post_body(post) # whose content_hash is unchanged are skipped (ContentHashSkipsUnchanged); the
raw_text = compose_embedding_source(post.title, body) # rest are embedded in batches (see embed_pending/2) so model inference is not
# serialised one post at a time. Labels keep their existing value or take the
# next free integer. Returns `{:error, reason}` if the model is unavailable.
defp build_key_rows(posts, existing_keys, base_label, on_progress, force?) do
prepared =
Enum.map(posts, fn post ->
raw_text = compose_embedding_source(post.title, resolve_post_body(post))
existing = Map.get(existing_keys, post.id)
content_hash = hash_text(raw_text) content_hash = hash_text(raw_text)
if existing_key && existing_key.content_hash == content_hash do %{
:skip post: post,
existing: existing,
raw_text: raw_text,
content_hash: content_hash,
needs_embed?: force? or is_nil(existing) or existing.content_hash != content_hash
}
end)
pending = Enum.filter(prepared, & &1.needs_embed?)
:ok = report_rebuild_started(on_progress, length(pending), "embedding entries")
case embed_pending(pending, on_progress) do
{:ok, vectors_by_post_id} -> {:ok, collect_rows(prepared, vectors_by_post_id, base_label)}
{:error, _reason} = error -> error
end
end
defp collect_rows(prepared, vectors_by_post_id, base_label) do
{rows, _next_label} =
Enum.reduce(prepared, {[], base_label + 1}, fn entry, {acc, next_label} ->
if entry.needs_embed? do
vector = Map.fetch!(vectors_by_post_id, entry.post.id)
label = if entry.existing, do: entry.existing.label, else: next_label
bump = if entry.existing, do: 0, else: 1
row = [
label,
entry.post.id,
entry.post.project_id,
entry.content_hash,
encode_vector(vector)
]
{[row | acc], next_label + bump}
else else
{:ok, vector} = embed_text(raw_text, post.language) {acc, next_label}
label = if existing_key, do: existing_key.label, else: next_label end
{:upsert, [label, post.id, post.project_id, content_hash, Jason.encode!(vector)]} end)
rows
end
defp embed_pending([], _on_progress), do: {:ok, %{}}
defp embed_pending(pending, on_progress) do
total = length(pending)
batch = batch_size()
pending
# Group by language so the lexical stub stems consistently; the neural
# backend is multilingual and ignores the language hint.
|> Enum.group_by(& &1.post.language)
|> Enum.reduce_while({%{}, 0}, fn {language, group}, acc ->
group
|> Enum.chunk_every(batch)
|> Enum.reduce_while(acc, fn chunk, {vectors, done} ->
case embed_many(Enum.map(chunk, & &1.raw_text), language) do
{:ok, chunk_vectors} ->
vectors =
chunk
|> Enum.zip(chunk_vectors)
|> Enum.reduce(vectors, fn {entry, vector}, acc ->
Map.put(acc, entry.post.id, vector)
end)
done = done + length(chunk)
:ok = report_rebuild_progress(on_progress, done, total, "embedding entries")
{:cont, {vectors, done}}
{:error, reason} ->
{:halt, {:error, reason}}
end
end)
|> case do
{:error, reason} -> {:halt, {:error, reason}}
accumulator -> {:cont, accumulator}
end
end)
|> case do
{:error, reason} -> {:error, reason}
{vectors, _done} -> {:ok, vectors}
end end
end end
@@ -308,22 +379,9 @@ defmodule BDS.Embeddings do
) )
existing_keys = preload_keys_by_post_id(project_id) existing_keys = preload_keys_by_post_id(project_id)
base_label = max_label_value()
{rows, _next_label} =
Enum.reduce(posts, {[], base_label + 1}, fn post, {acc, next_label} ->
existing_key = Map.get(existing_keys, post.id)
case compute_key_data(post, existing_key, next_label) do
:skip ->
{acc, next_label}
{:upsert, row} ->
bump = if existing_key, do: 0, else: 1
{[row | acc], next_label + bump}
end
end)
case build_key_rows(posts, existing_keys, max_label_value(), nil, false) do
{:ok, rows} ->
batch_upsert_keys(rows) batch_upsert_keys(rows)
:ok = rebuild_snapshot(project_id) :ok = rebuild_snapshot(project_id)
@@ -331,6 +389,10 @@ defmodule BDS.Embeddings do
Repo.all(from key in Key, where: key.project_id == ^project_id, select: key.post_id) Repo.all(from key in Key, where: key.project_id == ^project_id, select: key.post_id)
{:ok, indexed} {:ok, indexed}
{:error, _reason} = error ->
error
end
else else
{:ok, []} {:ok, []}
end end
@@ -344,28 +406,28 @@ defmodule BDS.Embeddings do
{:error, :not_found} -> {:error, :not_found} ->
{:ok, []} {:ok, []}
{:ok, post, source_vector} -> {:ok, _post, nil} ->
similar = {:ok, []}
case Index.neighbors(post.project_id, post.id, limit) do
{:ok, post, %Key{} = key} ->
{:ok, query_similar(post.project_id, key, limit)}
end
end
# Queries the HNSW index for a post's neighbours, rebuilding the index from
# the DB vectors if it is not currently loaded (e.g. after a restart).
defp query_similar(project_id, %Key{} = key, limit) do
case Index.neighbors(project_id, key.label, key.vector, limit) do
{:ok, neighbors} -> {:ok, neighbors} ->
neighbors neighbors
{:error, :missing} -> {:error, :missing} ->
Repo.all( :ok = rebuild_snapshot(project_id)
from key in Key,
where: key.project_id == ^post.project_id and key.post_id != ^post.id
)
|> Enum.map(fn key ->
%{
post_id: key.post_id,
score: cosine_similarity(source_vector, decode_vector(key.vector))
}
end)
|> Enum.sort_by(& &1.score, :desc)
|> Enum.take(max(limit, 0))
end
{:ok, similar} case Index.neighbors(project_id, key.label, key.vector, limit) do
{:ok, neighbors} -> neighbors
{:error, :missing} -> []
end
end end
end end
@@ -378,8 +440,12 @@ defmodule BDS.Embeddings do
{:error, :not_found} -> {:error, :not_found} ->
{:ok, %{}} {:ok, %{}}
{:ok, post, source_vector} -> {:ok, _post, nil} ->
{:ok, %{}}
{:ok, post, %Key{} = source_key} ->
target_ids = Enum.uniq(target_post_ids) target_ids = Enum.uniq(target_post_ids)
source_vector = decode_vector(source_key.vector)
scores = scores =
Repo.all( Repo.all(
@@ -435,47 +501,19 @@ defmodule BDS.Embeddings do
if enabled_for_project?(project_id) do if enabled_for_project?(project_id) do
on_progress = progress_callback(opts) on_progress = progress_callback(opts)
dismissed = dismissed_pair_keys(project_id) dismissed = dismissed_pair_keys(project_id)
entries = load_index_entries(project_id)
pairs =
case duplicate_pairs_with_rebuild(project_id, entries, on_progress) do
{:ok, pairs} -> pairs
{:error, :missing} -> []
end
duplicates = duplicates =
case Index.duplicate_pairs(project_id, @duplicate_threshold, on_progress: on_progress) do
{:ok, pairs} ->
pairs pairs
|> Enum.reject(fn pair -> pair_key(pair.post_id_a, pair.post_id_b) in dismissed end) |> Enum.reject(fn pair -> pair_key(pair.post_id_a, pair.post_id_b) in dismissed end)
|> enrich_duplicate_pairs(project_id) |> enrich_duplicate_pairs(project_id)
{:error, :missing} ->
keys =
Repo.all(
from key in Key,
where: key.project_id == ^project_id,
order_by: [asc: key.post_id]
)
total_keys = length(keys)
:ok = report_rebuild_started(on_progress, total_keys, "embedding entries")
keys
|> Enum.with_index(1)
|> Enum.flat_map(fn {left, index} ->
:ok = report_rebuild_progress(on_progress, index, total_keys, "embedding entries")
for right <- keys,
left.post_id < right.post_id,
pair_key(left.post_id, right.post_id) not in dismissed,
similarity =
cosine_similarity(decode_vector(left.vector), decode_vector(right.vector)),
similarity >= @duplicate_threshold do
%{
post_id_a: left.post_id,
post_id_b: right.post_id,
score: similarity
}
end
end)
|> enrich_duplicate_pairs(project_id)
end
:ok = report_rebuild_phase(on_progress, 0.99, "Resolving duplicate candidates") :ok = report_rebuild_phase(on_progress, 0.99, "Resolving duplicate candidates")
{:ok, duplicates} {:ok, duplicates}
else else
@@ -538,17 +576,33 @@ defmodule BDS.Embeddings do
with {:ok, post} <- fetch_post(post_id) do with {:ok, post} <- fetch_post(post_id) do
if enabled_for_project?(post.project_id) do if enabled_for_project?(post.project_id) do
:ok = ensure_key(post) :ok = ensure_key(post)
{:ok, post, Repo.get_by(Key, post_id: post.id, project_id: post.project_id)}
case Repo.get_by(Key, post_id: post.id, project_id: post.project_id) do
nil -> {:ok, post, []}
key -> {:ok, post, decode_vector(key.vector)}
end
else else
{:disabled, post.project_id} {:disabled, post.project_id}
end end
end end
end end
defp duplicate_pairs_with_rebuild(project_id, entries, on_progress) do
case Index.duplicate_pairs(project_id, entries, @duplicate_threshold, on_progress: on_progress) do
{:ok, pairs} ->
{:ok, pairs}
{:error, :missing} ->
:ok = rebuild_snapshot(project_id)
Index.duplicate_pairs(project_id, entries, @duplicate_threshold, on_progress: on_progress)
end
end
defp load_index_entries(project_id) do
Repo.all(
from key in Key,
where: key.project_id == ^project_id,
order_by: [asc: key.post_id]
)
|> Enum.map(fn key -> %{label: key.label, post_id: key.post_id, vector: key.vector} end)
end
defp ensure_key(%Post{} = post) do defp ensure_key(%Post{} = post) do
case Repo.get_by(Key, post_id: post.id, project_id: post.project_id) do case Repo.get_by(Key, post_id: post.id, project_id: post.project_id) do
nil -> sync_post(post) nil -> sync_post(post)
@@ -655,11 +709,42 @@ defmodule BDS.Embeddings do
end end
defp embed_text(raw_text, language) do defp embed_text(raw_text, language) do
configured_backend().embed("query: " <> raw_text, language: language) # Per-backend preprocessing (e5 "query: " prefix, pooling, normalisation)
# is the backend's responsibility — see BDS.Embeddings.Backends.Neural.
configured_backend().embed(raw_text, language: language)
end
# Embeds a batch of texts in one shot. Backends that implement the optional
# embed_many/2 callback (e.g. the neural backend, which feeds them through the
# model as a single batched inference run) handle the whole list; others fall
# back to sequential single embeds.
defp embed_many(texts, language) do
backend = configured_backend()
if function_exported?(backend, :embed_many, 2) do
backend.embed_many(texts, language: language)
else
Enum.reduce_while(texts, {:ok, []}, fn text, {:ok, acc} ->
case backend.embed(text, language: language) do
{:ok, vector} -> {:cont, {:ok, [vector | acc]}}
{:error, _reason} = error -> {:halt, error}
end
end)
|> case do
{:ok, vectors} -> {:ok, Enum.reverse(vectors)}
{:error, _reason} = error -> error
end
end
end
defp batch_size do
Application.get_env(:bds, :embeddings, [])
|> Keyword.get(:batch_size, 16)
|> max(1)
end end
defp rebuild_snapshot(project_id) do defp rebuild_snapshot(project_id) do
Index.rebuild(project_id, model_id: model_id(), dimensions: dimensions()) Index.put(project_id, dimensions(), load_index_entries(project_id))
end end
defp progress_callback(opts), do: ProgressReporter.callback(opts) defp progress_callback(opts), do: ProgressReporter.callback(opts)
@@ -684,13 +769,6 @@ defmodule BDS.Embeddings do
defp report_rebuild_phase(callback, value, label), defp report_rebuild_phase(callback, value, label),
do: ProgressReporter.report_phase(callback, value, label) do: ProgressReporter.report_phase(callback, value, label)
defp snapshot_content_hash(project_id, post_id) do
case Index.read(project_id) do
{:ok, snapshot} -> get_in(snapshot, ["entries", post_id, "content_hash"])
_other -> nil
end
end
defp current_embedding_status(nil, _expected_hash), do: "missing" defp current_embedding_status(nil, _expected_hash), do: "missing"
defp current_embedding_status(%Key{vector: vector}, _expected_hash) when vector in [nil, ""], defp current_embedding_status(%Key{vector: vector}, _expected_hash) when vector in [nil, ""],
@@ -726,8 +804,22 @@ defmodule BDS.Embeddings do
defp hash_text(text), do: :crypto.hash(:sha256, text) |> Base.encode16(case: :lower) defp hash_text(text), do: :crypto.hash(:sha256, text) |> Base.encode16(case: :lower)
# Vectors are persisted as a packed little-endian Float32 BLOB
# (`dimensions` * 4 bytes; 1536 bytes for multilingual-e5-small) per the
# VectorCacheInDb invariant in specs/embedding.allium.
defp encode_vector(values) when is_list(values) do
for value <- values, into: <<>>, do: <<float32(value)::float-32-little>>
end
defp float32(value) when is_float(value), do: value
defp float32(value) when is_integer(value), do: value * 1.0
defp decode_vector(nil), do: [] defp decode_vector(nil), do: []
defp decode_vector(vector), do: Jason.decode!(vector) defp decode_vector(<<>>), do: []
defp decode_vector(binary) when is_binary(binary) do
for <<value::float-32-little <- binary>>, do: value
end
defp cosine_similarity([], _other), do: 0.0 defp cosine_similarity([], _other), do: 0.0
defp cosine_similarity(_vector, []), do: 0.0 defp cosine_similarity(_vector, []), do: 0.0

View File

@@ -3,4 +3,15 @@ defmodule BDS.Embeddings.Backend do
@callback model_info() :: %{model_id: String.t(), dimensions: pos_integer()} @callback model_info() :: %{model_id: String.t(), dimensions: pos_integer()}
@callback embed(String.t(), keyword()) :: {:ok, [number()]} | {:error, term()} @callback embed(String.t(), keyword()) :: {:ok, [number()]} | {:error, term()}
@doc """
Embeds a list of texts in a single call.
Backends that can amortise work across inputs (e.g. running the neural model
on a batched tensor) should implement this. The result list is aligned with
the input list. Optional — callers fall back to repeated `embed/2`.
"""
@callback embed_many([String.t()], keyword()) :: {:ok, [[number()]]} | {:error, term()}
@optional_callbacks embed_many: 2
end end

View File

@@ -1,5 +1,13 @@
defmodule BDS.Embeddings.Backends.InApp do defmodule BDS.Embeddings.Backends.InApp do
@moduledoc false @moduledoc """
Deterministic lexical embedding stub.
This backend does NOT satisfy the `RealNeuralModel` invariant — it projects
stemmed tokens and bigrams into a sparse hashed vector. It exists only as an
offline, dependency-free fallback for tests and environments where the neural
model (see `BDS.Embeddings.Backends.Neural`) cannot be loaded. Production and
development use the neural backend.
"""
@behaviour BDS.Embeddings.Backend @behaviour BDS.Embeddings.Backend
@@ -29,6 +37,17 @@ defmodule BDS.Embeddings.Backends.InApp do
{:ok, vector} {:ok, vector}
end end
@impl true
def embed_many(texts, opts) when is_list(texts) and is_list(opts) do
vectors =
Enum.map(texts, fn text ->
{:ok, vector} = embed(text, opts)
vector
end)
{:ok, vectors}
end
defp tokenize(text) do defp tokenize(text) do
Regex.scan(~r/[[:alnum:]]+/u, String.downcase(text)) Regex.scan(~r/[[:alnum:]]+/u, String.downcase(text))
|> List.flatten() |> List.flatten()

View File

@@ -0,0 +1,152 @@
defmodule BDS.Embeddings.Backends.Neural do
@moduledoc """
Real on-device neural embedding backend.
Implements the `RealNeuralModel` and `ModelCaching` invariants from
`specs/embedding.allium`: embeddings are produced by the actual
multilingual-e5-small transformer (the `intfloat/multilingual-e5-small`
weights behind the `Xenova/multilingual-e5-small` identifier) via
Bumblebee + EXLA, never by a lexical approximation.
* Lazy-loaded — the model pipeline is built on the first embedding
request, not at application startup.
* Model files (~100 MB) are downloaded from the Hugging Face Hub on
first use and cached on disk (Bumblebee cache dir), persisting across
sessions and project switches.
* Text preprocessing follows the e5 convention: every input is prefixed
with `"query: "`, pooled with mean pooling over the attention mask, and
L2-normalised. This is what makes cross-language semantic similarity
work.
* Inference is batched. `embed_many/2` runs the model on `batch_size`
texts per compiled inference run instead of one at a time, which is the
dominant cost when (re)indexing large numbers of posts. The serving is
compiled for a fixed `batch_size`/`sequence_length` (configurable);
shorter sequences mean less wasted transformer compute.
EXLA on Apple Silicon runs on the CPU — XLA has no Metal/GPU backend. See
SPECGAPS A1-14c for the planned EMLX (Apple GPU via MLX) acceleration path.
"""
@behaviour BDS.Embeddings.Backend
use GenServer
@query_prefix "query: "
@embed_timeout :timer.minutes(10)
@default_model_id "Xenova/multilingual-e5-small"
@default_model_repo "intfloat/multilingual-e5-small"
@default_dimensions 384
@default_batch_size 16
@default_sequence_length 256
def child_spec(opts) do
%{id: __MODULE__, start: {__MODULE__, :start_link, [opts]}}
end
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@impl BDS.Embeddings.Backend
def model_info do
config = config()
%{
model_id: Keyword.get(config, :model_id, @default_model_id),
dimensions: Keyword.get(config, :dimensions, @default_dimensions)
}
end
@impl BDS.Embeddings.Backend
def embed(text, _opts) when is_binary(text) do
case run([@query_prefix <> text]) do
{:ok, [vector]} -> {:ok, vector}
{:ok, _other} -> {:error, :unexpected_embedding_result}
{:error, _reason} = error -> error
end
end
@impl BDS.Embeddings.Backend
def embed_many([], _opts), do: {:ok, []}
def embed_many(texts, _opts) when is_list(texts) do
run(Enum.map(texts, &(@query_prefix <> &1)))
end
defp run(prefixed_texts) do
GenServer.call(__MODULE__, {:embed, prefixed_texts}, @embed_timeout)
catch
:exit, reason -> {:error, {:embedding_backend_unavailable, reason}}
end
@impl GenServer
def init(_opts), do: {:ok, %{serving: nil}}
@impl GenServer
def handle_call({:embed, texts}, _from, state) do
case ensure_serving(state) do
{:ok, %{serving: serving} = next_state} ->
vectors =
texts
|> Enum.chunk_every(batch_size())
|> Enum.flat_map(&run_chunk(serving, &1))
{:reply, {:ok, vectors}, next_state}
{:error, _reason} = error ->
{:reply, error, state}
end
rescue
exception ->
{:reply, {:error, Exception.message(exception)}, state}
end
defp run_chunk(serving, [single]) do
%{embedding: tensor} = Nx.Serving.run(serving, single)
[Nx.to_flat_list(tensor)]
end
defp run_chunk(serving, chunk) do
serving
|> Nx.Serving.run(chunk)
|> Enum.map(fn %{embedding: tensor} -> Nx.to_flat_list(tensor) end)
end
defp ensure_serving(%{serving: nil} = state) do
case build_serving() do
{:ok, serving} -> {:ok, %{state | serving: serving}}
{:error, _reason} = error -> error
end
end
defp ensure_serving(state), do: {:ok, state}
defp build_serving do
repo = {:hf, Keyword.get(config(), :model_repo, @default_model_repo)}
with {:ok, model_info} <- Bumblebee.load_model(repo),
{:ok, tokenizer} <- Bumblebee.load_tokenizer(repo) do
serving =
Bumblebee.Text.text_embedding(model_info, tokenizer,
output_pool: :mean_pooling,
output_attribute: :hidden_state,
embedding_processor: :l2_norm,
compile: [batch_size: batch_size(), sequence_length: sequence_length()],
defn_options: [compiler: EXLA]
)
{:ok, serving}
end
end
defp batch_size do
config() |> Keyword.get(:batch_size, @default_batch_size) |> max(1)
end
defp sequence_length do
config() |> Keyword.get(:sequence_length, @default_sequence_length) |> max(1)
end
defp config, do: Application.get_env(:bds, :embeddings, [])
end

View File

@@ -1,214 +1,342 @@
defmodule BDS.Embeddings.Index do defmodule BDS.Embeddings.Index do
@moduledoc false @moduledoc """
Per-project approximate-nearest-neighbour index over post embeddings.
import Ecto.Query Backed by an HNSW graph (hnswlib) per the A1-14b / `specs/embedding.allium`
requirement — cosine space, connectivity M=16, efConstruction=128,
efSearch=64. This replaces the previous O(n²) brute-force cosine snapshot:
building is O(n·log n) and queries are O(log n).
The process is intentionally **database-free**: callers (running in their own
process, e.g. under the test SQL sandbox) read embedding vectors from the DB
and hand them in. This GenServer owns only the in-memory HNSW graphs, the
`label → post_id` maps, and file persistence.
Persistence (DebouncedPersistence invariant): the index file
(`embeddings.usearch`) plus a small sidecar holding the dimension and the
label→post_id map are written behind a 5s debounce, and force-saved on
project switch / shutdown. On a cold query the index is lazily reloaded from
those files; if they are absent the caller rebuilds from the DB vectors.
"""
use GenServer
alias BDS.Persistence
alias BDS.Embeddings.Key
alias BDS.Projects alias BDS.Projects
alias BDS.ProgressReporter alias BDS.ProgressReporter
alias BDS.Repo
@neighbor_limit 21 @neighbor_limit 21
@debounce_ms 5_000
@space :cosine
@m 16
@ef_construction 128
@ef_search 64
# ─── Public API ─────────────────────────────────────────────
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@doc "On-disk path of the HNSW index file for a project."
def path(project_id) when is_binary(project_id) do def path(project_id) when is_binary(project_id) do
Path.join(Projects.project_cache_dir(project_id), "embeddings.usearch") Path.join(Projects.project_cache_dir(project_id), "embeddings.usearch")
end end
def rebuild(project_id, opts) when is_binary(project_id) and is_list(opts) do @doc """
model_id = Keyword.fetch!(opts, :model_id) (Re)builds the index for a project from the given entries and schedules a
dimensions = Keyword.fetch!(opts, :dimensions) debounced save. `entries` is a list of `%{label:, post_id:, vector:}` where
`vector` is the packed little-endian Float32 BLOB.
"""
def put(project_id, dimensions, entries)
when is_binary(project_id) and is_integer(dimensions) and is_list(entries) do
GenServer.call(__MODULE__, {:put, project_id, dimensions, entries}, :infinity)
end
keys = @doc """
Repo.all( Returns up to `limit` nearest neighbours of `query_vector` (the post's packed
from key in Key, BLOB), excluding `query_label`. `{:error, :missing}` if no index is available.
where: key.project_id == ^project_id, """
order_by: [asc: key.post_id] def neighbors(project_id, query_label, query_vector, limit)
when is_binary(project_id) and is_integer(query_label) and is_binary(query_vector) do
GenServer.call(__MODULE__, {:neighbors, project_id, query_label, query_vector, limit}, :infinity)
end
@doc """
Finds near-duplicate pairs at/above `threshold` by querying the HNSW graph for
each entry's neighbours. `{:error, :missing}` if no index is available.
"""
def duplicate_pairs(project_id, entries, threshold, opts \\ [])
when is_binary(project_id) and is_list(entries) and is_number(threshold) do
GenServer.call(
__MODULE__,
{:duplicate_pairs, project_id, entries, threshold, opts},
:infinity
) )
end
entries = @doc "Forces a pending save for a project to disk now (e.g. on project switch)."
keys def flush(project_id) when is_binary(project_id) do
|> Enum.map(fn key -> GenServer.call(__MODULE__, {:flush, project_id}, :infinity)
vector = decode_vector(key.vector) end
@doc "Forces all pending saves to disk now (e.g. on shutdown)."
def flush_all do
GenServer.call(__MODULE__, :flush_all, :infinity)
end
@doc "Drops the in-memory index for a project (e.g. on project deletion)."
def forget(project_id) when is_binary(project_id) do
GenServer.call(__MODULE__, {:forget, project_id}, :infinity)
end
# ─── GenServer ──────────────────────────────────────────────
@impl true
def init(_opts) do
Process.flag(:trap_exit, true)
{:ok, %{}}
end
@impl true
def handle_call({:put, project_id, dimensions, entries}, _from, state) do
entry = build_entry(dimensions, entries)
state = state |> Map.put(project_id, entry) |> schedule_save(project_id)
{:reply, :ok, state}
end
def handle_call({:neighbors, project_id, query_label, query_vector, limit}, _from, state) do
case ensure_loaded(state, project_id) do
{:ok, %{index: nil}, state} ->
{:reply, {:error, :missing}, state}
{:ok, entry, state} ->
{:reply, {:ok, query_neighbors(entry, query_label, query_vector, limit)}, state}
{:missing, state} ->
{:reply, {:error, :missing}, state}
end
end
def handle_call({:duplicate_pairs, project_id, entries, threshold, opts}, _from, state) do
case ensure_loaded(state, project_id) do
{:ok, %{index: nil}, state} ->
{:reply, {:error, :missing}, state}
{:ok, entry, state} ->
{:reply, {:ok, scan_duplicates(entry, entries, threshold, opts)}, state}
{:missing, state} ->
{:reply, {:error, :missing}, state}
end
end
def handle_call({:flush, project_id}, _from, state) do
{:reply, :ok, save_now(state, project_id)}
end
def handle_call(:flush_all, _from, state) do
state = Enum.reduce(Map.keys(state), state, &save_now(&2, &1))
{:reply, :ok, state}
end
def handle_call({:forget, project_id}, _from, state) do
case Map.get(state, project_id) do
%{timer: timer} when is_reference(timer) -> Process.cancel_timer(timer)
_other -> :ok
end
{:reply, :ok, Map.delete(state, project_id)}
end
@impl true
def handle_info({:save, project_id}, state) do
{:noreply, save_now(state, project_id)}
end
def handle_info(_message, state), do: {:noreply, state}
@impl true
def terminate(_reason, state) do
Enum.each(Map.keys(state), &save_now(state, &1))
:ok
end
# ─── Build / query ──────────────────────────────────────────
defp build_entry(dimensions, []), do: %{index: nil, labels: %{}, dim: dimensions, timer: nil}
defp build_entry(dimensions, entries) do
count = length(entries)
{:ok, index} = HNSWLib.Index.new(@space, dimensions, count, m: @m, ef_construction: @ef_construction)
:ok = HNSWLib.Index.set_ef(index, @ef_search)
tensor =
entries
|> Enum.map(& &1.vector)
|> IO.iodata_to_binary()
|> Nx.from_binary(:f32)
|> Nx.reshape({count, dimensions})
:ok = HNSWLib.Index.add_items(index, tensor, ids: Enum.map(entries, & &1.label))
{key.post_id,
%{ %{
"label" => key.label, index: index,
"content_hash" => key.content_hash, labels: Map.new(entries, &{&1.label, &1.post_id}),
"neighbors" => neighbor_entries(keys, key, vector) dim: dimensions,
}} timer: nil
end)
|> Map.new()
payload = %{
"project_id" => project_id,
"model_id" => model_id,
"dimensions" => dimensions,
"updated_at" => Persistence.now_ms(),
"entries" => entries
} }
write_snapshot(path(project_id), payload, project_id)
end end
def read(project_id) when is_binary(project_id) do defp query_neighbors(%{index: index, labels: labels}, query_label, query_vector, limit) do
project_id case query(index, query_vector, limit + 1) do
|> candidate_paths() [] ->
|> read_snapshot_paths() []
end
def neighbors(project_id, post_id, limit) when is_binary(project_id) and is_binary(post_id) do results ->
with {:ok, snapshot} <- read(project_id), results
%{} = entry <- get_in(snapshot, ["entries", post_id]) do |> Enum.reject(fn {label, _score} -> label == query_label end)
entry |> Enum.map(fn {label, score} -> %{post_id: Map.get(labels, label), score: score} end)
|> Map.get("neighbors", []) |> Enum.reject(&is_nil(&1.post_id))
|> Enum.take(max(limit, 0)) |> Enum.take(max(limit, 0))
|> Enum.map(fn neighbor ->
%{
post_id: neighbor["post_id"],
score: neighbor["score"]
}
end)
|> then(&{:ok, &1})
else
_ -> {:error, :missing}
end end
end end
def duplicate_pairs(project_id, threshold, opts \\ []) when is_binary(project_id) do defp scan_duplicates(%{index: index, labels: labels}, entries, threshold, opts) do
with {:ok, snapshot} <- read(project_id) do on_progress = ProgressReporter.callback(opts)
entries = Map.get(snapshot, "entries", %{}) total = length(entries)
entry_count = map_size(entries) :ok = report_scan_started(on_progress, total, "embedding entries")
on_progress = progress_callback(opts)
:ok = report_scan_started(on_progress, entry_count, "embedding entries")
pairs =
entries entries
|> Enum.with_index(1) |> Enum.with_index(1)
|> Enum.flat_map(fn {{post_id, entry}, index} -> |> Enum.flat_map(fn {entry, position} ->
:ok = report_scan_progress(on_progress, index, entry_count, "embedding entries") :ok = report_scan_progress(on_progress, position, total, "embedding entries")
entry index
|> Map.get("neighbors", []) |> query(entry.vector, @neighbor_limit)
|> Enum.filter(&(&1["score"] >= threshold)) |> Enum.reject(fn {label, _score} -> label == entry.label end)
|> Enum.map(fn neighbor -> |> Enum.map(fn {label, score} -> {Map.get(labels, label), score} end)
{post_id_a, post_id_b} = sort_pair(post_id, neighbor["post_id"]) |> Enum.filter(fn {post_id, score} -> not is_nil(post_id) and score >= threshold end)
|> Enum.map(fn {other_post_id, score} ->
{{post_id_a, post_id_b}, {post_id_a, post_id_b} = sort_pair(entry.post_id, other_post_id)
%{ {{post_id_a, post_id_b}, %{post_id_a: post_id_a, post_id_b: post_id_b, score: score}}
post_id_a: post_id_a,
post_id_b: post_id_b,
score: neighbor["score"]
}}
end) end)
end) end)
|> Map.new() |> Map.new()
|> Map.values() |> Map.values()
|> Enum.sort_by(& &1.score, :desc) |> Enum.sort_by(& &1.score, :desc)
end
{:ok, pairs} # Runs a knn query and returns [{label, similarity}] sorted by descending
else # similarity. Cosine distance is converted to similarity as max(0, 1 - d).
_ -> {:error, :missing} defp query(index, query_vector, k) do
case HNSWLib.Index.get_current_count(index) do
{:ok, count} when count > 0 ->
clamped = min(k, count)
case HNSWLib.Index.knn_query(index, query_vector, k: clamped) do
{:ok, labels, distances} ->
Enum.zip(
Nx.to_flat_list(labels),
Enum.map(Nx.to_flat_list(distances), fn distance -> max(0.0, 1.0 - distance) end)
)
{:error, _reason} ->
[]
end
_other ->
[]
end end
end end
defp neighbor_entries(keys, current_key, current_vector) do # ─── Persistence ────────────────────────────────────────────
keys
|> Enum.reject(&(&1.post_id == current_key.post_id)) defp schedule_save(state, project_id) do
|> Enum.map(fn other_key -> entry = Map.fetch!(state, project_id)
%{ if is_reference(entry.timer), do: Process.cancel_timer(entry.timer)
"post_id" => other_key.post_id, timer = Process.send_after(self(), {:save, project_id}, @debounce_ms)
"label" => other_key.label, Map.put(state, project_id, %{entry | timer: timer})
"score" => cosine_similarity(current_vector, decode_vector(other_key.vector))
}
end)
|> Enum.sort_by(& &1["score"], :desc)
|> Enum.take(@neighbor_limit)
end end
defp write_snapshot(snapshot_path, payload, project_id) do defp save_now(state, project_id) do
:ok = Persistence.atomic_write(snapshot_path, Jason.encode!(payload)) case Map.get(state, project_id) do
legacy_path = legacy_path(snapshot_path) nil ->
state
if File.exists?(legacy_path) do entry ->
File.rm(legacy_path) if is_reference(entry.timer), do: Process.cancel_timer(entry.timer)
persist(project_id, entry)
Map.put(state, project_id, %{entry | timer: nil})
end
end end
cleanup_legacy_project_snapshots(project_id, snapshot_path) defp persist(_project_id, %{index: nil}), do: :ok
defp persist(project_id, %{index: index, labels: labels, dim: dim}) do
index_path = path(project_id)
File.mkdir_p!(Path.dirname(index_path))
HNSWLib.Index.save_index(index, index_path)
write_meta(index_path, dim, labels)
:ok :ok
rescue
_exception -> :ok
end end
defp candidate_paths(project_id) do defp write_meta(index_path, dim, labels) do
current_snapshot_path = path(project_id) payload = %{
legacy_project_snapshot_path = legacy_project_snapshot_path(project_id) "dim" => dim,
"labels" => Enum.map(labels, fn {label, post_id} -> [label, post_id] end)
}
[ File.write(meta_path(index_path), Jason.encode!(payload))
current_snapshot_path,
legacy_path(current_snapshot_path),
legacy_project_snapshot_path,
legacy_project_snapshot_path && legacy_path(legacy_project_snapshot_path)
]
|> Enum.filter(&is_binary/1)
|> Enum.uniq()
end end
defp read_snapshot_paths([]), do: {:error, :missing} defp ensure_loaded(state, project_id) do
case Map.get(state, project_id) do
nil ->
case load_from_disk(project_id) do
{:ok, entry} -> {:ok, entry, Map.put(state, project_id, entry)}
:error -> {:missing, state}
end
defp read_snapshot_paths([snapshot_path | rest]) do entry ->
case File.read(snapshot_path) do {:ok, entry, state}
{:ok, contents} -> {:ok, Jason.decode!(contents)}
{:error, :enoent} -> read_snapshot_paths(rest)
{:error, reason} -> {:error, reason}
end end
end end
defp cleanup_legacy_project_snapshots(project_id, snapshot_path) do defp load_from_disk(project_id) do
current_paths = [snapshot_path, legacy_path(snapshot_path)] index_path = path(project_id)
project_id with {:ok, %{dim: dim, labels: labels}} <- read_meta(index_path),
|> legacy_project_snapshot_path() true <- File.exists?(index_path),
|> then(fn legacy_snapshot_path -> {:ok, index} <- HNSWLib.Index.load_index(@space, dim, index_path) do
[legacy_snapshot_path, legacy_snapshot_path && legacy_path(legacy_snapshot_path)] :ok = HNSWLib.Index.set_ef(index, @ef_search)
end) {:ok, %{index: index, labels: labels, dim: dim, timer: nil}}
|> Enum.filter(&is_binary/1) else
|> Enum.reject(&(&1 in current_paths)) _other -> :error
|> Enum.each(fn legacy_snapshot_path ->
if File.exists?(legacy_snapshot_path) do
File.rm(legacy_snapshot_path)
end end
end) rescue
_exception -> :error
end end
defp legacy_project_snapshot_path(project_id) do defp read_meta(index_path) do
case Projects.get_project(project_id) do with {:ok, contents} <- File.read(meta_path(index_path)),
nil -> nil {:ok, %{"dim" => dim, "labels" => labels}} <- Jason.decode(contents) do
project -> Path.join(Projects.project_data_dir(project), "embeddings.usearch") {:ok,
%{
dim: dim,
labels: Map.new(labels, fn [label, post_id] -> {label, post_id} end)
}}
else
_other -> :error
end end
end end
defp legacy_path(snapshot_path) do defp meta_path(index_path), do: index_path <> ".meta.json"
Path.join(Path.dirname(snapshot_path), "embeddings.index.json")
end
defp decode_vector(nil), do: []
defp decode_vector(vector), do: Jason.decode!(vector)
defp cosine_similarity([], _other), do: 0.0
defp cosine_similarity(_vector, []), do: 0.0
defp cosine_similarity(left, right) do
Enum.zip(left, right)
|> Enum.reduce(0.0, fn {left_value, right_value}, acc -> acc + left_value * right_value end)
|> max(0.0)
end
defp sort_pair(post_id_a, post_id_b) when post_id_a <= post_id_b, do: {post_id_a, post_id_b} defp sort_pair(post_id_a, post_id_b) when post_id_a <= post_id_b, do: {post_id_a, post_id_b}
defp sort_pair(post_id_a, post_id_b), do: {post_id_b, post_id_a} defp sort_pair(post_id_a, post_id_b), do: {post_id_b, post_id_a}
defp progress_callback(opts), do: ProgressReporter.callback(opts)
defp report_scan_started(callback, total, label) do defp report_scan_started(callback, total, label) do
ProgressReporter.report_count_started(callback, total, label, ProgressReporter.report_count_started(callback, total, label,
verb: "Scanning", verb: "Scanning",

View File

@@ -12,7 +12,9 @@ defmodule BDS.Embeddings.Key do
belongs_to :project, BDS.Projects.Project, type: :string belongs_to :project, BDS.Projects.Project, type: :string
field :content_hash, :string field :content_hash, :string
field :vector, :string # Packed little-endian Float32 BLOB (dimensions * 4 bytes), per the
# VectorCacheInDb invariant in specs/embedding.allium.
field :vector, :binary
end end
def changeset(key, attrs) do def changeset(key, attrs) do

View File

@@ -7,10 +7,25 @@ defmodule BDS.Generation.Pagefind do
@typedoc "A (relative_path, content) generated file tuple." @typedoc "A (relative_path, content) generated file tuple."
@type generated_file :: {String.t(), String.t()} @type generated_file :: {String.t(), String.t()}
@assets_dir Application.app_dir(:bds, "priv/preview_assets/assets")
@ui_js_path Path.join(@assets_dir, "pagefind-ui.js")
@ui_css_path Path.join(@assets_dir, "pagefind-ui.css")
@external_resource @ui_js_path
@external_resource @ui_css_path
@ui_js File.read!(@ui_js_path)
@ui_css File.read!(@ui_css_path)
@doc """ @doc """
Build the per-language Pagefind index outputs (`pagefind/index.json`, Build the per-language Pagefind index outputs (`pagefind/index.json`,
`pagefind/pagefind-ui.js`, `pagefind/pagefind-ui.css`) for every blog `pagefind/pagefind-ui.js`, `pagefind/pagefind-ui.css`) for every blog
language declared on the plan. language declared on the plan.
The fragment index records one entry per indexable page, where indexable
means the page carries a `data-pagefind-body` region. Each entry stores the
page URL, its title, and the body text scoped to that region — mirroring
Pagefind's behaviour of ignoring content outside `data-pagefind-body`.
""" """
@spec build_outputs(map(), [html_output()]) :: [generated_file()] @spec build_outputs(map(), [html_output()]) :: [generated_file()]
def build_outputs(plan, html_outputs) do def build_outputs(plan, html_outputs) do
@@ -31,8 +46,8 @@ defmodule BDS.Generation.Pagefind do
[ [
{Path.join(prefix ++ ["index.json"]), {Path.join(prefix ++ ["index.json"]),
Jason.encode!(%{"language" => language, "pages" => pages})}, Jason.encode!(%{"language" => language, "pages" => pages})},
{Path.join(prefix ++ ["pagefind-ui.js"]), ui_js(language)}, {Path.join(prefix ++ ["pagefind-ui.js"]), @ui_js},
{Path.join(prefix ++ ["pagefind-ui.css"]), ui_css()} {Path.join(prefix ++ ["pagefind-ui.css"]), @ui_css}
] ]
end) end)
end end
@@ -43,11 +58,14 @@ defmodule BDS.Generation.Pagefind do
String.ends_with?(relative_path, ".html") and String.ends_with?(relative_path, ".html") and
language_match?(relative_path, route_language, other_prefixes) language_match?(relative_path, route_language, other_prefixes)
end) end)
|> Enum.map(fn {relative_path, content} -> |> Enum.flat_map(fn {relative_path, content} ->
%{ case body_text(content) do
"url" => "/" <> relative_path, nil ->
"text" => text(content) []
}
text ->
[%{"url" => "/" <> relative_path, "title" => title(content), "text" => text}]
end
end) end)
end end
@@ -60,19 +78,94 @@ defmodule BDS.Generation.Pagefind do
defp language_match?(relative_path, route_language, _other_prefixes), defp language_match?(relative_path, route_language, _other_prefixes),
do: String.starts_with?(relative_path, route_language <> "/") do: String.starts_with?(relative_path, route_language <> "/")
defp text(content) do # Extract the indexable body text scoped to the data-pagefind-body element.
content # Returns nil when the page is not marked, so unmarked pages are excluded
# from the index entirely (matching Pagefind semantics).
defp body_text(content) do
case Regex.run(~r/<([a-zA-Z0-9]+)[^>]*\bdata-pagefind-body\b[^>]*>/, content,
return: :index
) do
[{open_start, open_len}, {tag_start, tag_len}] ->
tag = binary_part(content, tag_start, tag_len)
region = scoped_region(content, tag, open_start + open_len)
plain_text(region)
_no_match ->
nil
end
end
# Capture the inner HTML of the marked element by balancing same-tag
# open/close pairs from the opening tag onward.
defp scoped_region(content, tag, body_start) do
rest = binary_part(content, body_start, byte_size(content) - body_start)
open_re = Regex.compile!("<#{tag}\\b", "i")
close_re = Regex.compile!("</#{tag}\\s*>", "i")
events =
(Regex.scan(open_re, rest, return: :index) ++ Regex.scan(close_re, rest, return: :index))
|> Enum.map(fn [{pos, _len}] -> pos end)
|> Enum.map(fn pos -> {pos, event_kind(rest, pos, tag)} end)
|> Enum.sort_by(&elem(&1, 0))
close_at = balanced_close(events, 0)
case close_at do
nil -> rest
pos -> binary_part(rest, 0, pos)
end
end
defp event_kind(rest, pos, tag) do
if String.starts_with?(binary_part(rest, pos, min(2 + byte_size(tag), byte_size(rest) - pos)), "</") do
:close
else
:open
end
end
defp balanced_close([], _depth), do: nil
defp balanced_close([{pos, :close} | _rest], 0), do: pos
defp balanced_close([{_pos, :close} | rest], depth),
do: balanced_close(rest, depth - 1)
defp balanced_close([{_pos, :open} | rest], depth),
do: balanced_close(rest, depth + 1)
defp title(content) do
tag_text(content, ~r/<title[^>]*>(.*?)<\/title>/si) ||
tag_text(content, ~r/<h1[^>]*>(.*?)<\/h1>/si) ||
""
end
defp tag_text(content, regex) do
case Regex.run(regex, content) do
[_full, raw] -> raw |> plain_text() |> nil_if_blank()
_no_match -> nil
end
end
defp nil_if_blank(""), do: nil
defp nil_if_blank(value), do: value
defp plain_text(html) do
html
|> String.replace(~r/<[^>]+>/, " ") |> String.replace(~r/<[^>]+>/, " ")
|> decode_entities()
|> String.replace(~r/\s+/u, " ") |> String.replace(~r/\s+/u, " ")
|> String.trim() |> String.trim()
end end
defp ui_js(language) do defp decode_entities(text) do
"window.bDSPagefind = { language: #{Jason.encode!(language)} };\n" text
end |> String.replace("&amp;", "&")
|> String.replace("&lt;", "<")
defp ui_css do |> String.replace("&gt;", ">")
".pagefind-ui{display:block;}\n" |> String.replace("&quot;", "\"")
|> String.replace("&#39;", "'")
|> String.replace("&nbsp;", " ")
end end
defp route_language(main_language, language) when main_language == language, do: nil defp route_language(main_language, language) when main_language == language, do: nil

View File

@@ -114,10 +114,19 @@ defmodule BDS.Git do
def history(project_id, branch, opts \\ []) def history(project_id, branch, opts \\ [])
when is_binary(project_id) and is_binary(branch) and is_list(opts) do when is_binary(project_id) and is_binary(branch) and is_list(opts) do
with {:ok, project_dir} <- project_dir(project_id), with {:ok, project_dir} <- project_dir(project_id),
{:ok, local_log} <- run_git(project_dir, ["log", "--format=%H%x09%s", branch], opts), {:ok, local_log} <-
{:ok, remote_log} <- run_git(
run_git(project_dir, ["log", "--format=%H", "origin/#{branch}"], opts) do project_dir,
local_commits = parse_local_history(local_log) ["log", "--date=short", "--format=%H%x09%an%x09%ad%x09%s", branch],
opts
) do
remote_log =
case run_git(project_dir, ["log", "--format=%H", "origin/#{branch}"], opts) do
{:ok, output} -> output
{:error, {:git_failed, _message}} -> ""
end
local_commits = parse_history_log(local_log)
remote_hashes = MapSet.new(parse_remote_history(remote_log)) remote_hashes = MapSet.new(parse_remote_history(remote_log))
local_hashes = MapSet.new(Enum.map(local_commits, & &1.hash)) local_hashes = MapSet.new(Enum.map(local_commits, & &1.hash))
@@ -126,7 +135,7 @@ defmodule BDS.Git do
|> MapSet.difference(local_hashes) |> MapSet.difference(local_hashes)
|> MapSet.to_list() |> MapSet.to_list()
|> Enum.map(fn hash -> |> Enum.map(fn hash ->
%{hash: hash, subject: nil, sync_status: %{kind: :remote_only}} %{hash: hash, subject: nil, author: nil, date: nil, sync_status: %{kind: :remote_only}}
end) end)
commits = commits =
@@ -204,6 +213,22 @@ defmodule BDS.Git do
end end
end end
def set_remote(project_id, remote_url, opts \\ [])
when is_binary(project_id) and is_binary(remote_url) and is_list(opts) do
with {:ok, project_dir} <- project_dir(project_id) do
case run_git(project_dir, ["remote", "add", "origin", remote_url], opts) do
{:ok, _output} ->
{:ok, %{remote_url: remote_url}}
{:error, {:git_failed, _message}} ->
with {:ok, _output} <-
run_git(project_dir, ["remote", "set-url", "origin", remote_url], opts) do
{:ok, %{remote_url: remote_url}}
end
end
end
end
def remote_state(project_id, opts \\ []) when is_binary(project_id) and is_list(opts) do def remote_state(project_id, opts \\ []) when is_binary(project_id) and is_list(opts) do
with {:ok, project_dir} <- project_dir(project_id), with {:ok, project_dir} <- project_dir(project_id),
{:ok, local_branch} <- current_branch(project_dir, opts) do {:ok, local_branch} <- current_branch(project_dir, opts) do
@@ -380,6 +405,23 @@ defmodule BDS.Git do
end) end)
end end
defp parse_history_log(output) do
output
|> String.split("\n", trim: true)
|> Enum.map(fn line ->
case String.split(line, "\t", parts: 4) do
[hash, author, date, subject] ->
%{hash: hash, author: author, date: date, subject: subject}
[hash, author, date] ->
%{hash: hash, author: author, date: date, subject: nil}
[hash | _rest] ->
%{hash: hash, author: nil, date: nil, subject: nil}
end
end)
end
defp parse_remote_history(output) do defp parse_remote_history(output) do
String.split(output, "\n", trim: true) String.split(output, "\n", trim: true)
end end

View File

@@ -101,11 +101,18 @@ defmodule BDS.Maintenance.Repair do
:file_to_db -> :file_to_db ->
post_ids = Enum.map(items, &metadata_diff_item_entity_id/1) post_ids = Enum.map(items, &metadata_diff_item_entity_id/1)
{:ok, repaired_post_ids} = Embeddings.repair_posts(project_id, post_ids) # If the embedding model is unavailable, every item is reported as
repaired_post_ids = MapSet.new(repaired_post_ids) # failed rather than crashing the repair task.
repaired =
case Embeddings.repair_posts(project_id, post_ids) do
{:ok, repaired_post_ids} -> repaired_post_ids
{:error, _reason} -> []
end
repaired_set = MapSet.new(repaired)
build_batch_repair_result(items, total, on_progress, fn item -> build_batch_repair_result(items, total, on_progress, fn item ->
MapSet.member?(repaired_post_ids, metadata_diff_item_entity_id(item)) MapSet.member?(repaired_set, metadata_diff_item_entity_id(item))
end) end)
:db_to_file -> :db_to_file ->

View File

@@ -1,6 +1,8 @@
defmodule BDS.Metadata do defmodule BDS.Metadata do
@moduledoc false @moduledoc false
require Logger
alias BDS.Embeddings alias BDS.Embeddings
alias BDS.I18n alias BDS.I18n
alias BDS.Persistence alias BDS.Persistence
@@ -653,7 +655,17 @@ defmodule BDS.Metadata do
) do ) do
if previous_state.semantic_similarity_enabled != true and if previous_state.semantic_similarity_enabled != true and
project_metadata.semantic_similarity_enabled == true do project_metadata.semantic_similarity_enabled == true do
{:ok, _indexed_post_ids} = Embeddings.index_unindexed(project_id) # Backfill is best-effort: if the embedding model is unavailable, keep the
# setting enabled and log it rather than failing the metadata update.
case Embeddings.index_unindexed(project_id) do
{:ok, _indexed_post_ids} ->
:ok
{:error, reason} ->
Logger.warning(
"Embedding backfill skipped for project #{project_id}: #{inspect(reason)}"
)
end
end end
result result

View File

@@ -14,6 +14,10 @@ defmodule BDS.Preview do
@host "127.0.0.1" @host "127.0.0.1"
@port 4123 @port 4123
# Max time to wait for inflight requests to finish during graceful shutdown
# before remaining request tasks are forcibly terminated.
@drain_timeout 5_000
def start_link(_opts) do def start_link(_opts) do
GenServer.start_link(__MODULE__, %{}, name: __MODULE__) GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
end end
@@ -56,7 +60,7 @@ defmodule BDS.Preview do
@impl true @impl true
def init(_state) do def init(_state) do
{:ok, %{current: nil}} {:ok, %{current: nil, stopping: nil}}
end end
@impl true @impl true
@@ -78,15 +82,12 @@ defmodule BDS.Preview do
{:reply, reply, next_state} {:reply, reply, next_state}
end end
def handle_call({:stop_preview, project_id}, _from, state) do def handle_call({:stop_preview, project_id}, from, state) do
next_state =
if match?(%{project_id: ^project_id}, state.current) do if match?(%{project_id: ^project_id}, state.current) do
stop_current_server(state) begin_graceful_stop(state, from)
else else
state {:reply, :ok, state}
end end
{:reply, :ok, next_state}
end end
def handle_call({:request, project_id, request_path, query_params}, _from, state) do def handle_call({:request, project_id, request_path, query_params}, _from, state) do
@@ -141,6 +142,25 @@ defmodule BDS.Preview do
end end
@impl true @impl true
def handle_cast({:track_request, pid}, %{current: %{} = current} = state) when is_pid(pid) do
ref = Process.monitor(pid)
inflight = Map.put(current.inflight, ref, pid)
{:noreply, %{state | current: %{current | inflight: inflight}}}
end
def handle_cast({:track_request, _pid}, state), do: {:noreply, state}
@impl true
def handle_info({:DOWN, ref, :process, _pid, _reason}, %{current: %{} = current} = state) do
inflight = Map.delete(current.inflight, ref)
state = %{state | current: %{current | inflight: inflight}}
{:noreply, maybe_finalize_stop(state)}
end
def handle_info(:drain_timeout, state) do
{:noreply, force_finalize_stop(state)}
end
def handle_info(_msg, state) do def handle_info(_msg, state) do
{:noreply, state} {:noreply, state}
end end
@@ -287,9 +307,18 @@ defmodule BDS.Preview do
defp accept_loop(listener, project_id) do defp accept_loop(listener, project_id) do
case :gen_tcp.accept(listener) do case :gen_tcp.accept(listener) do
{:ok, socket} -> {:ok, socket} ->
Task.Supervisor.start_child(BDS.TCP.TaskSupervisor, fn -> case Task.Supervisor.start_child(BDS.TCP.TaskSupervisor, fn ->
serve_client(socket, project_id) serve_client(socket, project_id)
end) end) do
{:ok, pid} ->
# Hand the socket to the request task so an inflight request survives
# the acceptor being shut down (it would otherwise close the socket).
_ = :gen_tcp.controlling_process(socket, pid)
GenServer.cast(__MODULE__, {:track_request, pid})
_other ->
:ok
end
accept_loop(listener, project_id) accept_loop(listener, project_id)
@@ -412,14 +441,58 @@ defmodule BDS.Preview do
end end
end end
defp stop_current_server(%{current: %{listener: listener, acceptor_pid: acceptor_pid}} = state) do # Graceful shutdown: stop accepting new connections, then wait for inflight
_ = :gen_tcp.close(listener) # request tasks to finish before reporting the server stopped. The stop call
if is_pid(acceptor_pid), do: Process.exit(acceptor_pid, :normal) # is parked (no immediate reply) and finalized from the :DOWN handlers, so the
# GenServer stays available to serve the requests it is draining.
defp begin_graceful_stop(%{current: current} = state, from) do
_ = :gen_tcp.close(current.listener)
if is_pid(current.acceptor_pid), do: Process.exit(current.acceptor_pid, :normal)
if map_size(current.inflight) == 0 do
{:reply, :ok, %{state | current: nil, stopping: nil}}
else
timer = Process.send_after(self(), :drain_timeout, @drain_timeout)
{:noreply, %{state | stopping: %{from: from, timer: timer}}}
end
end
defp maybe_finalize_stop(
%{stopping: %{from: from, timer: timer}, current: %{inflight: inflight}} = state
)
when map_size(inflight) == 0 do
if is_reference(timer), do: Process.cancel_timer(timer)
GenServer.reply(from, :ok)
%{state | current: nil, stopping: nil}
end
defp maybe_finalize_stop(state), do: state
defp force_finalize_stop(%{stopping: %{from: from}, current: %{inflight: inflight}} = state) do
kill_inflight(inflight)
GenServer.reply(from, :ok)
%{state | current: nil, stopping: nil}
end
defp force_finalize_stop(state), do: state
# Hard stop used when restarting the server in place (no graceful drain).
defp stop_current_server(%{current: %{} = current} = state) do
_ = :gen_tcp.close(current.listener)
if is_pid(current.acceptor_pid), do: Process.exit(current.acceptor_pid, :normal)
kill_inflight(current.inflight)
%{state | current: nil} %{state | current: nil}
end end
defp stop_current_server(state), do: state defp stop_current_server(state), do: state
defp kill_inflight(inflight) do
Enum.each(inflight, fn {ref, pid} ->
Process.demonitor(ref, [:flush])
if is_pid(pid), do: Process.exit(pid, :kill)
end)
end
defp start_server(state, project_id, data_dir, owner_pid) do defp start_server(state, project_id, data_dir, owner_pid) do
state = stop_current_server(state) state = stop_current_server(state)
maybe_allow_repo(owner_pid) maybe_allow_repo(owner_pid)
@@ -442,7 +515,8 @@ defmodule BDS.Preview do
port: @port, port: @port,
is_running: true, is_running: true,
listener: listener, listener: listener,
acceptor_pid: acceptor_pid acceptor_pid: acceptor_pid,
inflight: %{}
} }
{{:ok, public_server(server)}, %{state | current: server}} {{:ok, public_server(server)}, %{state | current: server}}

View File

@@ -148,6 +148,9 @@ defmodule BDS.Projects do
project -> project ->
now = Persistence.now_ms() now = Persistence.now_ms()
previous_active_id =
Repo.one(from p in Project, where: p.is_active == true, select: p.id)
Repo.transaction(fn -> Repo.transaction(fn ->
Repo.update_all( Repo.update_all(
from(p in Project, where: p.is_active == true), from(p in Project, where: p.is_active == true),
@@ -159,8 +162,16 @@ defmodule BDS.Projects do
|> Repo.update!() |> Repo.update!()
end) end)
|> case do |> case do
{:ok, active_project} -> {:ok, active_project} {:ok, active_project} ->
{:error, reason} -> {:error, reason} # Force-save the outgoing project's embedding index (DebouncedPersistence).
if is_binary(previous_active_id) and previous_active_id != active_project.id do
BDS.Embeddings.Index.flush(previous_active_id)
end
{:ok, active_project}
{:error, reason} ->
{:error, reason}
end end
end end
end end
@@ -194,6 +205,8 @@ defmodule BDS.Projects do
end) end)
|> case do |> case do
{:ok, deleted_project} -> {:ok, deleted_project} ->
BDS.Embeddings.Index.forget(deleted_project.id)
Enum.each(cleanup_dirs, fn dir -> Enum.each(cleanup_dirs, fn dir ->
_ = File.rm_rf(dir) _ = File.rm_rf(dir)
end) end)

View File

@@ -27,6 +27,7 @@ defmodule BDS.Rendering.Labels do
language_switcher_label: dgettext("render", "Language"), language_switcher_label: dgettext("render", "Language"),
site_search_label: dgettext("render", "Site search"), site_search_label: dgettext("render", "Site search"),
search_placeholder: dgettext("render", "Search..."), search_placeholder: dgettext("render", "Search..."),
search_no_results: dgettext("render", "No results found"),
not_found_message: dgettext("render", "The requested preview page could not be found."), not_found_message: dgettext("render", "The requested preview page could not be found."),
not_found_back_label: dgettext("render", "Back to preview home"), not_found_back_label: dgettext("render", "Back to preview home"),
youtube_video: dgettext("render", "YouTube video"), youtube_video: dgettext("render", "YouTube video"),

View File

@@ -57,10 +57,13 @@ defmodule BDS.Scripts do
{:error, :not_found} {:error, :not_found}
script -> script ->
content = script.content || ""
case validate_script(content) do
:ok ->
file_path = script_file_path(script.slug) file_path = script_file_path(script.slug)
full_path = full_file_path(script.project_id, file_path) full_path = full_file_path(script.project_id, file_path)
updated_at = Persistence.now_ms() updated_at = Persistence.now_ms()
content = script.content || ""
:ok = :ok =
Persistence.atomic_write( Persistence.atomic_write(
@@ -79,6 +82,10 @@ defmodule BDS.Scripts do
updated_at: updated_at updated_at: updated_at
}) })
|> Repo.update() |> Repo.update()
{:error, reason} ->
{:error, {:invalid_script, reason}}
end
end end
end end
@@ -254,6 +261,13 @@ defmodule BDS.Scripts do
not Repo.exists?(scoped_query) not Repo.exists?(scoped_query)
end end
defp validate_script(source) do
case BDS.Scripting.validate(source) do
:ok -> :ok
{:error, reason} -> {:error, reason}
end
end
defp script_file_path(slug), do: Path.join(["scripts", "#{slug}.lua"]) defp script_file_path(slug), do: Path.join(["scripts", "#{slug}.lua"])
defp full_file_path(project_id, relative_path) do defp full_file_path(project_id, relative_path) do

View File

@@ -28,23 +28,38 @@ defmodule BDS.Templates do
now = Persistence.now_ms() now = Persistence.now_ms()
project_id = attr(attrs, :project_id) project_id = attr(attrs, :project_id)
title = attr(attrs, :title) || "" title = attr(attrs, :title) || ""
slug = unique_slug(project_id, Slug.slugify(title), "template")
file_path = template_file_path(slug)
changeset =
%Template{} %Template{}
|> Template.changeset(%{ |> Template.changeset(%{
id: Ecto.UUID.generate(), id: Ecto.UUID.generate(),
project_id: project_id, project_id: project_id,
slug: unique_slug(project_id, Slug.slugify(title), "template"), slug: slug,
title: title, title: title,
kind: attr(attrs, :kind), kind: attr(attrs, :kind),
enabled: true, enabled: true,
version: 1, version: 1,
file_path: "", file_path: file_path,
status: :draft, status: :draft,
content: attr(attrs, :content), content: attr(attrs, :content),
created_at: now, created_at: now,
updated_at: now updated_at: now
}) })
|> Repo.insert()
with {:ok, template} <- Repo.insert(changeset) do
full_path = full_file_path(template.project_id, file_path)
File.mkdir_p!(Path.dirname(full_path))
:ok =
Persistence.atomic_write(
full_path,
serialize_template_file(template, template.content || "")
)
{:ok, template}
end
end end
@spec get_template(String.t()) :: Template.t() | nil @spec get_template(String.t()) :: Template.t() | nil
@@ -62,10 +77,13 @@ defmodule BDS.Templates do
{:error, :not_found} {:error, :not_found}
template -> template ->
content = template.content || ""
case validate_liquid(content) do
:ok ->
file_path = template_file_path(template.slug) file_path = template_file_path(template.slug)
full_path = full_file_path(template.project_id, file_path) full_path = full_file_path(template.project_id, file_path)
updated_at = Persistence.now_ms() updated_at = Persistence.now_ms()
content = template.content || ""
:ok = :ok =
Persistence.atomic_write( Persistence.atomic_write(
@@ -84,6 +102,10 @@ defmodule BDS.Templates do
updated_at: updated_at updated_at: updated_at
}) })
|> Repo.update() |> Repo.update()
{:error, reason} ->
{:error, {:invalid_liquid, reason}}
end
end end
end end
@@ -327,6 +349,13 @@ defmodule BDS.Templates do
not Repo.exists?(scoped_query) not Repo.exists?(scoped_query)
end end
defp validate_liquid(source) do
case Liquex.parse(source) do
{:ok, _ast} -> :ok
{:error, reason, line} -> {:error, "#{reason} at line #{line}"}
end
end
defp template_file_path(slug), do: Path.join(["templates", "#{slug}.liquid"]) defp template_file_path(slug), do: Path.join(["templates", "#{slug}.liquid"])
defp full_file_path(project_id, relative_path) do defp full_file_path(project_id, relative_path) do
@@ -334,7 +363,6 @@ defmodule BDS.Templates do
Path.join(Projects.project_data_dir(project), relative_path) Path.join(Projects.project_data_dir(project), relative_path)
end end
defp next_template_file_path(%Template{file_path: ""}, _next_slug), do: ""
defp next_template_file_path(%Template{}, next_slug), do: template_file_path(next_slug) defp next_template_file_path(%Template{}, next_slug), do: template_file_path(next_slug)
defp serialize_template_file(template, content) do defp serialize_template_file(template, content) do

View File

@@ -35,7 +35,7 @@ defmodule BDS.UI.Sidebar do
"import", "import",
list_import_definitions(project_id) list_import_definitions(project_id)
), ),
"git" => git_view(), "git" => git_view(project_id),
"settings" => settings_nav_view() "settings" => settings_nav_view()
} }
end end
@@ -94,7 +94,7 @@ defmodule BDS.UI.Sidebar do
) )
"git" -> "git" ->
git_view() git_view(project_id)
"settings" -> "settings" ->
settings_nav_view() settings_nav_view()
@@ -139,13 +139,17 @@ defmodule BDS.UI.Sidebar do
"import", "import",
[] []
), ),
"git" => git_view(), "git" => git_view(nil),
"settings" => settings_nav_view() "settings" => settings_nav_view()
} }
end end
defp empty_view("posts"), do: build_posts_view([], %{}, false, empty_filter_params(), %{}, [], [], []) defp empty_view("posts"),
defp empty_view("pages"), do: build_posts_view([], %{}, true, empty_filter_params(), %{}, [], [], []) do: build_posts_view([], %{}, false, empty_filter_params(), %{}, [], [], [])
defp empty_view("pages"),
do: build_posts_view([], %{}, true, empty_filter_params(), %{}, [], [], [])
defp empty_view("media"), do: build_media_view([], empty_filter_params(), %{}, [], [], 0) defp empty_view("media"), do: build_media_view([], empty_filter_params(), %{}, [], [], 0)
defp empty_view("scripts"), defp empty_view("scripts"),
@@ -186,7 +190,7 @@ defmodule BDS.UI.Sidebar do
[] []
) )
defp empty_view("git"), do: git_view() defp empty_view("git"), do: git_view(nil)
defp empty_view("settings"), do: settings_nav_view() defp empty_view("settings"), do: settings_nav_view()
defp empty_view(_other), defp empty_view(_other),
@@ -563,7 +567,14 @@ defmodule BDS.UI.Sidebar do
build_media_view(limited_media, filters, tag_colors, year_months, avail_tags, total_count) build_media_view(limited_media, filters, tag_colors, year_months, avail_tags, total_count)
end end
defp build_media_view(limited_media, filters, tag_colors, year_month_counts, available_tags, total_count) do defp build_media_view(
limited_media,
filters,
tag_colors,
year_month_counts,
available_tags,
total_count
) do
loaded_count = length(limited_media) loaded_count = length(limited_media)
%{ %{
@@ -779,24 +790,115 @@ defmodule BDS.UI.Sidebar do
} }
end end
defp git_view do @git_history_page_size 20
%{
defp git_view(project_id) do
base = %{
title: dgettext("ui", "Git"), title: dgettext("ui", "Git"),
subtitle: dgettext("ui", "Working tree and history"), subtitle: dgettext("ui", "Working tree and history"),
layout: "entity_list", layout: "git",
empty_message: dgettext("ui", "No items"), empty_message: dgettext("ui", "No items")
items: [
%{
id: "git-working-tree",
title: dgettext("ui", "Working tree"),
meta: dgettext("ui", "Working tree and history"),
route: "git_diff",
updated_at: nil
} }
]
if git_repo?(project_id) do
Map.merge(base, active_git_view(project_id))
else
Map.merge(base, %{git_state: "not_a_repo", remote_url: nil})
end
end
defp git_repo?(nil), do: false
defp git_repo?(project_id) when is_binary(project_id) do
case BDS.Projects.get_project(project_id) do
nil -> false
project -> File.dir?(Path.join(BDS.Projects.project_data_dir(project), ".git"))
end
end
defp active_git_view(project_id) do
repo =
case BDS.Git.repository(project_id) do
{:ok, repo} -> repo
_other -> %{current_branch: nil, remote_url: nil}
end
branch = repo[:current_branch]
remote =
case BDS.Git.remote_state(project_id) do
{:ok, state} -> state
_other -> %{upstream_branch: nil, ahead: 0, behind: 0}
end
status_files =
case BDS.Git.status(project_id) do
{:ok, %{files: files}} -> Enum.map(files, &git_status_file/1)
_other -> []
end
commits =
if is_binary(branch) do
case BDS.Git.history(project_id, branch) do
{:ok, %{commits: commits}} -> commits
_other -> []
end
else
[]
end
%{
git_state: "active",
branch: branch,
upstream: remote[:upstream_branch],
ahead: remote[:ahead] || 0,
behind: remote[:behind] || 0,
status_files: status_files,
history_entries:
commits |> Enum.take(@git_history_page_size) |> Enum.map(&git_history_entry/1),
has_more_history: length(commits) > @git_history_page_size,
remote_url: repo[:remote_url]
} }
end end
defp git_status_file(%{status: status} = file) do
%{
path: Map.get(file, :path, ""),
status: to_string(status),
code: git_status_code(status),
label: git_status_label(status)
}
end
defp git_status_code(:added), do: "A"
defp git_status_code(:deleted), do: "D"
defp git_status_code(:modified), do: "M"
defp git_status_code(:renamed), do: "R"
defp git_status_code(:untracked), do: "U"
defp git_status_code(_other), do: "M"
defp git_status_label(:added), do: dgettext("ui", "added")
defp git_status_label(:deleted), do: dgettext("ui", "deleted")
defp git_status_label(:modified), do: dgettext("ui", "modified")
defp git_status_label(:renamed), do: dgettext("ui", "renamed")
defp git_status_label(:untracked), do: dgettext("ui", "untracked")
defp git_status_label(_other), do: dgettext("ui", "modified")
defp git_history_entry(commit) do
%{
short_hash: commit |> Map.get(:hash, "") |> String.slice(0, 7),
subject: Map.get(commit, :subject),
author: Map.get(commit, :author),
date: Map.get(commit, :date),
sync_status: git_sync_status(get_in(commit, [:sync_status, :kind]))
}
end
defp git_sync_status(:both), do: "synced"
defp git_sync_status(:local_only), do: "local_only"
defp git_sync_status(:remote_only), do: "remote_only"
defp git_sync_status(_other), do: "synced"
defp entity_list_view(title, subtitle, route, items) do defp entity_list_view(title, subtitle, route, items) do
%{ %{
title: title, title: title,

View File

@@ -33,7 +33,11 @@ defmodule BDS.MixProject do
{:plug, "~> 1.18"}, {:plug, "~> 1.18"},
{:bandit, "~> 1.5"}, {:bandit, "~> 1.5"},
{:desktop, "~> 1.5"}, {:desktop, "~> 1.5"},
{:image, "~> 0.65"}, {:image, "~> 0.67"},
{:nx, "~> 0.10"},
{:exla, "~> 0.10"},
{:bumblebee, "~> 0.6.3"},
{:hnswlib, "~> 0.1.7"},
{:stemex, "~> 0.2.1"}, {:stemex, "~> 0.2.1"},
{:gettext, "~> 0.24"}, {:gettext, "~> 0.24"},
{:tailwind, "~> 0.3", runtime: Mix.env() == :dev}, {:tailwind, "~> 0.3", runtime: Mix.env() == :dev},
@@ -60,7 +64,7 @@ defmodule BDS.MixProject do
env = Mix.env() env = Mix.env()
[ [
plt_add_apps: [:mix, :inets, :ssl], plt_add_apps: [:mix, :inets, :ssl, :nx, :exla, :bumblebee, :hnswlib],
paths: ["_build/#{env}/lib/bds/ebin"] paths: ["_build/#{env}/lib/bds/ebin"]
] ]
end end

View File

@@ -1,12 +1,16 @@
%{ %{
"bandit": {:hex, :bandit, "1.10.4", "02b9734c67c5916a008e7eb7e2ba68aaea6f8177094a5f8d95f1fb99069aac17", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "a5faf501042ac1f31d736d9d4a813b3db4ef812e634583b6a457b0928798a51d"}, "axon": {:hex, :axon, "0.7.0", "2e2c6d93b4afcfa812566b8922204fa022b60081e86ebd411df4db7ea30f5457", [:mix], [{:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}, {:kino_vega_lite, "~> 0.1.7", [hex: :kino_vega_lite, repo: "hexpm", optional: true]}, {:nx, "~> 0.9", [hex: :nx, repo: "hexpm", optional: false]}, {:polaris, "~> 0.1", [hex: :polaris, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1", [hex: :table_rex, repo: "hexpm", optional: true]}], "hexpm", "ee9857a143c9486597ceff434e6ca833dc1241be6158b01025b8217757ed1036"},
"bandit": {:hex, :bandit, "1.11.1", "1eb33123cc3c17ae0c3447874eb83399ee530f960c39711ed240342fbd4865fa", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "d4401016df9abbc6dcd325c0b78b2b193e7c7c96bb68f31e576112be025d84a5"},
"bumblebee": {:hex, :bumblebee, "0.6.3", "c0028643c92de93258a9804da1d4d48797eaf7911b702464b3b3dd2cc7f938f1", [:mix], [{:axon, "~> 0.7.0", [hex: :axon, repo: "hexpm", optional: false]}, {:jason, "~> 1.4.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nx, "~> 0.9.0 or ~> 0.10.0", [hex: :nx, repo: "hexpm", optional: false]}, {:nx_image, "~> 0.1.0", [hex: :nx_image, repo: "hexpm", optional: false]}, {:nx_signal, "~> 0.2.0", [hex: :nx_signal, repo: "hexpm", optional: false]}, {:progress_bar, "~> 3.0", [hex: :progress_bar, repo: "hexpm", optional: false]}, {:safetensors, "~> 0.1.3", [hex: :safetensors, repo: "hexpm", optional: false]}, {:tokenizers, "~> 0.4", [hex: :tokenizers, repo: "hexpm", optional: false]}, {:unpickler, "~> 0.1.0", [hex: :unpickler, repo: "hexpm", optional: false]}, {:unzip, "~> 0.12.0", [hex: :unzip, repo: "hexpm", optional: false]}], "hexpm", "c619197787561f8e5fb2ffba269c341654accaec9d591999b7fddd55761dd079"},
"castore": {:hex, :castore, "1.0.19", "6903cabdfd9d1af46454126e7c8385186659dd33ecfb74a885cae52221ad6109", [:mix], [], "hexpm", "3669e6cab13f54c2df26b3e6833745d647f35b6e30d8ddd5975df0d5c842ca98"},
"cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"},
"color": {:hex, :color, "0.12.0", "f59f9bb6452a460760d44116ec0c1cf86f9d7707c8756c01f83c6d8fe042ae67", [:mix], [{:bandit, "~> 1.5", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "1e17768919dad0bd44f48d0daf294d24bdd5a615bbfe0b4e01a51312203bd294"}, "color": {:hex, :color, "0.13.0", "068110e5397ac5d3c9f97658282e0f4ab9a32468be6d7a2a91a8804e67b228d7", [:mix], [{:bandit, "~> 1.5", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "de127946869931d418bac2d82dc29feae1a8f5f729f135922fbccf0059a58ab2"},
"complex": {:hex, :complex, "0.7.0", "695632ef9487517aa5d57edd1697801079d622414cb2e1a7cf538b1f9a50f205", [:mix], [], "hexpm", "0ee39c0803129f546e7f3f640da8f021c9e659402bf59da6f7f2c4848f068f8d"},
"date_time_parser": {:hex, :date_time_parser, "1.3.0", "6ba16850b5ab83dd126576451023ab65349e29af2336ca5084aa1e37025b476e", [:mix], [{:kday, "~> 1.0", [hex: :kday, repo: "hexpm", optional: false]}], "hexpm", "93c8203a8ddc66b1f1531fc0e046329bf0b250c75ffa09567ef03d2c09218e8c"}, "date_time_parser": {:hex, :date_time_parser, "1.3.0", "6ba16850b5ab83dd126576451023ab65349e29af2336ca5084aa1e37025b476e", [:mix], [{:kday, "~> 1.0", [hex: :kday, repo: "hexpm", optional: false]}], "hexpm", "93c8203a8ddc66b1f1531fc0e046329bf0b250c75ffa09567ef03d2c09218e8c"},
"db_connection": {:hex, :db_connection, "2.9.0", "a6a97c5c958a2d7091a58a9be40caf41ab496b0701d21e1d1abff3fa27a7f371", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "17d502eacaf61829db98facf6f20808ed33da6ccf495354a41e64fe42f9c509c"}, "db_connection": {:hex, :db_connection, "2.9.0", "a6a97c5c958a2d7091a58a9be40caf41ab496b0701d21e1d1abff3fa27a7f371", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "17d502eacaf61829db98facf6f20808ed33da6ccf495354a41e64fe42f9c509c"},
"dbus": {:hex, :dbus, "0.8.0", "7c800681f35d909c199265e55a8ee4aea9ebe4acccce77a0740f89f29cc57648", [:make], [], "hexpm", "a9784f2d9717ffa1f74169144a226c39633ac0d9c7fe8cb3594aeb89c827cca5"}, "dbus": {:hex, :dbus, "0.8.0", "7c800681f35d909c199265e55a8ee4aea9ebe4acccce77a0740f89f29cc57648", [:make], [], "hexpm", "a9784f2d9717ffa1f74169144a226c39633ac0d9c7fe8cb3594aeb89c827cca5"},
"debouncer": {:hex, :debouncer, "0.1.13", "af5906b231c196943ac8386b5b5f45a2f36d54a8bcd7e1b29eef2671de33d287", [:mix], [], "hexpm", "a14f57420c7d4a287f8f08e715fc8759b5d28dcd1032f9585d57c45d22123382"}, "debouncer": {:hex, :debouncer, "0.1.13", "af5906b231c196943ac8386b5b5f45a2f36d54a8bcd7e1b29eef2671de33d287", [:mix], [], "hexpm", "a14f57420c7d4a287f8f08e715fc8759b5d28dcd1032f9585d57c45d22123382"},
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "decimal": {:hex, :decimal, "2.4.1", "6c0fbede12fb122ba685e9ab41c6a40c129e322b3aa192f9e072e61f3a6ffaf2", [:mix], [], "hexpm", "7e618897933a8455f19a727d7c5e50a2c071a544b700e5e724298ecb4340187f"},
"desktop": {:hex, :desktop, "1.5.3", "dcf875dcff5b49a54646b4e6964acb079545c8c9c3790799aa5f1ccdcd314d15", [:mix], [{:debouncer, "~> 0.1", [hex: :debouncer, repo: "hexpm", optional: false]}, {:ex_sni, "~> 0.2", [hex: :ex_sni, repo: "hexpm", optional: false]}, {:gettext, "> 0.10.0", [hex: :gettext, repo: "hexpm", optional: false]}, {:oncrash, "~> 0.1", [hex: :oncrash, repo: "hexpm", optional: false]}, {:phoenix, "> 1.0.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_live_view, "> 0.15.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:plug, "> 1.0.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3750aabb8ed8aaf09b33f3cad5bda20f8ce4dfa65b026c019baed99c5264e2aa"}, "desktop": {:hex, :desktop, "1.5.3", "dcf875dcff5b49a54646b4e6964acb079545c8c9c3790799aa5f1ccdcd314d15", [:mix], [{:debouncer, "~> 0.1", [hex: :debouncer, repo: "hexpm", optional: false]}, {:ex_sni, "~> 0.2", [hex: :ex_sni, repo: "hexpm", optional: false]}, {:gettext, "> 0.10.0", [hex: :gettext, repo: "hexpm", optional: false]}, {:oncrash, "~> 0.1", [hex: :oncrash, repo: "hexpm", optional: false]}, {:phoenix, "> 1.0.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_live_view, "> 0.15.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:plug, "> 1.0.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3750aabb8ed8aaf09b33f3cad5bda20f8ce4dfa65b026c019baed99c5264e2aa"},
"dialyxir": {:hex, :dialyxir, "1.4.7", "dda948fcee52962e4b6c5b4b16b2d8fa7d50d8645bbae8b8685c3f9ecb7f5f4d", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b34527202e6eb8cee198efec110996c25c5898f43a4094df157f8d28f27d9efe"}, "dialyxir": {:hex, :dialyxir, "1.4.7", "dda948fcee52962e4b6c5b4b16b2d8fa7d50d8645bbae8b8685c3f9ecb7f5f4d", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b34527202e6eb8cee198efec110996c25c5898f43a4094df157f8d28f27d9efe"},
"earmark": {:hex, :earmark, "1.4.48", "5f41e579d85ef812351211842b6e005f6e0cef111216dea7d4b9d58af4608434", [:mix], [], "hexpm", "a461a0ddfdc5432381c876af1c86c411fd78a25790c75023c7a4c035fdc858f9"}, "earmark": {:hex, :earmark, "1.4.48", "5f41e579d85ef812351211842b6e005f6e0cef111216dea7d4b9d58af4608434", [:mix], [], "hexpm", "a461a0ddfdc5432381c876af1c86c411fd78a25790c75023c7a4c035fdc858f9"},
@@ -19,15 +23,17 @@
"ex_dbus": {:hex, :ex_dbus, "0.1.4", "053df83d45b27ba0b9b6ef55a47253922069a3ace12a2a7dd30d3aff58301e17", [:mix], [{:dbus, "~> 0.8.0", [hex: :dbus, repo: "hexpm", optional: false]}, {:saxy, "~> 1.4.0", [hex: :saxy, repo: "hexpm", optional: false]}], "hexpm", "d8baeaf465eab57b70a47b70e29fdfef6eb09ba110fc37176eebe6ac7874d6d5"}, "ex_dbus": {:hex, :ex_dbus, "0.1.4", "053df83d45b27ba0b9b6ef55a47253922069a3ace12a2a7dd30d3aff58301e17", [:mix], [{:dbus, "~> 0.8.0", [hex: :dbus, repo: "hexpm", optional: false]}, {:saxy, "~> 1.4.0", [hex: :saxy, repo: "hexpm", optional: false]}], "hexpm", "d8baeaf465eab57b70a47b70e29fdfef6eb09ba110fc37176eebe6ac7874d6d5"},
"ex_sni": {:hex, :ex_sni, "0.2.9", "81f9421035dd3edb6d69f1a4dd5f53c7071b41628130d32ba5ab7bb4bfdc2da0", [:mix], [{:debouncer, "~> 0.1", [hex: :debouncer, repo: "hexpm", optional: false]}, {:ex_dbus, "~> 0.1", [hex: :ex_dbus, repo: "hexpm", optional: false]}, {:saxy, "~> 1.4.0", [hex: :saxy, repo: "hexpm", optional: false]}], "hexpm", "921d67d913765ed20ea8354fd1798dabc957bf66990a6842d6aaa7cd5ee5bc06"}, "ex_sni": {:hex, :ex_sni, "0.2.9", "81f9421035dd3edb6d69f1a4dd5f53c7071b41628130d32ba5ab7bb4bfdc2da0", [:mix], [{:debouncer, "~> 0.1", [hex: :debouncer, repo: "hexpm", optional: false]}, {:ex_dbus, "~> 0.1", [hex: :ex_dbus, repo: "hexpm", optional: false]}, {:saxy, "~> 1.4.0", [hex: :saxy, repo: "hexpm", optional: false]}], "hexpm", "921d67d913765ed20ea8354fd1798dabc957bf66990a6842d6aaa7cd5ee5bc06"},
"ex_stemmers": {:hex, :ex_stemmers, "0.1.0", "63a84ae3a6f0c28a1d75768411f0ae15cfe8462fb70589b60977aa1b04c9372d", [:mix], [{:rustler, "~> 0.32.1", [hex: :rustler, repo: "hexpm", optional: false]}], "hexpm", "498826e2188e502f41d1a15f3d90e7738f0d94747e197367f03a2a44c09167c0"}, "ex_stemmers": {:hex, :ex_stemmers, "0.1.0", "63a84ae3a6f0c28a1d75768411f0ae15cfe8462fb70589b60977aa1b04c9372d", [:mix], [{:rustler, "~> 0.32.1", [hex: :rustler, repo: "hexpm", optional: false]}], "hexpm", "498826e2188e502f41d1a15f3d90e7738f0d94747e197367f03a2a44c09167c0"},
"exla": {:hex, :exla, "0.10.0", "93e7d75a774fbc06ce05b96de20c4b01bda413b315238cb3c727c09a05d2bc3a", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:fine, "~> 0.1.0", [hex: :fine, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:nx, "~> 0.10.0", [hex: :nx, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:xla, "~> 0.9.0", [hex: :xla, repo: "hexpm", optional: false]}], "hexpm", "16fffdb64667d7f0a3bc683fdcd2792b143a9b345e4b1f1d5cd50330c63d8119"},
"expo": {:hex, :expo, "1.1.1", "4202e1d2ca6e2b3b63e02f69cfe0a404f77702b041d02b58597c00992b601db5", [:mix], [], "hexpm", "5fb308b9cb359ae200b7e23d37c76978673aa1b06e2b3075d814ce12c5811640"}, "expo": {:hex, :expo, "1.1.1", "4202e1d2ca6e2b3b63e02f69cfe0a404f77702b041d02b58597c00992b601db5", [:mix], [], "hexpm", "5fb308b9cb359ae200b7e23d37c76978673aa1b06e2b3075d814ce12c5811640"},
"exqlite": {:hex, :exqlite, "0.36.0", "07b4f95d61cb82b8d52946d0639497fa7d32117e09b2c8d25e24a38723c295cb", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "cbeca3ce781f9ff07cfa9a87486f3ebd512a143ad6a14ed5c9fca21fe0bf3ae7"}, "exqlite": {:hex, :exqlite, "0.36.0", "07b4f95d61cb82b8d52946d0639497fa7d32117e09b2c8d25e24a38723c295cb", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "cbeca3ce781f9ff07cfa9a87486f3ebd512a143ad6a14ed5c9fca21fe0bf3ae7"},
"fine": {:hex, :fine, "0.1.6", "4bf7151493443c454aac9f2fa2f34f5fefd0346a83fb5586a016c4a135c63247", [:mix], [], "hexpm", "5638eb4495488e885ebec167fa57973e5c35e1a50c344eb7666c90ec1c4e3b12"}, "fine": {:hex, :fine, "0.1.6", "4bf7151493443c454aac9f2fa2f34f5fefd0346a83fb5586a016c4a135c63247", [:mix], [], "hexpm", "5638eb4495488e885ebec167fa57973e5c35e1a50c344eb7666c90ec1c4e3b12"},
"gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"}, "gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"},
"hnswlib": {:hex, :hnswlib, "0.1.7", "784afdbfbc9af53e64d4b6da3f685c07039e472636a98fa954ffae5292ad6cc4", [:make, :mix], [{:cc_precompiler, "~> 0.1.0", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nx, "~> 0.5", [hex: :nx, repo: "hexpm", optional: false]}], "hexpm", "fb43bb675facc8bb1ef0f4f8fec92479fc23317ed0f35c7160b2f95aff3e4742"},
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
"html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"}, "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"},
"html_sanitize_ex": {:hex, :html_sanitize_ex, "1.4.4", "271455b4d300d5d53a5d92b5bd1c00ad14c5abf1c9ff87be069af5736496515c", [:mix], [{:mochiweb, "~> 2.15 or ~> 3.1", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm", "12e1754204e7db5df1750df0a5dba1bbdf89260800019ab081f2b046596be56b"}, "html_sanitize_ex": {:hex, :html_sanitize_ex, "1.4.4", "271455b4d300d5d53a5d92b5bd1c00ad14c5abf1c9ff87be069af5736496515c", [:mix], [{:mochiweb, "~> 2.15 or ~> 3.1", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm", "12e1754204e7db5df1750df0a5dba1bbdf89260800019ab081f2b046596be56b"},
"image": {:hex, :image, "0.65.0", "44908233a1a0dcdbb6ae873ec09fd9ae533d1840d300d8b0b1b186d586b935e6", [:mix], [{:color, "~> 0.4", [hex: :color, repo: "hexpm", optional: false]}, {:evision, "~> 0.1.33 or ~> 0.2", [hex: :evision, repo: "hexpm", optional: true]}, {:exla, "0.11.0", [hex: :exla, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:kino, "~> 0.13", [hex: :kino, repo: "hexpm", optional: true]}, {:nx, "~> 0.11.0", [hex: :nx, repo: "hexpm", optional: true]}, {:nx_image, "~> 0.1", [hex: :nx_image, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.1 or ~> 3.2 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: true]}, {:req, "~> 0.4", [hex: :req, repo: "hexpm", optional: true]}, {:rustler, "> 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:scholar, "~> 0.3", [hex: :scholar, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: false]}, {:vix, "~> 0.33", [hex: :vix, repo: "hexpm", optional: false]}, {:xav, "~> 0.10", [hex: :xav, repo: "hexpm", optional: true]}], "hexpm", "d2060e08d0f42564f49de1ea97a82a5d237f9ac91edb141dece51f1238dd8b4a"}, "image": {:hex, :image, "0.67.0", "886325f45bd39f3d705d32f223680163f3eaba142526d34f7f871c2232577e64", [:mix], [{:color, "~> 0.13", [hex: :color, repo: "hexpm", optional: false]}, {:evision, "~> 0.1.33 or ~> 0.2", [hex: :evision, repo: "hexpm", optional: true]}, {:exla, "~> 0.10", [hex: :exla, repo: "hexpm", optional: true]}, {:kino, "~> 0.13", [hex: :kino, repo: "hexpm", optional: true]}, {:nx, "~> 0.10", [hex: :nx, repo: "hexpm", optional: true]}, {:nx_image, "~> 0.1", [hex: :nx_image, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.1 or ~> 3.2 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: true]}, {:req, "~> 0.4", [hex: :req, repo: "hexpm", optional: true]}, {:rustler, "> 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:scholar, "~> 0.3", [hex: :scholar, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: false]}, {:vix, "~> 0.33", [hex: :vix, repo: "hexpm", optional: false]}, {:xav, "~> 0.10", [hex: :xav, repo: "hexpm", optional: true]}], "hexpm", "401c3e13137af8932eee377ad8bc8a8ae1a8343894266543e8bedd36b414c999"},
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"},
"kday": {:hex, :kday, "1.1.0", "64efac85279a12283eaaf3ad6f13001ca2dff943eda8c53288179775a8c057a0", [:mix], [{:ex_doc, "~> 0.21", [hex: :ex_doc, repo: "hexpm", optional: true]}], "hexpm", "69703055d63b8d5b260479266c78b0b3e66f7aecdd2022906cd9bf09892a266d"}, "kday": {:hex, :kday, "1.1.0", "64efac85279a12283eaaf3ad6f13001ca2dff943eda8c53288179775a8c057a0", [:mix], [{:ex_doc, "~> 0.21", [hex: :ex_doc, repo: "hexpm", optional: true]}], "hexpm", "69703055d63b8d5b260479266c78b0b3e66f7aecdd2022906cd9bf09892a266d"},
"lazy_html": {:hex, :lazy_html, "0.1.11", "136c8e9cd616b4f4e9c1562daa683880891120b759606dc4c3b6b18058ba5d79", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.9.0", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:fine, "~> 0.1.0", [hex: :fine, repo: "hexpm", optional: false]}], "hexpm", "3b1be592929c31eca1a21673d25696e5c14cddfe922d9d1a3e3b48be4163883b"}, "lazy_html": {:hex, :lazy_html, "0.1.11", "136c8e9cd616b4f4e9c1562daa683880891120b759606dc4c3b6b18058ba5d79", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.9.0", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:fine, "~> 0.1.0", [hex: :fine, repo: "hexpm", optional: false]}], "hexpm", "3b1be592929c31eca1a21673d25696e5c14cddfe922d9d1a3e3b48be4163883b"},
"liquex": {:hex, :liquex, "0.13.1", "49f90d0b85fb2908f2558f35cd49d78497fe77a895eb55b360889940e1d7afb9", [:mix], [{:date_time_parser, "~> 1.2", [hex: :date_time_parser, repo: "hexpm", optional: false]}, {:html_entities, "~> 0.5.2", [hex: :html_entities, repo: "hexpm", optional: false]}, {:html_sanitize_ex, "~> 1.4.3", [hex: :html_sanitize_ex, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "fbea5b9db264c1758a69bfafdcc8aaebcd56e168365bb9575392cd55d800108f"}, "liquex": {:hex, :liquex, "0.13.1", "49f90d0b85fb2908f2558f35cd49d78497fe77a895eb55b360889940e1d7afb9", [:mix], [{:date_time_parser, "~> 1.2", [hex: :date_time_parser, repo: "hexpm", optional: false]}, {:html_entities, "~> 0.5.2", [hex: :html_entities, repo: "hexpm", optional: false]}, {:html_sanitize_ex, "~> 1.4.3", [hex: :html_sanitize_ex, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "fbea5b9db264c1758a69bfafdcc8aaebcd56e168365bb9575392cd55d800108f"},
@@ -35,23 +41,35 @@
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
"mochiweb": {:hex, :mochiweb, "3.3.0", "2898ad0bfeee234e4cbae623c7052abc3ff0d73d499ba6e6ffef445b13ffd07a", [:rebar3], [], "hexpm", "aa85b777fb23e9972ebc424e40b5d35106f19bc998873e026dedd876df8ee50c"}, "mochiweb": {:hex, :mochiweb, "3.3.0", "2898ad0bfeee234e4cbae623c7052abc3ff0d73d499ba6e6ffef445b13ffd07a", [:rebar3], [], "hexpm", "aa85b777fb23e9972ebc424e40b5d35106f19bc998873e026dedd876df8ee50c"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
"nx": {:hex, :nx, "0.10.0", "128e4a094cb790f663e20e1334b127c1f2a4df54edfb8b13c22757ec33133b4f", [:mix], [{:complex, "~> 0.6", [hex: :complex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3db8892c124aeee091df0e6fbf8e5bf1b81f502eb0d4f5ba63e6378ebcae7da4"},
"nx_image": {:hex, :nx_image, "0.1.2", "0c6e3453c1dc30fc80c723a54861204304cebc8a89ed3b806b972c73ee5d119d", [:mix], [{:nx, "~> 0.4", [hex: :nx, repo: "hexpm", optional: false]}], "hexpm", "9161863c42405ddccb6dbbbeae078ad23e30201509cc804b3b3a7c9e98764b81"},
"nx_signal": {:hex, :nx_signal, "0.2.0", "e1ca0318877b17c81ce8906329f5125f1e2361e4c4235a5baac8a95ee88ea98e", [:mix], [{:nx, "~> 0.6", [hex: :nx, repo: "hexpm", optional: false]}], "hexpm", "7247e5e18a177a59c4cb5355952900c62fdeadeb2bad02a9a34237b68744e2bb"},
"oncrash": {:hex, :oncrash, "0.1.0", "9cf4ae8eba4ea250b579470172c5e9b8c75418b2264de7dbcf42e408d62e30fb", [:mix], [], "hexpm", "6968e775491cd857f9b6ff940bf2574fd1c2fab84fa7e14d5f56c39174c00018"}, "oncrash": {:hex, :oncrash, "0.1.0", "9cf4ae8eba4ea250b579470172c5e9b8c75418b2264de7dbcf42e408d62e30fb", [:mix], [], "hexpm", "6968e775491cd857f9b6ff940bf2574fd1c2fab84fa7e14d5f56c39174c00018"},
"phoenix": {:hex, :phoenix, "1.8.5", "919db335247e6d4891764dc3063415b0d2457641c5f9b3751b5df03d8e20bbcf", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "83b2bb125127e02e9f475c8e3e92736325b5b01b0b9b05407bcb4083b7a32485"}, "phoenix": {:hex, :phoenix, "1.8.5", "919db335247e6d4891764dc3063415b0d2457641c5f9b3751b5df03d8e20bbcf", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "83b2bb125127e02e9f475c8e3e92736325b5b01b0b9b05407bcb4083b7a32485"},
"phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"}, "phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"},
"phoenix_live_view": {:hex, :phoenix_live_view, "1.1.28", "8a8e123d018025f756605a2fb02a4854f0d3cd7b207f710fef1fd5d9d72d0254", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "24faad535b65089642c3a7d84088109dc58f49c1f1c5a978659855d643466353"}, "phoenix_live_view": {:hex, :phoenix_live_view, "1.1.28", "8a8e123d018025f756605a2fb02a4854f0d3cd7b207f710fef1fd5d9d72d0254", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "24faad535b65089642c3a7d84088109dc58f49c1f1c5a978659855d643466353"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"},
"phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
"plug": {:hex, :plug, "1.19.1", "09bac17ae7a001a68ae393658aa23c7e38782be5c5c00c80be82901262c394c0", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "560a0017a8f6d5d30146916862aaf9300b7280063651dd7e532b8be168511e62"}, "plug": {:hex, :plug, "1.19.2", "e4950525b22c6789dfb38a3f95d47171ba159da3fc5a33be9643b43d5e8adb98", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b6fce20a56af5e60fa5dfecf3f907bb98ec981be43c79a3809a499bc3d133de0"},
"plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
"polaris": {:hex, :polaris, "0.1.0", "dca61b18e3e801ecdae6ac9f0eca5f19792b44a5cb4b8d63db50fc40fc038d22", [:mix], [{:nx, "~> 0.5", [hex: :nx, repo: "hexpm", optional: false]}], "hexpm", "13ef2b166650e533cb24b10e2f3b8ab4f2f449ba4d63156e8c569527f206e2c2"},
"progress_bar": {:hex, :progress_bar, "3.0.0", "f54ff038c2ac540cfbb4c2bfe97c75e7116ead044f3c2b10c9f212452194b5cd", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "6981c2b25ab24aecc91a2dc46623658e1399c21a2ae24db986b90d678530f2b7"},
"rustler": {:hex, :rustler, "0.32.1", "f4cf5a39f9e85d182c0a3f75fa15b5d0add6542ab0bf9ceac6b4023109ebd3fc", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.6", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "b96be75526784f86f6587f051bc8d6f4eaff23d6e0f88dbcfe4d5871f52946f7"}, "rustler": {:hex, :rustler, "0.32.1", "f4cf5a39f9e85d182c0a3f75fa15b5d0add6542ab0bf9ceac6b4023109ebd3fc", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.6", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "b96be75526784f86f6587f051bc8d6f4eaff23d6e0f88dbcfe4d5871f52946f7"},
"rustler_precompiled": {:hex, :rustler_precompiled, "0.9.0", "3a052eda09f3d2436364645cc1f13279cf95db310eb0c17b0d8f25484b233aa0", [:mix], [{:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "471d97315bd3bf7b64623418b3693eedd8e47de3d1cb79a0ac8f9da7d770d94c"},
"safetensors": {:hex, :safetensors, "0.1.3", "7ff3c22391e213289c713898481d492c9c28a49ab1d0705b72630fb8360426b2", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:nx, "~> 0.5", [hex: :nx, repo: "hexpm", optional: false]}], "hexpm", "fe50b53ea59fde4e723dd1a2e31cfdc6013e69343afac84c6be86d6d7c562c14"},
"saxy": {:hex, :saxy, "1.4.0", "c7203ad20001f72eaaad07d08f82be063fa94a40924e6bb39d93d55f979abcba", [:mix], [], "hexpm", "3fe790354d3f2234ad0b5be2d99822a23fa2d4e8ccd6657c672901dac172e9a9"}, "saxy": {:hex, :saxy, "1.4.0", "c7203ad20001f72eaaad07d08f82be063fa94a40924e6bb39d93d55f979abcba", [:mix], [], "hexpm", "3fe790354d3f2234ad0b5be2d99822a23fa2d4e8ccd6657c672901dac172e9a9"},
"stemex": {:hex, :stemex, "0.2.1", "47017c6b10cdd6926a0d523ccf1f801c5f3faf5a0a9c862f49304e07f9b5584f", [:mix], [], "hexpm", "dbfc76d27adfa31d831d183979c595942884e6530a4496714aa5b70d0964c2e4"}, "stemex": {:hex, :stemex, "0.2.1", "47017c6b10cdd6926a0d523ccf1f801c5f3faf5a0a9c862f49304e07f9b5584f", [:mix], [], "hexpm", "dbfc76d27adfa31d831d183979c595942884e6530a4496714aa5b70d0964c2e4"},
"sweet_xml": {:hex, :sweet_xml, "0.7.5", "803a563113981aaac202a1dbd39771562d0ad31004ddbfc9b5090bdcd5605277", [:mix], [], "hexpm", "193b28a9b12891cae351d81a0cead165ffe67df1b73fe5866d10629f4faefb12"}, "sweet_xml": {:hex, :sweet_xml, "0.7.5", "803a563113981aaac202a1dbd39771562d0ad31004ddbfc9b5090bdcd5605277", [:mix], [], "hexpm", "193b28a9b12891cae351d81a0cead165ffe67df1b73fe5866d10629f4faefb12"},
"tailwind": {:hex, :tailwind, "0.4.1", "e7bcc222fe96a1e55f948e76d13dd84a1a7653fb051d2a167135db3b4b08d3e9", [:mix], [], "hexpm", "6249d4f9819052911120dbdbe9e532e6bd64ea23476056adb7f730aa25c220d1"}, "tailwind": {:hex, :tailwind, "0.4.1", "e7bcc222fe96a1e55f948e76d13dd84a1a7653fb051d2a167135db3b4b08d3e9", [:mix], [], "hexpm", "6249d4f9819052911120dbdbe9e532e6bd64ea23476056adb7f730aa25c220d1"},
"telemetry": {:hex, :telemetry, "1.4.1", "ab6de178e2b29b58e8256b92b382ea3f590a47152ca3651ea857a6cae05ac423", [:rebar3], [], "hexpm", "2172e05a27531d3d31dd9782841065c50dd5c3c7699d95266b2edd54c2dafa1c"}, "telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"},
"thousand_island": {:hex, :thousand_island, "1.4.3", "2158209580f633be38d43ec4e3ce0a01079592b9657afff9080d5d8ca149a3af", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e4ce09b0fd761a58594d02814d40f77daff460c48a7354a15ab353bb998ea0b"}, "thousand_island": {:hex, :thousand_island, "1.4.3", "2158209580f633be38d43ec4e3ce0a01079592b9657afff9080d5d8ca149a3af", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e4ce09b0fd761a58594d02814d40f77daff460c48a7354a15ab353bb998ea0b"},
"tokenizers": {:hex, :tokenizers, "0.5.1", "b0975d92b4ee5b18e8f47b5d65b9d5f1e583d9130189b1a2620401af4e7d4b35", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.6", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "5f08d97cc7f2ed3d71d370d68120da6d3de010948ccf676c9c0eb591ba4bacc9"},
"toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"}, "toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"},
"unpickler": {:hex, :unpickler, "0.1.0", "c2262c0819e6985b761e7107546cef96a485f401816be5304a65fdd200d5bd6a", [:mix], [], "hexpm", "e2b3f61e62406187ac52afead8a63bfb4e49394028993f3c4c42712743cab79e"},
"unzip": {:hex, :unzip, "0.12.0", "beed92238724732418b41eba77dcb7f51e235b707406c05b1732a3052d1c0f36", [:mix], [], "hexpm", "95655b72db368e5a84951f0bed586ac053b55ee3815fd96062fce10ce4fc998d"},
"vix": {:hex, :vix, "0.38.0", "77529ee4f6ced339c3d5f90a9eacf306f5b7109d3d1b5e3ef391a984ad404f75", [:make, :mix], [{:cc_precompiler, "~> 0.1.4 or ~> 0.2", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7.3 or ~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}], "hexpm", "dca58f654922fa678d5df8e028317483d9c0f8acb2e2714076a8468695687aa7"}, "vix": {:hex, :vix, "0.38.0", "77529ee4f6ced339c3d5f90a9eacf306f5b7109d3d1b5e3ef391a984ad404f75", [:make, :mix], [{:cc_precompiler, "~> 0.1.4 or ~> 0.2", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7.3 or ~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}], "hexpm", "dca58f654922fa678d5df8e028317483d9c0f8acb2e2714076a8468695687aa7"},
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
"websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"}, "websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"},
"xla": {:hex, :xla, "0.9.1", "cca0040ff94902764007a118871bfc667f1a0085d4a5074533a47d6b58bec61e", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "eb5e443ae5391b1953f253e051f2307bea183b59acee138053a9300779930daf"},
} }

File diff suppressed because one or more lines are too long

View File

@@ -15,7 +15,7 @@
</svg> </svg>
</button> </button>
<div class="blog-search-panel" data-blog-search-panel hidden> <div class="blog-search-panel" data-blog-search-panel hidden>
<div id="blog-search" data-blog-search-root data-search-placeholder="{{ labels.search_placeholder }}"></div> <div id="blog-search" data-blog-search-root data-search-placeholder="{{ labels.search_placeholder }}" data-search-no-results="{{ labels.search_no_results }}"></div>
</div> </div>
</div> </div>
</nav> </nav>
@@ -35,7 +35,7 @@
</svg> </svg>
</button> </button>
<div class="blog-search-panel" data-blog-search-panel hidden> <div class="blog-search-panel" data-blog-search-panel hidden>
<div id="blog-search" data-blog-search-root data-search-placeholder="{{ labels.search_placeholder }}"></div> <div id="blog-search" data-blog-search-root data-search-placeholder="{{ labels.search_placeholder }}" data-search-no-results="{{ labels.search_no_results }}"></div>
</div> </div>
</div> </div>
{% endif %} {% endif %}

View File

@@ -5,7 +5,7 @@
msgid "Archive" msgid "Archive"
msgstr "Archiv" msgstr "Archiv"
#: lib/bds/rendering/labels.ex:54 #: lib/bds/rendering/labels.ex:55
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "April" msgid "April"
msgstr "Apr." msgstr "Apr."
@@ -15,7 +15,7 @@ msgstr "Apr."
msgid "Archive calendar" msgid "Archive calendar"
msgstr "Archiv" msgstr "Archiv"
#: lib/bds/rendering/labels.ex:70 #: lib/bds/rendering/labels.ex:71
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "August" msgid "August"
msgstr "Aug." msgstr "Aug."
@@ -35,27 +35,27 @@ msgstr "Kalenderdaten konnten nicht geladen werden."
msgid "Close calendar" msgid "Close calendar"
msgstr "Kalender schließen" msgstr "Kalender schließen"
#: lib/bds/rendering/labels.ex:86 #: lib/bds/rendering/labels.ex:87
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "December" msgid "December"
msgstr "Dezember" msgstr "Dezember"
#: lib/bds/rendering/labels.ex:46 #: lib/bds/rendering/labels.ex:47
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "February" msgid "February"
msgstr "Februar" msgstr "Februar"
#: lib/bds/rendering/labels.ex:42 #: lib/bds/rendering/labels.ex:43
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "January" msgid "January"
msgstr "Januar" msgstr "Januar"
#: lib/bds/rendering/labels.ex:66 #: lib/bds/rendering/labels.ex:67
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "July" msgid "July"
msgstr "Juli" msgstr "Juli"
#: lib/bds/rendering/labels.ex:62 #: lib/bds/rendering/labels.ex:63
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "June" msgid "June"
msgstr "Juni" msgstr "Juni"
@@ -75,22 +75,22 @@ msgstr "Verlinkt von"
msgid "Loading calendar…" msgid "Loading calendar…"
msgstr "Kalender wird geladen …" msgstr "Kalender wird geladen …"
#: lib/bds/rendering/labels.ex:50 #: lib/bds/rendering/labels.ex:51
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "March" msgid "March"
msgstr "März" msgstr "März"
#: lib/bds/rendering/labels.ex:58 #: lib/bds/rendering/labels.ex:59
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "May" msgid "May"
msgstr "Mai" msgstr "Mai"
#: lib/bds/rendering/labels.ex:82 #: lib/bds/rendering/labels.ex:83
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "November" msgid "November"
msgstr "Nov." msgstr "Nov."
#: lib/bds/rendering/labels.ex:78 #: lib/bds/rendering/labels.ex:79
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "October" msgid "October"
msgstr "Oktober" msgstr "Oktober"
@@ -110,7 +110,7 @@ msgstr "Seitennummerierung"
msgid "Search..." msgid "Search..."
msgstr "Suchen..." msgstr "Suchen..."
#: lib/bds/rendering/labels.ex:74 #: lib/bds/rendering/labels.ex:75
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "September" msgid "September"
msgstr "Sept." msgstr "Sept."
@@ -135,22 +135,27 @@ msgstr "neuer"
msgid "older" msgid "older"
msgstr "älter" msgstr "älter"
#: lib/bds/rendering/labels.ex:31 #: lib/bds/rendering/labels.ex:32
#, 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:30 #: lib/bds/rendering/labels.ex:31
#, 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:33 #: lib/bds/rendering/labels.ex:34
#, 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:32 #: lib/bds/rendering/labels.ex:33
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "YouTube video" msgid "YouTube video"
msgstr "YouTube-Video" msgstr "YouTube-Video"
#: lib/bds/rendering/labels.ex:30
#, elixir-autogen, elixir-format
msgid "No results found"
msgstr "Keine Ergebnisse gefunden"

View File

@@ -79,7 +79,7 @@ msgstr "KI-Einstellungen"
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:42 #: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:42
#: lib/bds/desktop/shell_live/overlay_manager.ex:72 #: lib/bds/desktop/shell_live/overlay_manager.ex:72
#: lib/bds/desktop/shell_live/post_editor.ex:776 #: lib/bds/desktop/shell_live/post_editor.ex:781
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:43 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:43
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "AI Suggestions" msgid "AI Suggestions"
@@ -257,14 +257,14 @@ msgid "Auto"
msgstr "Automatisch" msgstr "Automatisch"
#: lib/bds/desktop/shell_data.ex:98 #: lib/bds/desktop/shell_data.ex:98
#: lib/bds/desktop/shell_live.ex:409 #: lib/bds/desktop/shell_live.ex:430
#: lib/bds/desktop/shell_live/chat_editor.ex:231 #: lib/bds/desktop/shell_live/chat_editor.ex:231
#: lib/bds/desktop/shell_live/media_editor.ex:156 #: lib/bds/desktop/shell_live/media_editor.ex:156
#: lib/bds/desktop/shell_live/media_editor.ex:349 #: lib/bds/desktop/shell_live/media_editor.ex:349
#: lib/bds/desktop/shell_live/media_editor.ex:538 #: lib/bds/desktop/shell_live/media_editor.ex:538
#: lib/bds/desktop/shell_live/overlay_manager.ex:73 #: lib/bds/desktop/shell_live/overlay_manager.ex:73
#: lib/bds/desktop/shell_live/post_editor.ex:643 #: lib/bds/desktop/shell_live/post_editor.ex:648
#: lib/bds/desktop/shell_live/post_editor.ex:692 #: lib/bds/desktop/shell_live/post_editor.ex:697
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Automatic AI actions stay gated by airplane mode." msgid "Automatic AI actions stay gated by airplane mode."
msgstr "Automatische KI-Aktionen bleiben durch den Flugmodus gesperrt." msgstr "Automatische KI-Aktionen bleiben durch den Flugmodus gesperrt."
@@ -404,7 +404,7 @@ msgstr "Kategorie-Standards, Render-Flags und Template-Zuordnung"
msgid "Category name is required" msgid "Category name is required"
msgstr "Kategoriename ist erforderlich" msgstr "Kategoriename ist erforderlich"
#: lib/bds/desktop/shell_live.ex:932 #: lib/bds/desktop/shell_live.ex:953
#: lib/bds/desktop/shell_live/chat_editor.ex:87 #: lib/bds/desktop/shell_live/chat_editor.ex:87
#: lib/bds/desktop/shell_live/chat_editor.ex:230 #: lib/bds/desktop/shell_live/chat_editor.ex:230
#: lib/bds/desktop/shell_live/chat_editor.ex:318 #: lib/bds/desktop/shell_live/chat_editor.ex:318
@@ -537,7 +537,7 @@ msgstr "MCP-Konfiguration %{path} konnte nicht gelesen werden: %{reason}"
msgid "Could not write MCP config %{path}: %{reason}" msgid "Could not write MCP config %{path}: %{reason}"
msgstr "MCP-Konfiguration %{path} konnte nicht geschrieben werden: %{reason}" msgstr "MCP-Konfiguration %{path} konnte nicht geschrieben werden: %{reason}"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:42 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:48
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Create" msgid "Create"
msgstr "Erstellen" msgstr "Erstellen"
@@ -636,12 +636,12 @@ msgstr "Standard-Bearbeitungsmodus und Diff-Darstellung"
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:166 #: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:166
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:116 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:116
#: lib/bds/desktop/shell_live/script_editor_html/script_editor.html.heex:16 #: lib/bds/desktop/shell_live/script_editor_html/script_editor.html.heex:16
#: lib/bds/desktop/shell_live/sidebar_components.ex:515 #: lib/bds/desktop/shell_live/sidebar_components.ex:651
#: lib/bds/desktop/shell_live/sidebar_components.ex:518 #: lib/bds/desktop/shell_live/sidebar_components.ex:654
#: lib/bds/desktop/shell_live/sidebar_components.ex:521 #: lib/bds/desktop/shell_live/sidebar_components.ex:657
#: lib/bds/desktop/shell_live/sidebar_components.ex:524 #: lib/bds/desktop/shell_live/sidebar_components.ex:660
#: lib/bds/desktop/shell_live/sidebar_components.ex:527 #: lib/bds/desktop/shell_live/sidebar_components.ex:663
#: lib/bds/desktop/shell_live/sidebar_components.ex:529 #: lib/bds/desktop/shell_live/sidebar_components.ex:665
#: lib/bds/desktop/shell_live/sidebar_delete.ex:72 #: lib/bds/desktop/shell_live/sidebar_delete.ex:72
#: lib/bds/desktop/shell_live/sidebar_delete.ex:174 #: lib/bds/desktop/shell_live/sidebar_delete.ex:174
#: lib/bds/desktop/shell_live/sidebar_delete.ex:175 #: lib/bds/desktop/shell_live/sidebar_delete.ex:175
@@ -649,7 +649,7 @@ msgstr "Standard-Bearbeitungsmodus und Diff-Darstellung"
#: lib/bds/desktop/shell_live/sidebar_delete.ex:179 #: lib/bds/desktop/shell_live/sidebar_delete.ex:179
#: lib/bds/desktop/shell_live/sidebar_delete.ex:181 #: lib/bds/desktop/shell_live/sidebar_delete.ex:181
#: lib/bds/desktop/shell_live/sidebar_delete.ex:182 #: lib/bds/desktop/shell_live/sidebar_delete.ex:182
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:58 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:70
#: lib/bds/desktop/shell_live/template_editor_html/template_editor.html.heex:15 #: lib/bds/desktop/shell_live/template_editor_html/template_editor.html.heex:15
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Delete" msgid "Delete"
@@ -665,7 +665,7 @@ msgstr "Medium loeschen"
msgid "Delete Translation" msgid "Delete Translation"
msgstr "Übersetzung löschen" msgstr "Übersetzung löschen"
#: lib/bds/desktop/shell_live/sidebar_components.ex:514 #: lib/bds/desktop/shell_live/sidebar_components.ex:650
#: lib/bds/desktop/shell_live/sidebar_delete.ex:173 #: lib/bds/desktop/shell_live/sidebar_delete.ex:173
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Delete conversation" msgid "Delete conversation"
@@ -706,9 +706,9 @@ msgstr "Erkennen"
#: lib/bds/desktop/shell_live/media_editor.ex:199 #: lib/bds/desktop/shell_live/media_editor.ex:199
#: lib/bds/desktop/shell_live/media_editor.ex:205 #: lib/bds/desktop/shell_live/media_editor.ex:205
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:59 #: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:59
#: lib/bds/desktop/shell_live/post_editor.ex:642 #: lib/bds/desktop/shell_live/post_editor.ex:647
#: lib/bds/desktop/shell_live/post_editor.ex:671 #: lib/bds/desktop/shell_live/post_editor.ex:676
#: lib/bds/desktop/shell_live/post_editor.ex:677 #: lib/bds/desktop/shell_live/post_editor.ex:682
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Detect Language" msgid "Detect Language"
msgstr "Sprache erkennen" msgstr "Sprache erkennen"
@@ -749,7 +749,7 @@ msgid "Discard changes and restore the published version"
msgstr "Änderungen verwerfen und veröffentlichte Version wiederherstellen" msgstr "Änderungen verwerfen und veröffentlichte Version wiederherstellen"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:21 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:21
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:84 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:96
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Discover" msgid "Discover"
msgstr "Entdecken" msgstr "Entdecken"
@@ -991,8 +991,8 @@ msgstr "Galerie"
msgid "Generate Site" msgid "Generate Site"
msgstr "Website generieren" msgstr "Website generieren"
#: lib/bds/desktop/shell_live.ex:933 #: lib/bds/desktop/shell_live.ex:954
#: lib/bds/ui/sidebar.ex:784 #: lib/bds/ui/sidebar.ex:786
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Git" msgid "Git"
msgstr "Git" msgstr "Git"
@@ -1004,7 +1004,7 @@ msgid "Git Diff"
msgstr "Git-Diff" msgstr "Git-Diff"
#: lib/bds/desktop/shell_data.ex:244 #: lib/bds/desktop/shell_data.ex:244
#: lib/bds/desktop/shell_live.ex:929 #: lib/bds/desktop/shell_live.ex:950
#: lib/bds/desktop/shell_live/panel_renderer.ex:171 #: lib/bds/desktop/shell_live/panel_renderer.ex:171
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Git Log" msgid "Git Log"
@@ -1033,7 +1033,7 @@ msgstr "Host"
#: lib/bds/desktop/shell_data.ex:116 #: lib/bds/desktop/shell_data.ex:116
#: lib/bds/desktop/shell_live/index.html.heex:666 #: lib/bds/desktop/shell_live/index.html.heex:666
#: lib/bds/desktop/shell_live/media_editor.ex:703 #: lib/bds/desktop/shell_live/media_editor.ex:703
#: lib/bds/desktop/shell_live/post_editor.ex:894 #: lib/bds/desktop/shell_live/post_editor.ex:899
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Idle" msgid "Idle"
msgstr "Leerlauf" msgstr "Leerlauf"
@@ -1075,7 +1075,7 @@ msgstr "Bilder und Dateien"
#: lib/bds/desktop/shell_live/import_editor/taxonomy_editing.ex:127 #: lib/bds/desktop/shell_live/import_editor/taxonomy_editing.ex:127
#: lib/bds/desktop/shell_live/import_editor/taxonomy_editing.ex:137 #: lib/bds/desktop/shell_live/import_editor/taxonomy_editing.ex:137
#: lib/bds/desktop/shell_live/misc_editor_html/misc_editor.html.heex:182 #: lib/bds/desktop/shell_live/misc_editor_html/misc_editor.html.heex:182
#: lib/bds/desktop/shell_live/sidebar_components.ex:527 #: lib/bds/desktop/shell_live/sidebar_components.ex:663
#: lib/bds/desktop/shell_live/sidebar_delete.ex:181 #: lib/bds/desktop/shell_live/sidebar_delete.ex:181
#: lib/bds/ui/registry.ex:70 #: lib/bds/ui/registry.ex:70
#: lib/bds/ui/registry.ex:105 #: lib/bds/ui/registry.ex:105
@@ -1127,9 +1127,9 @@ msgstr "Importdefinitionen"
msgid "Import failed: %{error}" msgid "Import failed: %{error}"
msgstr "Import fehlgeschlagen: %{error}" msgstr "Import fehlgeschlagen: %{error}"
#: lib/bds/desktop/shell_live.ex:580 #: lib/bds/desktop/shell_live.ex:601
#: lib/bds/desktop/shell_live.ex:970 #: lib/bds/desktop/shell_live.ex:991
#: lib/bds/desktop/shell_live.ex:976 #: lib/bds/desktop/shell_live.ex:997
#: lib/bds/desktop/shell_live/sidebar_create.ex:47 #: lib/bds/desktop/shell_live/sidebar_create.ex:47
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Import media" msgid "Import media"
@@ -1204,7 +1204,7 @@ msgid "Language"
msgstr "Sprache" msgstr "Sprache"
#: lib/bds/desktop/shell_live/media_editor.ex:206 #: lib/bds/desktop/shell_live/media_editor.ex:206
#: lib/bds/desktop/shell_live/post_editor.ex:678 #: lib/bds/desktop/shell_live/post_editor.ex:683
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Language detection failed." msgid "Language detection failed."
msgstr "Spracherkennung fehlgeschlagen." msgstr "Spracherkennung fehlgeschlagen."
@@ -1288,7 +1288,7 @@ msgstr "Zuordnen zu..."
msgid "Mapped" msgid "Mapped"
msgstr "Zugeordnet" msgstr "Zugeordnet"
#: lib/bds/desktop/shell_live/post_editor.ex:897 #: lib/bds/desktop/shell_live/post_editor.ex:902
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:120 #: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:120
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Markdown" msgid "Markdown"
@@ -1304,7 +1304,7 @@ msgstr "Maximale Beiträge pro Seite"
#: lib/bds/desktop/shell_live/media_editor.ex:494 #: lib/bds/desktop/shell_live/media_editor.ex:494
#: lib/bds/desktop/shell_live/misc_editor.ex:744 #: lib/bds/desktop/shell_live/misc_editor.ex:744
#: lib/bds/desktop/shell_live/misc_editor.ex:771 #: lib/bds/desktop/shell_live/misc_editor.ex:771
#: lib/bds/desktop/shell_live/sidebar_components.ex:518 #: lib/bds/desktop/shell_live/sidebar_components.ex:654
#: lib/bds/desktop/shell_live/sidebar_delete.ex:175 #: lib/bds/desktop/shell_live/sidebar_delete.ex:175
#: lib/bds/ui/registry.ex:30 #: lib/bds/ui/registry.ex:30
#: lib/bds/ui/registry.ex:100 #: lib/bds/ui/registry.ex:100
@@ -1328,12 +1328,12 @@ msgstr "Medium gespeichert"
msgid "Menu" msgid "Menu"
msgstr "Menü" msgstr "Menü"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:75 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:87
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Merge" msgid "Merge"
msgstr "Zusammenführen" msgstr "Zusammenführen"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:66 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:78
#: lib/bds/ui/sidebar.ex:747 #: lib/bds/ui/sidebar.ex:747
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Merge Tags" msgid "Merge Tags"
@@ -1424,7 +1424,7 @@ msgstr "Neues Untermenü"
msgid "New Template" msgid "New Template"
msgstr "Neue Vorlage" msgstr "Neue Vorlage"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:52 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:64
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "No Template" msgid "No Template"
msgstr "Kein Template" msgstr "Kein Template"
@@ -1453,12 +1453,12 @@ msgstr "Noch keine Git-Historie"
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:72 #: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:72
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:308 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:308
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:320 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:320
#: lib/bds/desktop/shell_live/sidebar_components.ex:320 #: lib/bds/desktop/shell_live/sidebar_components.ex:321
#: lib/bds/desktop/shell_live/sidebar_components.ex:380 #: lib/bds/desktop/shell_live/sidebar_components.ex:381
#: lib/bds/desktop/shell_live/sidebar_components.ex:454 #: lib/bds/desktop/shell_live/sidebar_components.ex:455
#: lib/bds/ui/sidebar.ex:198 #: lib/bds/ui/sidebar.ex:198
#: lib/bds/ui/sidebar.ex:787 #: lib/bds/ui/sidebar.ex:789
#: lib/bds/ui/sidebar.ex:805 #: lib/bds/ui/sidebar.ex:896
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "No items" msgid "No items"
msgstr "Keine Einträge" msgstr "Keine Einträge"
@@ -1721,7 +1721,7 @@ msgstr "Sonstige"
msgid "Other (%{count})" msgid "Other (%{count})"
msgstr "Andere (%{count})" msgstr "Andere (%{count})"
#: lib/bds/desktop/shell_live.ex:928 #: lib/bds/desktop/shell_live.ex:949
#: lib/bds/desktop/shell_live/panel_renderer.ex:83 #: lib/bds/desktop/shell_live/panel_renderer.ex:83
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Output" msgid "Output"
@@ -1786,17 +1786,17 @@ msgid "Persist the detected language for this media item"
msgstr "Die erkannte Sprache für dieses Medium speichern" msgstr "Die erkannte Sprache für dieses Medium speichern"
#: lib/bds/desktop/shell_live/misc_editor.ex:742 #: lib/bds/desktop/shell_live/misc_editor.ex:742
#: lib/bds/desktop/shell_live/post_editor.ex:474 #: lib/bds/desktop/shell_live/post_editor.ex:479
#: lib/bds/desktop/shell_live/post_editor.ex:478 #: lib/bds/desktop/shell_live/post_editor.ex:483
#: lib/bds/desktop/shell_live/post_editor.ex:513 #: lib/bds/desktop/shell_live/post_editor.ex:518
#: lib/bds/desktop/shell_live/post_editor.ex:517 #: lib/bds/desktop/shell_live/post_editor.ex:522
#: lib/bds/desktop/shell_live/post_editor.ex:552 #: lib/bds/desktop/shell_live/post_editor.ex:557
#: lib/bds/desktop/shell_live/post_editor.ex:567 #: lib/bds/desktop/shell_live/post_editor.ex:572
#: lib/bds/desktop/shell_live/post_editor.ex:596 #: lib/bds/desktop/shell_live/post_editor.ex:601
#: lib/bds/desktop/shell_live/post_editor.ex:599 #: lib/bds/desktop/shell_live/post_editor.ex:604
#: lib/bds/desktop/shell_live/post_editor.ex:629 #: lib/bds/desktop/shell_live/post_editor.ex:634
#: lib/bds/desktop/shell_live/post_editor.ex:632 #: lib/bds/desktop/shell_live/post_editor.ex:637
#: lib/bds/desktop/shell_live/sidebar_components.ex:515 #: lib/bds/desktop/shell_live/sidebar_components.ex:651
#: lib/bds/desktop/shell_live/sidebar_delete.ex:174 #: lib/bds/desktop/shell_live/sidebar_delete.ex:174
#: lib/bds/ui/registry.ex:99 #: lib/bds/ui/registry.ex:99
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@@ -1825,12 +1825,12 @@ msgstr "Beitragsvorlage"
msgid "Post is marked as do-not-translate but has translations" msgid "Post is marked as do-not-translate but has translations"
msgstr "Beitrag ist als nicht-übersetzen markiert, hat aber Übersetzungen" msgstr "Beitrag ist als nicht-übersetzen markiert, hat aber Übersetzungen"
#: lib/bds/desktop/shell_live/post_editor.ex:513 #: lib/bds/desktop/shell_live/post_editor.ex:518
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Post published" msgid "Post published"
msgstr "Beitrag veröffentlicht" msgstr "Beitrag veröffentlicht"
#: lib/bds/desktop/shell_live/post_editor.ex:474 #: lib/bds/desktop/shell_live/post_editor.ex:479
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Post saved" msgid "Post saved"
msgstr "Beitrag gespeichert" msgstr "Beitrag gespeichert"
@@ -1854,7 +1854,7 @@ msgstr "Beiträge (%{count})"
msgid "Preferences" msgid "Preferences"
msgstr "Einstellungen" msgstr "Einstellungen"
#: lib/bds/desktop/shell_live/post_editor.ex:898 #: lib/bds/desktop/shell_live/post_editor.ex:903
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:121 #: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:121
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Preview" msgid "Preview"
@@ -1923,7 +1923,7 @@ msgid "Publish Selected"
msgstr "Ausgewähltes veröffentlichen" msgstr "Ausgewähltes veröffentlichen"
#: lib/bds/desktop/shell_data.ex:181 #: lib/bds/desktop/shell_data.ex:181
#: lib/bds/desktop/shell_live/post_editor.ex:892 #: lib/bds/desktop/shell_live/post_editor.ex:897
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:456 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:456
#: lib/bds/ui/sidebar.ex:320 #: lib/bds/ui/sidebar.ex:320
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@@ -2121,7 +2121,7 @@ msgstr "Lösung"
msgid "Result" msgid "Result"
msgstr "Ergebnis" msgstr "Ergebnis"
#: lib/bds/desktop/shell_live/post_editor.ex:893 #: lib/bds/desktop/shell_live/post_editor.ex:898
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Reverted" msgid "Reverted"
msgstr "Zurückgesetzt" msgstr "Zurückgesetzt"
@@ -2161,7 +2161,7 @@ msgstr "SSH-Modus"
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:312 #: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:312
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:329 #: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:329
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:342 #: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:342
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:57 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:69
#: lib/bds/desktop/shell_live/template_editor_html/template_editor.html.heex:13 #: lib/bds/desktop/shell_live/template_editor_html/template_editor.html.heex:13
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Save" msgid "Save"
@@ -2173,7 +2173,7 @@ msgid "Save Translation"
msgstr "Übersetzung speichern" msgstr "Übersetzung speichern"
#: lib/bds/desktop/shell_live/media_editor.ex:702 #: lib/bds/desktop/shell_live/media_editor.ex:702
#: lib/bds/desktop/shell_live/post_editor.ex:891 #: lib/bds/desktop/shell_live/post_editor.ex:896
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Saved" msgid "Saved"
msgstr "Gespeichert" msgstr "Gespeichert"
@@ -2184,7 +2184,7 @@ msgid "Scanning entries..."
msgstr "Einträge werden gescannt..." msgstr "Einträge werden gescannt..."
#: lib/bds/desktop/shell_live/misc_editor.ex:746 #: lib/bds/desktop/shell_live/misc_editor.ex:746
#: lib/bds/desktop/shell_live/sidebar_components.ex:521 #: lib/bds/desktop/shell_live/sidebar_components.ex:657
#: lib/bds/desktop/shell_live/sidebar_delete.ex:176 #: lib/bds/desktop/shell_live/sidebar_delete.ex:176
#: lib/bds/ui/registry.ex:133 #: lib/bds/ui/registry.ex:133
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@@ -2431,7 +2431,7 @@ msgstr "Untermenü"
msgid "Switch project" msgid "Switch project"
msgstr "Projekt wechseln" msgstr "Projekt wechseln"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:82 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:94
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Sync" msgid "Sync"
msgstr "Synchronisieren" msgstr "Synchronisieren"
@@ -2468,11 +2468,11 @@ msgstr "Schlagwortname"
#: lib/bds/desktop/shell_live/index.html.heex:325 #: lib/bds/desktop/shell_live/index.html.heex:325
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:161 #: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:161
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:158 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:158
#: lib/bds/desktop/shell_live/tags_editor.ex:94 #: lib/bds/desktop/shell_live/tags_editor.ex:104
#: lib/bds/desktop/shell_live/tags_editor.ex:136 #: lib/bds/desktop/shell_live/tags_editor.ex:166
#: lib/bds/desktop/shell_live/tags_editor.ex:189 #: lib/bds/desktop/shell_live/tags_editor.ex:219
#: lib/bds/desktop/shell_live/tags_editor.ex:203 #: lib/bds/desktop/shell_live/tags_editor.ex:233
#: lib/bds/desktop/shell_live/tags_editor.ex:234 #: lib/bds/desktop/shell_live/tags_editor.ex:264
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:11 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:11
#: lib/bds/ui/registry.ex:54 #: lib/bds/ui/registry.ex:54
#: lib/bds/ui/registry.ex:103 #: lib/bds/ui/registry.ex:103
@@ -2483,7 +2483,7 @@ msgstr "Schlagwortname"
msgid "Tags" msgid "Tags"
msgstr "Tags" msgstr "Tags"
#: lib/bds/desktop/shell_live.ex:927 #: lib/bds/desktop/shell_live.ex:948
#: lib/bds/desktop/shell_live/panel_renderer.ex:54 #: lib/bds/desktop/shell_live/panel_renderer.ex:54
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Tasks" msgid "Tasks"
@@ -2497,7 +2497,7 @@ msgstr "Technik"
#: lib/bds/desktop/shell_live/misc_editor.ex:747 #: lib/bds/desktop/shell_live/misc_editor.ex:747
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:286 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:286
#: lib/bds/desktop/shell_live/sidebar_components.ex:524 #: lib/bds/desktop/shell_live/sidebar_components.ex:660
#: lib/bds/desktop/shell_live/sidebar_delete.ex:179 #: lib/bds/desktop/shell_live/sidebar_delete.ex:179
#: lib/bds/ui/registry.ex:134 #: lib/bds/ui/registry.ex:134
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@@ -2648,9 +2648,9 @@ msgstr "Seitenleiste umschalten"
#: lib/bds/desktop/shell_live/media_editor.ex:558 #: lib/bds/desktop/shell_live/media_editor.ex:558
#: lib/bds/desktop/shell_live/media_editor.ex:563 #: lib/bds/desktop/shell_live/media_editor.ex:563
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:76 #: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:76
#: lib/bds/desktop/shell_live/post_editor.ex:691 #: lib/bds/desktop/shell_live/post_editor.ex:696
#: lib/bds/desktop/shell_live/post_editor.ex:720
#: lib/bds/desktop/shell_live/post_editor.ex:725 #: lib/bds/desktop/shell_live/post_editor.ex:725
#: lib/bds/desktop/shell_live/post_editor.ex:730
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:60 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:60
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Translate" msgid "Translate"
@@ -2727,7 +2727,7 @@ msgstr "Verknüpfung mit Beitrag aufheben"
#: lib/bds/desktop/shell_live/media_editor.ex:701 #: lib/bds/desktop/shell_live/media_editor.ex:701
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:10 #: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:10
#: lib/bds/desktop/shell_live/post_editor.ex:890 #: lib/bds/desktop/shell_live/post_editor.ex:895
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:7 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:7
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Unsaved" msgid "Unsaved"
@@ -2735,7 +2735,7 @@ msgstr "Nicht gespeichert"
#: lib/bds/desktop/shell_live/import_editor.ex:867 #: lib/bds/desktop/shell_live/import_editor.ex:867
#: lib/bds/desktop/shell_live/post_editor/post_metadata.ex:166 #: lib/bds/desktop/shell_live/post_editor/post_metadata.ex:166
#: lib/bds/ui/sidebar.ex:1014 #: lib/bds/ui/sidebar.ex:1105
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Untitled" msgid "Untitled"
msgstr "Ohne Titel" msgstr "Ohne Titel"
@@ -2829,14 +2829,12 @@ msgid "WYSIWYG"
msgstr "WYSIWYG" msgstr "WYSIWYG"
#: lib/bds/desktop/shell_live/tab_helpers.ex:191 #: lib/bds/desktop/shell_live/tab_helpers.ex:191
#: lib/bds/ui/sidebar.ex:791
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Working tree" msgid "Working tree"
msgstr "Arbeitsverzeichnis" msgstr "Arbeitsverzeichnis"
#: lib/bds/desktop/shell_live/tab_helpers.ex:191 #: lib/bds/desktop/shell_live/tab_helpers.ex:191
#: lib/bds/ui/sidebar.ex:785 #: lib/bds/ui/sidebar.ex:787
#: lib/bds/ui/sidebar.ex:792
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Working tree and history" msgid "Working tree and history"
msgstr "Arbeitsverzeichnis und Verlauf" msgstr "Arbeitsverzeichnis und Verlauf"
@@ -3179,12 +3177,12 @@ msgstr "Willkommen beim KI-Assistenten"
msgid "Comparing database and filesystem metadata" msgid "Comparing database and filesystem metadata"
msgstr "Vergleicht Datenbank- und Dateisystem-Metadaten" msgstr "Vergleicht Datenbank- und Dateisystem-Metadaten"
#: lib/bds/desktop/shell_live.ex:650 #: lib/bds/desktop/shell_live.ex:671
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Added %{count} images to post" msgid "Added %{count} images to post"
msgstr "%{count} Bilder zum Beitrag hinzugefügt" msgstr "%{count} Bilder zum Beitrag hinzugefügt"
#: lib/bds/desktop/shell_live.ex:621 #: lib/bds/desktop/shell_live.ex:642
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Added %{title}" msgid "Added %{title}"
msgstr "%{title} hinzugefügt" msgstr "%{title} hinzugefügt"
@@ -3204,18 +3202,18 @@ msgstr "Endbenutzer-Anleitung für redaktionelle Arbeitsabläufe, Medien, Vorlag
msgid "Image Import Concurrency" msgid "Image Import Concurrency"
msgstr "Gleichzeitige Bildimporte" msgstr "Gleichzeitige Bildimporte"
#: lib/bds/desktop/shell_live.ex:408 #: lib/bds/desktop/shell_live.ex:429
#: lib/bds/desktop/shell_live.ex:421 #: lib/bds/desktop/shell_live.ex:442
#: lib/bds/desktop/shell_live.ex:621 #: lib/bds/desktop/shell_live.ex:642
#: lib/bds/desktop/shell_live.ex:649 #: lib/bds/desktop/shell_live.ex:670
#: lib/bds/desktop/shell_live.ex:658 #: lib/bds/desktop/shell_live.ex:679
#: lib/bds/desktop/shell_live.ex:665 #: lib/bds/desktop/shell_live.ex:686
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:407 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:407
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Add Gallery Images" msgid "Add Gallery Images"
msgstr "Galerie-Bilder hinzufügen" msgstr "Galerie-Bilder hinzufügen"
#: lib/bds/desktop/shell_live.ex:666 #: lib/bds/desktop/shell_live.ex:687
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Failed to process %{path}: %{reason}" msgid "Failed to process %{path}: %{reason}"
msgstr "%{path} konnte nicht verarbeitet werden: %{reason}" msgstr "%{path} konnte nicht verarbeitet werden: %{reason}"
@@ -3230,12 +3228,12 @@ msgstr "Archivieren"
msgid "Move this post to the archive" msgid "Move this post to the archive"
msgstr "Diesen Beitrag ins Archiv verschieben" msgstr "Diesen Beitrag ins Archiv verschieben"
#: lib/bds/desktop/shell_live/post_editor.ex:596 #: lib/bds/desktop/shell_live/post_editor.ex:601
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Post archived" msgid "Post archived"
msgstr "Beitrag archiviert" msgstr "Beitrag archiviert"
#: lib/bds/desktop/shell_live/post_editor.ex:629 #: lib/bds/desktop/shell_live/post_editor.ex:634
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Post unarchived" msgid "Post unarchived"
msgstr "Beitrag wiederhergestellt" msgstr "Beitrag wiederhergestellt"
@@ -3249,3 +3247,157 @@ msgstr "Diesen Beitrag als Entwurf wiederherstellen"
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Unarchive" msgid "Unarchive"
msgstr "Wiederherstellen" msgstr "Wiederherstellen"
#: lib/bds/desktop/shell_live/sidebar_components.ex:531
#, elixir-autogen, elixir-format
msgid "Ahead"
msgstr "Voraus"
#: lib/bds/desktop/shell_live/sidebar_components.ex:532
#, elixir-autogen, elixir-format
msgid "Behind"
msgstr "Zurück"
#: lib/bds/desktop/shell_live/sidebar_components.ex:550
#, elixir-autogen, elixir-format
msgid "Changes"
msgstr "Änderungen"
#: lib/bds/desktop/shell_live.ex:1055
#: lib/bds/desktop/shell_live.ex:1071
#: lib/bds/desktop/shell_live.ex:1077
#: lib/bds/desktop/shell_live/sidebar_components.ex:556
#, elixir-autogen, elixir-format
msgid "Commit"
msgstr "Commit"
#: lib/bds/desktop/shell_live/sidebar_components.ex:555
#, elixir-autogen, elixir-format
msgid "Commit message"
msgstr "Commit-Nachricht"
#: lib/bds/desktop/shell_live.ex:1056
#, elixir-autogen, elixir-format
msgid "Commit message is required"
msgstr "Commit-Nachricht erforderlich"
#: lib/bds/desktop/shell_live.ex:1120
#, elixir-autogen, elixir-format
msgid "Done"
msgstr "Fertig"
#: lib/bds/desktop/shell_live.ex:1041
#: lib/bds/desktop/shell_live/sidebar_components.ex:542
#: lib/bds/desktop/shell_live/sidebar_components.ex:542
#, elixir-autogen, elixir-format
msgid "Fetch"
msgstr "Abrufen"
#: lib/bds/desktop/shell_live/sidebar_components.ex:586
#, elixir-autogen, elixir-format
msgid "History"
msgstr "Verlauf"
#: lib/bds/desktop/shell_live.ex:1091
#: lib/bds/desktop/shell_live.ex:1099
#: lib/bds/desktop/shell_live/sidebar_components.ex:513
#, elixir-autogen, elixir-format
msgid "Initialize Git"
msgstr "Git initialisieren"
#: lib/bds/desktop/shell_live/sidebar_components.ex:536
#, elixir-autogen, elixir-format
msgid "Local only"
msgstr "Nur lokal"
#: lib/bds/desktop/shell_live.ex:1127
#, elixir-autogen, elixir-format
msgid "No active project"
msgstr "Kein aktives Projekt"
#: lib/bds/desktop/shell_live/sidebar_components.ex:580
#, elixir-autogen, elixir-format
msgid "No changes"
msgstr "Keine Änderungen"
#: lib/bds/desktop/shell_live/sidebar_components.ex:616
#, elixir-autogen, elixir-format
msgid "No commits yet"
msgstr "Noch keine Commits"
#: lib/bds/desktop/shell_live/sidebar_components.ex:613
#, elixir-autogen, elixir-format
msgid "Older history available"
msgstr "Ältere Einträge verfügbar"
#: lib/bds/desktop/shell_live.ex:1044
#: lib/bds/desktop/shell_live/sidebar_components.ex:545
#: lib/bds/desktop/shell_live/sidebar_components.ex:545
#, elixir-autogen, elixir-format
msgid "Prune LFS"
msgstr "LFS bereinigen"
#: lib/bds/desktop/shell_live.ex:1042
#: lib/bds/desktop/shell_live/sidebar_components.ex:543
#: lib/bds/desktop/shell_live/sidebar_components.ex:543
#, elixir-autogen, elixir-format
msgid "Pull"
msgstr "Pull"
#: lib/bds/desktop/shell_live.ex:1043
#: lib/bds/desktop/shell_live/sidebar_components.ex:544
#: lib/bds/desktop/shell_live/sidebar_components.ex:544
#, elixir-autogen, elixir-format
msgid "Push"
msgstr "Push"
#: lib/bds/desktop/shell_live/sidebar_components.ex:509
#, elixir-autogen, elixir-format
msgid "Remote URL (optional)"
msgstr "Remote-URL (optional)"
#: lib/bds/desktop/shell_live/sidebar_components.ex:537
#, elixir-autogen, elixir-format
msgid "Remote only"
msgstr "Nur remote"
#: lib/bds/desktop/shell_live.ex:1092
#, elixir-autogen, elixir-format
msgid "Repository initialized"
msgstr "Repository initialisiert"
#: lib/bds/desktop/shell_live/sidebar_components.ex:535
#, elixir-autogen, elixir-format
msgid "Synced"
msgstr "Synchronisiert"
#: lib/bds/desktop/shell_live/sidebar_components.ex:504
#, elixir-autogen, elixir-format
msgid "This project is not a Git repository yet."
msgstr "Dieses Projekt ist noch kein Git-Repository."
#: lib/bds/ui/sidebar.ex:869
#, elixir-autogen, elixir-format
msgid "added"
msgstr "hinzugefügt"
#: lib/bds/ui/sidebar.ex:870
#, elixir-autogen, elixir-format
msgid "deleted"
msgstr "gelöscht"
#: lib/bds/ui/sidebar.ex:871
#: lib/bds/ui/sidebar.ex:874
#, elixir-autogen, elixir-format
msgid "modified"
msgstr "geändert"
#: lib/bds/ui/sidebar.ex:872
#, elixir-autogen, elixir-format
msgid "renamed"
msgstr "umbenannt"
#: lib/bds/ui/sidebar.ex:873
#, elixir-autogen, elixir-format
msgid "untracked"
msgstr "nicht verfolgt"

View File

@@ -5,7 +5,7 @@
msgid "Archive" msgid "Archive"
msgstr "" msgstr ""
#: lib/bds/rendering/labels.ex:54 #: lib/bds/rendering/labels.ex:55
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "April" msgid "April"
msgstr "" msgstr ""
@@ -15,7 +15,7 @@ msgstr ""
msgid "Archive calendar" msgid "Archive calendar"
msgstr "" msgstr ""
#: lib/bds/rendering/labels.ex:70 #: lib/bds/rendering/labels.ex:71
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "August" msgid "August"
msgstr "" msgstr ""
@@ -35,27 +35,27 @@ msgstr ""
msgid "Close calendar" msgid "Close calendar"
msgstr "" msgstr ""
#: lib/bds/rendering/labels.ex:86 #: lib/bds/rendering/labels.ex:87
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "December" msgid "December"
msgstr "" msgstr ""
#: lib/bds/rendering/labels.ex:46 #: lib/bds/rendering/labels.ex:47
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "February" msgid "February"
msgstr "" msgstr ""
#: lib/bds/rendering/labels.ex:42 #: lib/bds/rendering/labels.ex:43
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "January" msgid "January"
msgstr "" msgstr ""
#: lib/bds/rendering/labels.ex:66 #: lib/bds/rendering/labels.ex:67
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "July" msgid "July"
msgstr "" msgstr ""
#: lib/bds/rendering/labels.ex:62 #: lib/bds/rendering/labels.ex:63
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "June" msgid "June"
msgstr "" msgstr ""
@@ -75,22 +75,22 @@ msgstr ""
msgid "Loading calendar…" msgid "Loading calendar…"
msgstr "" msgstr ""
#: lib/bds/rendering/labels.ex:50 #: lib/bds/rendering/labels.ex:51
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "March" msgid "March"
msgstr "" msgstr ""
#: lib/bds/rendering/labels.ex:58 #: lib/bds/rendering/labels.ex:59
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "May" msgid "May"
msgstr "" msgstr ""
#: lib/bds/rendering/labels.ex:82 #: lib/bds/rendering/labels.ex:83
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "November" msgid "November"
msgstr "" msgstr ""
#: lib/bds/rendering/labels.ex:78 #: lib/bds/rendering/labels.ex:79
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "October" msgid "October"
msgstr "" msgstr ""
@@ -110,7 +110,7 @@ msgstr ""
msgid "Search..." msgid "Search..."
msgstr "" msgstr ""
#: lib/bds/rendering/labels.ex:74 #: lib/bds/rendering/labels.ex:75
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "September" msgid "September"
msgstr "" msgstr ""
@@ -135,22 +135,27 @@ msgstr ""
msgid "older" msgid "older"
msgstr "" msgstr ""
#: lib/bds/rendering/labels.ex:31 #: lib/bds/rendering/labels.ex:32
#, 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:30 #: lib/bds/rendering/labels.ex:31
#, 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:33 #: lib/bds/rendering/labels.ex:34
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Vimeo video" msgid "Vimeo video"
msgstr "" msgstr ""
#: lib/bds/rendering/labels.ex:32 #: lib/bds/rendering/labels.ex:33
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "YouTube video" msgid "YouTube video"
msgstr "" msgstr ""
#: lib/bds/rendering/labels.ex:30
#, elixir-autogen, elixir-format
msgid "No results found"
msgstr ""

View File

@@ -79,7 +79,7 @@ msgstr ""
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:42 #: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:42
#: lib/bds/desktop/shell_live/overlay_manager.ex:72 #: lib/bds/desktop/shell_live/overlay_manager.ex:72
#: lib/bds/desktop/shell_live/post_editor.ex:776 #: lib/bds/desktop/shell_live/post_editor.ex:781
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:43 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:43
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "AI Suggestions" msgid "AI Suggestions"
@@ -257,14 +257,14 @@ msgid "Auto"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_data.ex:98 #: lib/bds/desktop/shell_data.ex:98
#: lib/bds/desktop/shell_live.ex:409 #: lib/bds/desktop/shell_live.ex:430
#: lib/bds/desktop/shell_live/chat_editor.ex:231 #: lib/bds/desktop/shell_live/chat_editor.ex:231
#: lib/bds/desktop/shell_live/media_editor.ex:156 #: lib/bds/desktop/shell_live/media_editor.ex:156
#: lib/bds/desktop/shell_live/media_editor.ex:349 #: lib/bds/desktop/shell_live/media_editor.ex:349
#: lib/bds/desktop/shell_live/media_editor.ex:538 #: lib/bds/desktop/shell_live/media_editor.ex:538
#: lib/bds/desktop/shell_live/overlay_manager.ex:73 #: lib/bds/desktop/shell_live/overlay_manager.ex:73
#: lib/bds/desktop/shell_live/post_editor.ex:643 #: lib/bds/desktop/shell_live/post_editor.ex:648
#: lib/bds/desktop/shell_live/post_editor.ex:692 #: lib/bds/desktop/shell_live/post_editor.ex:697
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Automatic AI actions stay gated by airplane mode." msgid "Automatic AI actions stay gated by airplane mode."
msgstr "" msgstr ""
@@ -404,7 +404,7 @@ msgstr ""
msgid "Category name is required" msgid "Category name is required"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live.ex:932 #: lib/bds/desktop/shell_live.ex:953
#: lib/bds/desktop/shell_live/chat_editor.ex:87 #: lib/bds/desktop/shell_live/chat_editor.ex:87
#: lib/bds/desktop/shell_live/chat_editor.ex:230 #: lib/bds/desktop/shell_live/chat_editor.ex:230
#: lib/bds/desktop/shell_live/chat_editor.ex:318 #: lib/bds/desktop/shell_live/chat_editor.ex:318
@@ -537,7 +537,7 @@ msgstr ""
msgid "Could not write MCP config %{path}: %{reason}" msgid "Could not write MCP config %{path}: %{reason}"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:42 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:48
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Create" msgid "Create"
msgstr "" msgstr ""
@@ -636,12 +636,12 @@ msgstr ""
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:166 #: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:166
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:116 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:116
#: lib/bds/desktop/shell_live/script_editor_html/script_editor.html.heex:16 #: lib/bds/desktop/shell_live/script_editor_html/script_editor.html.heex:16
#: lib/bds/desktop/shell_live/sidebar_components.ex:515 #: lib/bds/desktop/shell_live/sidebar_components.ex:651
#: lib/bds/desktop/shell_live/sidebar_components.ex:518 #: lib/bds/desktop/shell_live/sidebar_components.ex:654
#: lib/bds/desktop/shell_live/sidebar_components.ex:521 #: lib/bds/desktop/shell_live/sidebar_components.ex:657
#: lib/bds/desktop/shell_live/sidebar_components.ex:524 #: lib/bds/desktop/shell_live/sidebar_components.ex:660
#: lib/bds/desktop/shell_live/sidebar_components.ex:527 #: lib/bds/desktop/shell_live/sidebar_components.ex:663
#: lib/bds/desktop/shell_live/sidebar_components.ex:529 #: lib/bds/desktop/shell_live/sidebar_components.ex:665
#: lib/bds/desktop/shell_live/sidebar_delete.ex:72 #: lib/bds/desktop/shell_live/sidebar_delete.ex:72
#: lib/bds/desktop/shell_live/sidebar_delete.ex:174 #: lib/bds/desktop/shell_live/sidebar_delete.ex:174
#: lib/bds/desktop/shell_live/sidebar_delete.ex:175 #: lib/bds/desktop/shell_live/sidebar_delete.ex:175
@@ -649,7 +649,7 @@ msgstr ""
#: lib/bds/desktop/shell_live/sidebar_delete.ex:179 #: lib/bds/desktop/shell_live/sidebar_delete.ex:179
#: lib/bds/desktop/shell_live/sidebar_delete.ex:181 #: lib/bds/desktop/shell_live/sidebar_delete.ex:181
#: lib/bds/desktop/shell_live/sidebar_delete.ex:182 #: lib/bds/desktop/shell_live/sidebar_delete.ex:182
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:58 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:70
#: lib/bds/desktop/shell_live/template_editor_html/template_editor.html.heex:15 #: lib/bds/desktop/shell_live/template_editor_html/template_editor.html.heex:15
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Delete" msgid "Delete"
@@ -665,7 +665,7 @@ msgstr ""
msgid "Delete Translation" msgid "Delete Translation"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:514 #: lib/bds/desktop/shell_live/sidebar_components.ex:650
#: lib/bds/desktop/shell_live/sidebar_delete.ex:173 #: lib/bds/desktop/shell_live/sidebar_delete.ex:173
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Delete conversation" msgid "Delete conversation"
@@ -706,9 +706,9 @@ msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:199 #: lib/bds/desktop/shell_live/media_editor.ex:199
#: lib/bds/desktop/shell_live/media_editor.ex:205 #: lib/bds/desktop/shell_live/media_editor.ex:205
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:59 #: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:59
#: lib/bds/desktop/shell_live/post_editor.ex:642 #: lib/bds/desktop/shell_live/post_editor.ex:647
#: lib/bds/desktop/shell_live/post_editor.ex:671 #: lib/bds/desktop/shell_live/post_editor.ex:676
#: lib/bds/desktop/shell_live/post_editor.ex:677 #: lib/bds/desktop/shell_live/post_editor.ex:682
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Detect Language" msgid "Detect Language"
msgstr "" msgstr ""
@@ -749,7 +749,7 @@ msgid "Discard changes and restore the published version"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:21 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:21
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:84 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:96
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Discover" msgid "Discover"
msgstr "" msgstr ""
@@ -991,8 +991,8 @@ msgstr ""
msgid "Generate Site" msgid "Generate Site"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live.ex:933 #: lib/bds/desktop/shell_live.ex:954
#: lib/bds/ui/sidebar.ex:784 #: lib/bds/ui/sidebar.ex:786
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Git" msgid "Git"
msgstr "" msgstr ""
@@ -1004,7 +1004,7 @@ msgid "Git Diff"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_data.ex:244 #: lib/bds/desktop/shell_data.ex:244
#: lib/bds/desktop/shell_live.ex:929 #: lib/bds/desktop/shell_live.ex:950
#: lib/bds/desktop/shell_live/panel_renderer.ex:171 #: lib/bds/desktop/shell_live/panel_renderer.ex:171
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Git Log" msgid "Git Log"
@@ -1033,7 +1033,7 @@ msgstr ""
#: lib/bds/desktop/shell_data.ex:116 #: lib/bds/desktop/shell_data.ex:116
#: lib/bds/desktop/shell_live/index.html.heex:666 #: lib/bds/desktop/shell_live/index.html.heex:666
#: lib/bds/desktop/shell_live/media_editor.ex:703 #: lib/bds/desktop/shell_live/media_editor.ex:703
#: lib/bds/desktop/shell_live/post_editor.ex:894 #: lib/bds/desktop/shell_live/post_editor.ex:899
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Idle" msgid "Idle"
msgstr "" msgstr ""
@@ -1075,7 +1075,7 @@ msgstr ""
#: lib/bds/desktop/shell_live/import_editor/taxonomy_editing.ex:127 #: lib/bds/desktop/shell_live/import_editor/taxonomy_editing.ex:127
#: lib/bds/desktop/shell_live/import_editor/taxonomy_editing.ex:137 #: lib/bds/desktop/shell_live/import_editor/taxonomy_editing.ex:137
#: lib/bds/desktop/shell_live/misc_editor_html/misc_editor.html.heex:182 #: lib/bds/desktop/shell_live/misc_editor_html/misc_editor.html.heex:182
#: lib/bds/desktop/shell_live/sidebar_components.ex:527 #: lib/bds/desktop/shell_live/sidebar_components.ex:663
#: lib/bds/desktop/shell_live/sidebar_delete.ex:181 #: lib/bds/desktop/shell_live/sidebar_delete.ex:181
#: lib/bds/ui/registry.ex:70 #: lib/bds/ui/registry.ex:70
#: lib/bds/ui/registry.ex:105 #: lib/bds/ui/registry.ex:105
@@ -1127,9 +1127,9 @@ msgstr ""
msgid "Import failed: %{error}" msgid "Import failed: %{error}"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live.ex:580 #: lib/bds/desktop/shell_live.ex:601
#: lib/bds/desktop/shell_live.ex:970 #: lib/bds/desktop/shell_live.ex:991
#: lib/bds/desktop/shell_live.ex:976 #: lib/bds/desktop/shell_live.ex:997
#: lib/bds/desktop/shell_live/sidebar_create.ex:47 #: lib/bds/desktop/shell_live/sidebar_create.ex:47
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Import media" msgid "Import media"
@@ -1204,7 +1204,7 @@ msgid "Language"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:206 #: lib/bds/desktop/shell_live/media_editor.ex:206
#: lib/bds/desktop/shell_live/post_editor.ex:678 #: lib/bds/desktop/shell_live/post_editor.ex:683
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Language detection failed." msgid "Language detection failed."
msgstr "" msgstr ""
@@ -1288,7 +1288,7 @@ msgstr ""
msgid "Mapped" msgid "Mapped"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:897 #: lib/bds/desktop/shell_live/post_editor.ex:902
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:120 #: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:120
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Markdown" msgid "Markdown"
@@ -1304,7 +1304,7 @@ msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:494 #: lib/bds/desktop/shell_live/media_editor.ex:494
#: lib/bds/desktop/shell_live/misc_editor.ex:744 #: lib/bds/desktop/shell_live/misc_editor.ex:744
#: lib/bds/desktop/shell_live/misc_editor.ex:771 #: lib/bds/desktop/shell_live/misc_editor.ex:771
#: lib/bds/desktop/shell_live/sidebar_components.ex:518 #: lib/bds/desktop/shell_live/sidebar_components.ex:654
#: lib/bds/desktop/shell_live/sidebar_delete.ex:175 #: lib/bds/desktop/shell_live/sidebar_delete.ex:175
#: lib/bds/ui/registry.ex:30 #: lib/bds/ui/registry.ex:30
#: lib/bds/ui/registry.ex:100 #: lib/bds/ui/registry.ex:100
@@ -1328,12 +1328,12 @@ msgstr ""
msgid "Menu" msgid "Menu"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:75 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:87
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Merge" msgid "Merge"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:66 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:78
#: lib/bds/ui/sidebar.ex:747 #: lib/bds/ui/sidebar.ex:747
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Merge Tags" msgid "Merge Tags"
@@ -1424,7 +1424,7 @@ msgstr ""
msgid "New Template" msgid "New Template"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:52 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:64
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "No Template" msgid "No Template"
msgstr "" msgstr ""
@@ -1453,12 +1453,12 @@ msgstr ""
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:72 #: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:72
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:308 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:308
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:320 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:320
#: lib/bds/desktop/shell_live/sidebar_components.ex:320 #: lib/bds/desktop/shell_live/sidebar_components.ex:321
#: lib/bds/desktop/shell_live/sidebar_components.ex:380 #: lib/bds/desktop/shell_live/sidebar_components.ex:381
#: lib/bds/desktop/shell_live/sidebar_components.ex:454 #: lib/bds/desktop/shell_live/sidebar_components.ex:455
#: lib/bds/ui/sidebar.ex:198 #: lib/bds/ui/sidebar.ex:198
#: lib/bds/ui/sidebar.ex:787 #: lib/bds/ui/sidebar.ex:789
#: lib/bds/ui/sidebar.ex:805 #: lib/bds/ui/sidebar.ex:896
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "No items" msgid "No items"
msgstr "" msgstr ""
@@ -1721,7 +1721,7 @@ msgstr ""
msgid "Other (%{count})" msgid "Other (%{count})"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live.ex:928 #: lib/bds/desktop/shell_live.ex:949
#: lib/bds/desktop/shell_live/panel_renderer.ex:83 #: lib/bds/desktop/shell_live/panel_renderer.ex:83
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Output" msgid "Output"
@@ -1786,17 +1786,17 @@ msgid "Persist the detected language for this media item"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:742 #: lib/bds/desktop/shell_live/misc_editor.ex:742
#: lib/bds/desktop/shell_live/post_editor.ex:474 #: lib/bds/desktop/shell_live/post_editor.ex:479
#: lib/bds/desktop/shell_live/post_editor.ex:478 #: lib/bds/desktop/shell_live/post_editor.ex:483
#: lib/bds/desktop/shell_live/post_editor.ex:513 #: lib/bds/desktop/shell_live/post_editor.ex:518
#: lib/bds/desktop/shell_live/post_editor.ex:517 #: lib/bds/desktop/shell_live/post_editor.ex:522
#: lib/bds/desktop/shell_live/post_editor.ex:552 #: lib/bds/desktop/shell_live/post_editor.ex:557
#: lib/bds/desktop/shell_live/post_editor.ex:567 #: lib/bds/desktop/shell_live/post_editor.ex:572
#: lib/bds/desktop/shell_live/post_editor.ex:596 #: lib/bds/desktop/shell_live/post_editor.ex:601
#: lib/bds/desktop/shell_live/post_editor.ex:599 #: lib/bds/desktop/shell_live/post_editor.ex:604
#: lib/bds/desktop/shell_live/post_editor.ex:629 #: lib/bds/desktop/shell_live/post_editor.ex:634
#: lib/bds/desktop/shell_live/post_editor.ex:632 #: lib/bds/desktop/shell_live/post_editor.ex:637
#: lib/bds/desktop/shell_live/sidebar_components.ex:515 #: lib/bds/desktop/shell_live/sidebar_components.ex:651
#: lib/bds/desktop/shell_live/sidebar_delete.ex:174 #: lib/bds/desktop/shell_live/sidebar_delete.ex:174
#: lib/bds/ui/registry.ex:99 #: lib/bds/ui/registry.ex:99
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@@ -1825,12 +1825,12 @@ msgstr ""
msgid "Post is marked as do-not-translate but has translations" msgid "Post is marked as do-not-translate but has translations"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:513 #: lib/bds/desktop/shell_live/post_editor.ex:518
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Post published" msgid "Post published"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:474 #: lib/bds/desktop/shell_live/post_editor.ex:479
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Post saved" msgid "Post saved"
msgstr "" msgstr ""
@@ -1854,7 +1854,7 @@ msgstr ""
msgid "Preferences" msgid "Preferences"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:898 #: lib/bds/desktop/shell_live/post_editor.ex:903
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:121 #: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:121
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Preview" msgid "Preview"
@@ -1923,7 +1923,7 @@ msgid "Publish Selected"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_data.ex:181 #: lib/bds/desktop/shell_data.ex:181
#: lib/bds/desktop/shell_live/post_editor.ex:892 #: lib/bds/desktop/shell_live/post_editor.ex:897
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:456 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:456
#: lib/bds/ui/sidebar.ex:320 #: lib/bds/ui/sidebar.ex:320
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@@ -2121,7 +2121,7 @@ msgstr ""
msgid "Result" msgid "Result"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:893 #: lib/bds/desktop/shell_live/post_editor.ex:898
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Reverted" msgid "Reverted"
msgstr "" msgstr ""
@@ -2161,7 +2161,7 @@ msgstr ""
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:312 #: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:312
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:329 #: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:329
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:342 #: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:342
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:57 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:69
#: lib/bds/desktop/shell_live/template_editor_html/template_editor.html.heex:13 #: lib/bds/desktop/shell_live/template_editor_html/template_editor.html.heex:13
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Save" msgid "Save"
@@ -2173,7 +2173,7 @@ msgid "Save Translation"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:702 #: lib/bds/desktop/shell_live/media_editor.ex:702
#: lib/bds/desktop/shell_live/post_editor.ex:891 #: lib/bds/desktop/shell_live/post_editor.ex:896
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Saved" msgid "Saved"
msgstr "" msgstr ""
@@ -2184,7 +2184,7 @@ msgid "Scanning entries..."
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:746 #: lib/bds/desktop/shell_live/misc_editor.ex:746
#: lib/bds/desktop/shell_live/sidebar_components.ex:521 #: lib/bds/desktop/shell_live/sidebar_components.ex:657
#: lib/bds/desktop/shell_live/sidebar_delete.ex:176 #: lib/bds/desktop/shell_live/sidebar_delete.ex:176
#: lib/bds/ui/registry.ex:133 #: lib/bds/ui/registry.ex:133
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@@ -2431,7 +2431,7 @@ msgstr ""
msgid "Switch project" msgid "Switch project"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:82 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:94
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Sync" msgid "Sync"
msgstr "" msgstr ""
@@ -2468,11 +2468,11 @@ msgstr ""
#: lib/bds/desktop/shell_live/index.html.heex:325 #: lib/bds/desktop/shell_live/index.html.heex:325
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:161 #: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:161
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:158 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:158
#: lib/bds/desktop/shell_live/tags_editor.ex:94 #: lib/bds/desktop/shell_live/tags_editor.ex:104
#: lib/bds/desktop/shell_live/tags_editor.ex:136 #: lib/bds/desktop/shell_live/tags_editor.ex:166
#: lib/bds/desktop/shell_live/tags_editor.ex:189 #: lib/bds/desktop/shell_live/tags_editor.ex:219
#: lib/bds/desktop/shell_live/tags_editor.ex:203 #: lib/bds/desktop/shell_live/tags_editor.ex:233
#: lib/bds/desktop/shell_live/tags_editor.ex:234 #: lib/bds/desktop/shell_live/tags_editor.ex:264
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:11 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:11
#: lib/bds/ui/registry.ex:54 #: lib/bds/ui/registry.ex:54
#: lib/bds/ui/registry.ex:103 #: lib/bds/ui/registry.ex:103
@@ -2483,7 +2483,7 @@ msgstr ""
msgid "Tags" msgid "Tags"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live.ex:927 #: lib/bds/desktop/shell_live.ex:948
#: lib/bds/desktop/shell_live/panel_renderer.ex:54 #: lib/bds/desktop/shell_live/panel_renderer.ex:54
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Tasks" msgid "Tasks"
@@ -2497,7 +2497,7 @@ msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:747 #: lib/bds/desktop/shell_live/misc_editor.ex:747
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:286 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:286
#: lib/bds/desktop/shell_live/sidebar_components.ex:524 #: lib/bds/desktop/shell_live/sidebar_components.ex:660
#: lib/bds/desktop/shell_live/sidebar_delete.ex:179 #: lib/bds/desktop/shell_live/sidebar_delete.ex:179
#: lib/bds/ui/registry.ex:134 #: lib/bds/ui/registry.ex:134
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@@ -2648,9 +2648,9 @@ msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:558 #: lib/bds/desktop/shell_live/media_editor.ex:558
#: lib/bds/desktop/shell_live/media_editor.ex:563 #: lib/bds/desktop/shell_live/media_editor.ex:563
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:76 #: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:76
#: lib/bds/desktop/shell_live/post_editor.ex:691 #: lib/bds/desktop/shell_live/post_editor.ex:696
#: lib/bds/desktop/shell_live/post_editor.ex:720
#: lib/bds/desktop/shell_live/post_editor.ex:725 #: lib/bds/desktop/shell_live/post_editor.ex:725
#: lib/bds/desktop/shell_live/post_editor.ex:730
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:60 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:60
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Translate" msgid "Translate"
@@ -2727,7 +2727,7 @@ msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:701 #: lib/bds/desktop/shell_live/media_editor.ex:701
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:10 #: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:10
#: lib/bds/desktop/shell_live/post_editor.ex:890 #: lib/bds/desktop/shell_live/post_editor.ex:895
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:7 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:7
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Unsaved" msgid "Unsaved"
@@ -2735,7 +2735,7 @@ msgstr ""
#: lib/bds/desktop/shell_live/import_editor.ex:867 #: lib/bds/desktop/shell_live/import_editor.ex:867
#: lib/bds/desktop/shell_live/post_editor/post_metadata.ex:166 #: lib/bds/desktop/shell_live/post_editor/post_metadata.ex:166
#: lib/bds/ui/sidebar.ex:1014 #: lib/bds/ui/sidebar.ex:1105
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Untitled" msgid "Untitled"
msgstr "" msgstr ""
@@ -2829,14 +2829,12 @@ msgid "WYSIWYG"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/tab_helpers.ex:191 #: lib/bds/desktop/shell_live/tab_helpers.ex:191
#: lib/bds/ui/sidebar.ex:791
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Working tree" msgid "Working tree"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/tab_helpers.ex:191 #: lib/bds/desktop/shell_live/tab_helpers.ex:191
#: lib/bds/ui/sidebar.ex:785 #: lib/bds/ui/sidebar.ex:787
#: lib/bds/ui/sidebar.ex:792
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Working tree and history" msgid "Working tree and history"
msgstr "" msgstr ""
@@ -3179,12 +3177,12 @@ msgstr ""
msgid "Comparing database and filesystem metadata" msgid "Comparing database and filesystem metadata"
msgstr "Comparing database and filesystem metadata" msgstr "Comparing database and filesystem metadata"
#: lib/bds/desktop/shell_live.ex:650 #: lib/bds/desktop/shell_live.ex:671
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Added %{count} images to post" msgid "Added %{count} images to post"
msgstr "Added %{count} images to post" msgstr "Added %{count} images to post"
#: lib/bds/desktop/shell_live.ex:621 #: lib/bds/desktop/shell_live.ex:642
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Added %{title}" msgid "Added %{title}"
msgstr "Added %{title}" msgstr "Added %{title}"
@@ -3204,18 +3202,18 @@ msgstr ""
msgid "Image Import Concurrency" msgid "Image Import Concurrency"
msgstr "Image Import Concurrency" msgstr "Image Import Concurrency"
#: lib/bds/desktop/shell_live.ex:408 #: lib/bds/desktop/shell_live.ex:429
#: lib/bds/desktop/shell_live.ex:421 #: lib/bds/desktop/shell_live.ex:442
#: lib/bds/desktop/shell_live.ex:621 #: lib/bds/desktop/shell_live.ex:642
#: lib/bds/desktop/shell_live.ex:649 #: lib/bds/desktop/shell_live.ex:670
#: lib/bds/desktop/shell_live.ex:658 #: lib/bds/desktop/shell_live.ex:679
#: lib/bds/desktop/shell_live.ex:665 #: lib/bds/desktop/shell_live.ex:686
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:407 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:407
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Add Gallery Images" msgid "Add Gallery Images"
msgstr "Add Gallery Images" msgstr "Add Gallery Images"
#: lib/bds/desktop/shell_live.ex:666 #: lib/bds/desktop/shell_live.ex:687
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Failed to process %{path}: %{reason}" msgid "Failed to process %{path}: %{reason}"
msgstr "Failed to process %{path}: %{reason}" msgstr "Failed to process %{path}: %{reason}"
@@ -3230,12 +3228,12 @@ msgstr ""
msgid "Move this post to the archive" msgid "Move this post to the archive"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:596 #: lib/bds/desktop/shell_live/post_editor.ex:601
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Post archived" msgid "Post archived"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:629 #: lib/bds/desktop/shell_live/post_editor.ex:634
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Post unarchived" msgid "Post unarchived"
msgstr "" msgstr ""
@@ -3249,3 +3247,157 @@ msgstr ""
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Unarchive" msgid "Unarchive"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:531
#, elixir-autogen, elixir-format
msgid "Ahead"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:532
#, elixir-autogen, elixir-format
msgid "Behind"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:550
#, elixir-autogen, elixir-format
msgid "Changes"
msgstr ""
#: lib/bds/desktop/shell_live.ex:1055
#: lib/bds/desktop/shell_live.ex:1071
#: lib/bds/desktop/shell_live.ex:1077
#: lib/bds/desktop/shell_live/sidebar_components.ex:556
#, elixir-autogen, elixir-format
msgid "Commit"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:555
#, elixir-autogen, elixir-format
msgid "Commit message"
msgstr ""
#: lib/bds/desktop/shell_live.ex:1056
#, elixir-autogen, elixir-format
msgid "Commit message is required"
msgstr ""
#: lib/bds/desktop/shell_live.ex:1120
#, elixir-autogen, elixir-format, fuzzy
msgid "Done"
msgstr ""
#: lib/bds/desktop/shell_live.ex:1041
#: lib/bds/desktop/shell_live/sidebar_components.ex:542
#: lib/bds/desktop/shell_live/sidebar_components.ex:542
#, elixir-autogen, elixir-format
msgid "Fetch"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:586
#, elixir-autogen, elixir-format
msgid "History"
msgstr ""
#: lib/bds/desktop/shell_live.ex:1091
#: lib/bds/desktop/shell_live.ex:1099
#: lib/bds/desktop/shell_live/sidebar_components.ex:513
#, elixir-autogen, elixir-format
msgid "Initialize Git"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:536
#, elixir-autogen, elixir-format
msgid "Local only"
msgstr ""
#: lib/bds/desktop/shell_live.ex:1127
#, elixir-autogen, elixir-format
msgid "No active project"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:580
#, elixir-autogen, elixir-format
msgid "No changes"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:616
#, elixir-autogen, elixir-format, fuzzy
msgid "No commits yet"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:613
#, elixir-autogen, elixir-format
msgid "Older history available"
msgstr ""
#: lib/bds/desktop/shell_live.ex:1044
#: lib/bds/desktop/shell_live/sidebar_components.ex:545
#: lib/bds/desktop/shell_live/sidebar_components.ex:545
#, elixir-autogen, elixir-format
msgid "Prune LFS"
msgstr ""
#: lib/bds/desktop/shell_live.ex:1042
#: lib/bds/desktop/shell_live/sidebar_components.ex:543
#: lib/bds/desktop/shell_live/sidebar_components.ex:543
#, elixir-autogen, elixir-format
msgid "Pull"
msgstr ""
#: lib/bds/desktop/shell_live.ex:1043
#: lib/bds/desktop/shell_live/sidebar_components.ex:544
#: lib/bds/desktop/shell_live/sidebar_components.ex:544
#, elixir-autogen, elixir-format, fuzzy
msgid "Push"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:509
#, elixir-autogen, elixir-format
msgid "Remote URL (optional)"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:537
#, elixir-autogen, elixir-format
msgid "Remote only"
msgstr ""
#: lib/bds/desktop/shell_live.ex:1092
#, elixir-autogen, elixir-format
msgid "Repository initialized"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:535
#, elixir-autogen, elixir-format, fuzzy
msgid "Synced"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:504
#, elixir-autogen, elixir-format
msgid "This project is not a Git repository yet."
msgstr ""
#: lib/bds/ui/sidebar.ex:869
#, elixir-autogen, elixir-format, fuzzy
msgid "added"
msgstr ""
#: lib/bds/ui/sidebar.ex:870
#, elixir-autogen, elixir-format, fuzzy
msgid "deleted"
msgstr ""
#: lib/bds/ui/sidebar.ex:871
#: lib/bds/ui/sidebar.ex:874
#, elixir-autogen, elixir-format
msgid "modified"
msgstr ""
#: lib/bds/ui/sidebar.ex:872
#, elixir-autogen, elixir-format, fuzzy
msgid "renamed"
msgstr ""
#: lib/bds/ui/sidebar.ex:873
#, elixir-autogen, elixir-format
msgid "untracked"
msgstr ""

View File

@@ -5,7 +5,7 @@
msgid "Archive" msgid "Archive"
msgstr "Archivo" msgstr "Archivo"
#: lib/bds/rendering/labels.ex:54 #: lib/bds/rendering/labels.ex:55
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "April" msgid "April"
msgstr "abril" msgstr "abril"
@@ -15,7 +15,7 @@ msgstr "abril"
msgid "Archive calendar" msgid "Archive calendar"
msgstr "Archivo" msgstr "Archivo"
#: lib/bds/rendering/labels.ex:70 #: lib/bds/rendering/labels.ex:71
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "August" msgid "August"
msgstr "agosto" msgstr "agosto"
@@ -35,27 +35,27 @@ msgstr "No se pudieron cargar los datos del calendario."
msgid "Close calendar" msgid "Close calendar"
msgstr "Cerrar calendario" msgstr "Cerrar calendario"
#: lib/bds/rendering/labels.ex:86 #: lib/bds/rendering/labels.ex:87
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "December" msgid "December"
msgstr "diciembre" msgstr "diciembre"
#: lib/bds/rendering/labels.ex:46 #: lib/bds/rendering/labels.ex:47
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "February" msgid "February"
msgstr "febrero" msgstr "febrero"
#: lib/bds/rendering/labels.ex:42 #: lib/bds/rendering/labels.ex:43
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "January" msgid "January"
msgstr "enero" msgstr "enero"
#: lib/bds/rendering/labels.ex:66 #: lib/bds/rendering/labels.ex:67
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "July" msgid "July"
msgstr "julio" msgstr "julio"
#: lib/bds/rendering/labels.ex:62 #: lib/bds/rendering/labels.ex:63
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "June" msgid "June"
msgstr "junio" msgstr "junio"
@@ -75,22 +75,22 @@ msgstr "Enlazado desde"
msgid "Loading calendar…" msgid "Loading calendar…"
msgstr "Cargando calendario…" msgstr "Cargando calendario…"
#: lib/bds/rendering/labels.ex:50 #: lib/bds/rendering/labels.ex:51
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "March" msgid "March"
msgstr "marzo" msgstr "marzo"
#: lib/bds/rendering/labels.ex:58 #: lib/bds/rendering/labels.ex:59
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "May" msgid "May"
msgstr "mayo" msgstr "mayo"
#: lib/bds/rendering/labels.ex:82 #: lib/bds/rendering/labels.ex:83
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "November" msgid "November"
msgstr "noviembre" msgstr "noviembre"
#: lib/bds/rendering/labels.ex:78 #: lib/bds/rendering/labels.ex:79
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "October" msgid "October"
msgstr "octubre" msgstr "octubre"
@@ -110,7 +110,7 @@ msgstr "Paginación"
msgid "Search..." msgid "Search..."
msgstr "Buscar..." msgstr "Buscar..."
#: lib/bds/rendering/labels.ex:74 #: lib/bds/rendering/labels.ex:75
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "September" msgid "September"
msgstr "septiembre" msgstr "septiembre"
@@ -135,22 +135,27 @@ msgstr "más reciente"
msgid "older" msgid "older"
msgstr "más antiguo" msgstr "más antiguo"
#: lib/bds/rendering/labels.ex:31 #: lib/bds/rendering/labels.ex:32
#, 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:30 #: lib/bds/rendering/labels.ex:31
#, 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:33 #: lib/bds/rendering/labels.ex:34
#, 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:32 #: lib/bds/rendering/labels.ex:33
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "YouTube video" msgid "YouTube video"
msgstr "Vídeo de YouTube" msgstr "Vídeo de YouTube"
#: lib/bds/rendering/labels.ex:30
#, elixir-autogen, elixir-format
msgid "No results found"
msgstr "No se encontraron resultados"

View File

@@ -79,7 +79,7 @@ msgstr "Configuración de IA"
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:42 #: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:42
#: lib/bds/desktop/shell_live/overlay_manager.ex:72 #: lib/bds/desktop/shell_live/overlay_manager.ex:72
#: lib/bds/desktop/shell_live/post_editor.ex:776 #: lib/bds/desktop/shell_live/post_editor.ex:781
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:43 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:43
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "AI Suggestions" msgid "AI Suggestions"
@@ -257,14 +257,14 @@ msgid "Auto"
msgstr "Automático" msgstr "Automático"
#: lib/bds/desktop/shell_data.ex:98 #: lib/bds/desktop/shell_data.ex:98
#: lib/bds/desktop/shell_live.ex:409 #: lib/bds/desktop/shell_live.ex:430
#: lib/bds/desktop/shell_live/chat_editor.ex:231 #: lib/bds/desktop/shell_live/chat_editor.ex:231
#: lib/bds/desktop/shell_live/media_editor.ex:156 #: lib/bds/desktop/shell_live/media_editor.ex:156
#: lib/bds/desktop/shell_live/media_editor.ex:349 #: lib/bds/desktop/shell_live/media_editor.ex:349
#: lib/bds/desktop/shell_live/media_editor.ex:538 #: lib/bds/desktop/shell_live/media_editor.ex:538
#: lib/bds/desktop/shell_live/overlay_manager.ex:73 #: lib/bds/desktop/shell_live/overlay_manager.ex:73
#: lib/bds/desktop/shell_live/post_editor.ex:643 #: lib/bds/desktop/shell_live/post_editor.ex:648
#: lib/bds/desktop/shell_live/post_editor.ex:692 #: lib/bds/desktop/shell_live/post_editor.ex:697
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Automatic AI actions stay gated by airplane mode." msgid "Automatic AI actions stay gated by airplane mode."
msgstr "Las acciones automáticas de IA siguen bloqueadas por el modo avión." msgstr "Las acciones automáticas de IA siguen bloqueadas por el modo avión."
@@ -404,7 +404,7 @@ msgstr "Valores predeterminados de categoría, opciones de renderizado y conexi
msgid "Category name is required" msgid "Category name is required"
msgstr "El nombre de la categoría es obligatorio" msgstr "El nombre de la categoría es obligatorio"
#: lib/bds/desktop/shell_live.ex:932 #: lib/bds/desktop/shell_live.ex:953
#: lib/bds/desktop/shell_live/chat_editor.ex:87 #: lib/bds/desktop/shell_live/chat_editor.ex:87
#: lib/bds/desktop/shell_live/chat_editor.ex:230 #: lib/bds/desktop/shell_live/chat_editor.ex:230
#: lib/bds/desktop/shell_live/chat_editor.ex:318 #: lib/bds/desktop/shell_live/chat_editor.ex:318
@@ -537,7 +537,7 @@ msgstr "No se pudo leer la configuración MCP %{path}: %{reason}"
msgid "Could not write MCP config %{path}: %{reason}" msgid "Could not write MCP config %{path}: %{reason}"
msgstr "No se pudo escribir la configuración MCP %{path}: %{reason}" msgstr "No se pudo escribir la configuración MCP %{path}: %{reason}"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:42 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:48
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Create" msgid "Create"
msgstr "Crear" msgstr "Crear"
@@ -636,12 +636,12 @@ msgstr "Modo de edición predeterminado y presentación de diff"
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:166 #: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:166
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:116 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:116
#: lib/bds/desktop/shell_live/script_editor_html/script_editor.html.heex:16 #: lib/bds/desktop/shell_live/script_editor_html/script_editor.html.heex:16
#: lib/bds/desktop/shell_live/sidebar_components.ex:515 #: lib/bds/desktop/shell_live/sidebar_components.ex:651
#: lib/bds/desktop/shell_live/sidebar_components.ex:518 #: lib/bds/desktop/shell_live/sidebar_components.ex:654
#: lib/bds/desktop/shell_live/sidebar_components.ex:521 #: lib/bds/desktop/shell_live/sidebar_components.ex:657
#: lib/bds/desktop/shell_live/sidebar_components.ex:524 #: lib/bds/desktop/shell_live/sidebar_components.ex:660
#: lib/bds/desktop/shell_live/sidebar_components.ex:527 #: lib/bds/desktop/shell_live/sidebar_components.ex:663
#: lib/bds/desktop/shell_live/sidebar_components.ex:529 #: lib/bds/desktop/shell_live/sidebar_components.ex:665
#: lib/bds/desktop/shell_live/sidebar_delete.ex:72 #: lib/bds/desktop/shell_live/sidebar_delete.ex:72
#: lib/bds/desktop/shell_live/sidebar_delete.ex:174 #: lib/bds/desktop/shell_live/sidebar_delete.ex:174
#: lib/bds/desktop/shell_live/sidebar_delete.ex:175 #: lib/bds/desktop/shell_live/sidebar_delete.ex:175
@@ -649,7 +649,7 @@ msgstr "Modo de edición predeterminado y presentación de diff"
#: lib/bds/desktop/shell_live/sidebar_delete.ex:179 #: lib/bds/desktop/shell_live/sidebar_delete.ex:179
#: lib/bds/desktop/shell_live/sidebar_delete.ex:181 #: lib/bds/desktop/shell_live/sidebar_delete.ex:181
#: lib/bds/desktop/shell_live/sidebar_delete.ex:182 #: lib/bds/desktop/shell_live/sidebar_delete.ex:182
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:58 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:70
#: lib/bds/desktop/shell_live/template_editor_html/template_editor.html.heex:15 #: lib/bds/desktop/shell_live/template_editor_html/template_editor.html.heex:15
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Delete" msgid "Delete"
@@ -665,7 +665,7 @@ msgstr "Eliminar medio"
msgid "Delete Translation" msgid "Delete Translation"
msgstr "Eliminar traducción" msgstr "Eliminar traducción"
#: lib/bds/desktop/shell_live/sidebar_components.ex:514 #: lib/bds/desktop/shell_live/sidebar_components.ex:650
#: lib/bds/desktop/shell_live/sidebar_delete.ex:173 #: lib/bds/desktop/shell_live/sidebar_delete.ex:173
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Delete conversation" msgid "Delete conversation"
@@ -706,9 +706,9 @@ msgstr "Detectar"
#: lib/bds/desktop/shell_live/media_editor.ex:199 #: lib/bds/desktop/shell_live/media_editor.ex:199
#: lib/bds/desktop/shell_live/media_editor.ex:205 #: lib/bds/desktop/shell_live/media_editor.ex:205
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:59 #: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:59
#: lib/bds/desktop/shell_live/post_editor.ex:642 #: lib/bds/desktop/shell_live/post_editor.ex:647
#: lib/bds/desktop/shell_live/post_editor.ex:671 #: lib/bds/desktop/shell_live/post_editor.ex:676
#: lib/bds/desktop/shell_live/post_editor.ex:677 #: lib/bds/desktop/shell_live/post_editor.ex:682
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Detect Language" msgid "Detect Language"
msgstr "Detectar idioma" msgstr "Detectar idioma"
@@ -749,7 +749,7 @@ msgid "Discard changes and restore the published version"
msgstr "Descartar cambios y restaurar la versión publicada" msgstr "Descartar cambios y restaurar la versión publicada"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:21 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:21
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:84 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:96
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Discover" msgid "Discover"
msgstr "Descubrir" msgstr "Descubrir"
@@ -991,8 +991,8 @@ msgstr "Galeria"
msgid "Generate Site" msgid "Generate Site"
msgstr "Generar sitio" msgstr "Generar sitio"
#: lib/bds/desktop/shell_live.ex:933 #: lib/bds/desktop/shell_live.ex:954
#: lib/bds/ui/sidebar.ex:784 #: lib/bds/ui/sidebar.ex:786
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Git" msgid "Git"
msgstr "Git" msgstr "Git"
@@ -1004,7 +1004,7 @@ msgid "Git Diff"
msgstr "Diff de Git" msgstr "Diff de Git"
#: lib/bds/desktop/shell_data.ex:244 #: lib/bds/desktop/shell_data.ex:244
#: lib/bds/desktop/shell_live.ex:929 #: lib/bds/desktop/shell_live.ex:950
#: lib/bds/desktop/shell_live/panel_renderer.ex:171 #: lib/bds/desktop/shell_live/panel_renderer.ex:171
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Git Log" msgid "Git Log"
@@ -1033,7 +1033,7 @@ msgstr "Host"
#: lib/bds/desktop/shell_data.ex:116 #: lib/bds/desktop/shell_data.ex:116
#: lib/bds/desktop/shell_live/index.html.heex:666 #: lib/bds/desktop/shell_live/index.html.heex:666
#: lib/bds/desktop/shell_live/media_editor.ex:703 #: lib/bds/desktop/shell_live/media_editor.ex:703
#: lib/bds/desktop/shell_live/post_editor.ex:894 #: lib/bds/desktop/shell_live/post_editor.ex:899
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Idle" msgid "Idle"
msgstr "Inactivo" msgstr "Inactivo"
@@ -1075,7 +1075,7 @@ msgstr "Imágenes y archivos"
#: lib/bds/desktop/shell_live/import_editor/taxonomy_editing.ex:127 #: lib/bds/desktop/shell_live/import_editor/taxonomy_editing.ex:127
#: lib/bds/desktop/shell_live/import_editor/taxonomy_editing.ex:137 #: lib/bds/desktop/shell_live/import_editor/taxonomy_editing.ex:137
#: lib/bds/desktop/shell_live/misc_editor_html/misc_editor.html.heex:182 #: lib/bds/desktop/shell_live/misc_editor_html/misc_editor.html.heex:182
#: lib/bds/desktop/shell_live/sidebar_components.ex:527 #: lib/bds/desktop/shell_live/sidebar_components.ex:663
#: lib/bds/desktop/shell_live/sidebar_delete.ex:181 #: lib/bds/desktop/shell_live/sidebar_delete.ex:181
#: lib/bds/ui/registry.ex:70 #: lib/bds/ui/registry.ex:70
#: lib/bds/ui/registry.ex:105 #: lib/bds/ui/registry.ex:105
@@ -1127,9 +1127,9 @@ msgstr "Definiciones de importación"
msgid "Import failed: %{error}" msgid "Import failed: %{error}"
msgstr "La importación falló: %{error}" msgstr "La importación falló: %{error}"
#: lib/bds/desktop/shell_live.ex:580 #: lib/bds/desktop/shell_live.ex:601
#: lib/bds/desktop/shell_live.ex:970 #: lib/bds/desktop/shell_live.ex:991
#: lib/bds/desktop/shell_live.ex:976 #: lib/bds/desktop/shell_live.ex:997
#: lib/bds/desktop/shell_live/sidebar_create.ex:47 #: lib/bds/desktop/shell_live/sidebar_create.ex:47
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Import media" msgid "Import media"
@@ -1204,7 +1204,7 @@ msgid "Language"
msgstr "Idioma" msgstr "Idioma"
#: lib/bds/desktop/shell_live/media_editor.ex:206 #: lib/bds/desktop/shell_live/media_editor.ex:206
#: lib/bds/desktop/shell_live/post_editor.ex:678 #: lib/bds/desktop/shell_live/post_editor.ex:683
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Language detection failed." msgid "Language detection failed."
msgstr "La detección de idioma falló." msgstr "La detección de idioma falló."
@@ -1288,7 +1288,7 @@ msgstr "Mapear a..."
msgid "Mapped" msgid "Mapped"
msgstr "Mapeado" msgstr "Mapeado"
#: lib/bds/desktop/shell_live/post_editor.ex:897 #: lib/bds/desktop/shell_live/post_editor.ex:902
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:120 #: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:120
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Markdown" msgid "Markdown"
@@ -1304,7 +1304,7 @@ msgstr "Máximo de publicaciones por página"
#: lib/bds/desktop/shell_live/media_editor.ex:494 #: lib/bds/desktop/shell_live/media_editor.ex:494
#: lib/bds/desktop/shell_live/misc_editor.ex:744 #: lib/bds/desktop/shell_live/misc_editor.ex:744
#: lib/bds/desktop/shell_live/misc_editor.ex:771 #: lib/bds/desktop/shell_live/misc_editor.ex:771
#: lib/bds/desktop/shell_live/sidebar_components.ex:518 #: lib/bds/desktop/shell_live/sidebar_components.ex:654
#: lib/bds/desktop/shell_live/sidebar_delete.ex:175 #: lib/bds/desktop/shell_live/sidebar_delete.ex:175
#: lib/bds/ui/registry.ex:30 #: lib/bds/ui/registry.ex:30
#: lib/bds/ui/registry.ex:100 #: lib/bds/ui/registry.ex:100
@@ -1328,12 +1328,12 @@ msgstr "Medio guardado"
msgid "Menu" msgid "Menu"
msgstr "Menú" msgstr "Menú"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:75 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:87
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Merge" msgid "Merge"
msgstr "Fusionar" msgstr "Fusionar"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:66 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:78
#: lib/bds/ui/sidebar.ex:747 #: lib/bds/ui/sidebar.ex:747
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Merge Tags" msgid "Merge Tags"
@@ -1424,7 +1424,7 @@ msgstr "Nuevo submenú"
msgid "New Template" msgid "New Template"
msgstr "Nueva plantilla" msgstr "Nueva plantilla"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:52 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:64
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "No Template" msgid "No Template"
msgstr "Sin plantilla" msgstr "Sin plantilla"
@@ -1453,12 +1453,12 @@ msgstr "Aún no hay historial de Git"
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:72 #: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:72
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:308 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:308
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:320 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:320
#: lib/bds/desktop/shell_live/sidebar_components.ex:320 #: lib/bds/desktop/shell_live/sidebar_components.ex:321
#: lib/bds/desktop/shell_live/sidebar_components.ex:380 #: lib/bds/desktop/shell_live/sidebar_components.ex:381
#: lib/bds/desktop/shell_live/sidebar_components.ex:454 #: lib/bds/desktop/shell_live/sidebar_components.ex:455
#: lib/bds/ui/sidebar.ex:198 #: lib/bds/ui/sidebar.ex:198
#: lib/bds/ui/sidebar.ex:787 #: lib/bds/ui/sidebar.ex:789
#: lib/bds/ui/sidebar.ex:805 #: lib/bds/ui/sidebar.ex:896
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "No items" msgid "No items"
msgstr "No hay elementos" msgstr "No hay elementos"
@@ -1721,7 +1721,7 @@ msgstr "Otros"
msgid "Other (%{count})" msgid "Other (%{count})"
msgstr "Otros (%{count})" msgstr "Otros (%{count})"
#: lib/bds/desktop/shell_live.ex:928 #: lib/bds/desktop/shell_live.ex:949
#: lib/bds/desktop/shell_live/panel_renderer.ex:83 #: lib/bds/desktop/shell_live/panel_renderer.ex:83
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Output" msgid "Output"
@@ -1786,17 +1786,17 @@ msgid "Persist the detected language for this media item"
msgstr "Guardar el idioma detectado para este medio" msgstr "Guardar el idioma detectado para este medio"
#: lib/bds/desktop/shell_live/misc_editor.ex:742 #: lib/bds/desktop/shell_live/misc_editor.ex:742
#: lib/bds/desktop/shell_live/post_editor.ex:474 #: lib/bds/desktop/shell_live/post_editor.ex:479
#: lib/bds/desktop/shell_live/post_editor.ex:478 #: lib/bds/desktop/shell_live/post_editor.ex:483
#: lib/bds/desktop/shell_live/post_editor.ex:513 #: lib/bds/desktop/shell_live/post_editor.ex:518
#: lib/bds/desktop/shell_live/post_editor.ex:517 #: lib/bds/desktop/shell_live/post_editor.ex:522
#: lib/bds/desktop/shell_live/post_editor.ex:552 #: lib/bds/desktop/shell_live/post_editor.ex:557
#: lib/bds/desktop/shell_live/post_editor.ex:567 #: lib/bds/desktop/shell_live/post_editor.ex:572
#: lib/bds/desktop/shell_live/post_editor.ex:596 #: lib/bds/desktop/shell_live/post_editor.ex:601
#: lib/bds/desktop/shell_live/post_editor.ex:599 #: lib/bds/desktop/shell_live/post_editor.ex:604
#: lib/bds/desktop/shell_live/post_editor.ex:629 #: lib/bds/desktop/shell_live/post_editor.ex:634
#: lib/bds/desktop/shell_live/post_editor.ex:632 #: lib/bds/desktop/shell_live/post_editor.ex:637
#: lib/bds/desktop/shell_live/sidebar_components.ex:515 #: lib/bds/desktop/shell_live/sidebar_components.ex:651
#: lib/bds/desktop/shell_live/sidebar_delete.ex:174 #: lib/bds/desktop/shell_live/sidebar_delete.ex:174
#: lib/bds/ui/registry.ex:99 #: lib/bds/ui/registry.ex:99
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@@ -1825,12 +1825,12 @@ msgstr "Plantilla de publicación"
msgid "Post is marked as do-not-translate but has translations" msgid "Post is marked as do-not-translate but has translations"
msgstr "La entrada está marcada como no-traducir pero tiene traducciones" msgstr "La entrada está marcada como no-traducir pero tiene traducciones"
#: lib/bds/desktop/shell_live/post_editor.ex:513 #: lib/bds/desktop/shell_live/post_editor.ex:518
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Post published" msgid "Post published"
msgstr "Artículo publicado" msgstr "Artículo publicado"
#: lib/bds/desktop/shell_live/post_editor.ex:474 #: lib/bds/desktop/shell_live/post_editor.ex:479
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Post saved" msgid "Post saved"
msgstr "Artículo guardado" msgstr "Artículo guardado"
@@ -1854,7 +1854,7 @@ msgstr "Publicaciones (%{count})"
msgid "Preferences" msgid "Preferences"
msgstr "Preferencias" msgstr "Preferencias"
#: lib/bds/desktop/shell_live/post_editor.ex:898 #: lib/bds/desktop/shell_live/post_editor.ex:903
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:121 #: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:121
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Preview" msgid "Preview"
@@ -1923,7 +1923,7 @@ msgid "Publish Selected"
msgstr "Publicar seleccionados" msgstr "Publicar seleccionados"
#: lib/bds/desktop/shell_data.ex:181 #: lib/bds/desktop/shell_data.ex:181
#: lib/bds/desktop/shell_live/post_editor.ex:892 #: lib/bds/desktop/shell_live/post_editor.ex:897
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:456 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:456
#: lib/bds/ui/sidebar.ex:320 #: lib/bds/ui/sidebar.ex:320
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@@ -2121,7 +2121,7 @@ msgstr "Resolución"
msgid "Result" msgid "Result"
msgstr "Resultado" msgstr "Resultado"
#: lib/bds/desktop/shell_live/post_editor.ex:893 #: lib/bds/desktop/shell_live/post_editor.ex:898
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Reverted" msgid "Reverted"
msgstr "Revertido" msgstr "Revertido"
@@ -2161,7 +2161,7 @@ msgstr "Modo SSH"
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:312 #: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:312
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:329 #: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:329
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:342 #: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:342
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:57 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:69
#: lib/bds/desktop/shell_live/template_editor_html/template_editor.html.heex:13 #: lib/bds/desktop/shell_live/template_editor_html/template_editor.html.heex:13
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Save" msgid "Save"
@@ -2173,7 +2173,7 @@ msgid "Save Translation"
msgstr "Guardar traducción" msgstr "Guardar traducción"
#: lib/bds/desktop/shell_live/media_editor.ex:702 #: lib/bds/desktop/shell_live/media_editor.ex:702
#: lib/bds/desktop/shell_live/post_editor.ex:891 #: lib/bds/desktop/shell_live/post_editor.ex:896
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Saved" msgid "Saved"
msgstr "Guardado" msgstr "Guardado"
@@ -2184,7 +2184,7 @@ msgid "Scanning entries..."
msgstr "Escaneando entradas..." msgstr "Escaneando entradas..."
#: lib/bds/desktop/shell_live/misc_editor.ex:746 #: lib/bds/desktop/shell_live/misc_editor.ex:746
#: lib/bds/desktop/shell_live/sidebar_components.ex:521 #: lib/bds/desktop/shell_live/sidebar_components.ex:657
#: lib/bds/desktop/shell_live/sidebar_delete.ex:176 #: lib/bds/desktop/shell_live/sidebar_delete.ex:176
#: lib/bds/ui/registry.ex:133 #: lib/bds/ui/registry.ex:133
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@@ -2431,7 +2431,7 @@ msgstr "Submenú"
msgid "Switch project" msgid "Switch project"
msgstr "Cambiar proyecto" msgstr "Cambiar proyecto"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:82 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:94
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Sync" msgid "Sync"
msgstr "Sincronizar" msgstr "Sincronizar"
@@ -2468,11 +2468,11 @@ msgstr "Nombre de la etiqueta"
#: lib/bds/desktop/shell_live/index.html.heex:325 #: lib/bds/desktop/shell_live/index.html.heex:325
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:161 #: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:161
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:158 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:158
#: lib/bds/desktop/shell_live/tags_editor.ex:94 #: lib/bds/desktop/shell_live/tags_editor.ex:104
#: lib/bds/desktop/shell_live/tags_editor.ex:136 #: lib/bds/desktop/shell_live/tags_editor.ex:166
#: lib/bds/desktop/shell_live/tags_editor.ex:189 #: lib/bds/desktop/shell_live/tags_editor.ex:219
#: lib/bds/desktop/shell_live/tags_editor.ex:203 #: lib/bds/desktop/shell_live/tags_editor.ex:233
#: lib/bds/desktop/shell_live/tags_editor.ex:234 #: lib/bds/desktop/shell_live/tags_editor.ex:264
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:11 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:11
#: lib/bds/ui/registry.ex:54 #: lib/bds/ui/registry.ex:54
#: lib/bds/ui/registry.ex:103 #: lib/bds/ui/registry.ex:103
@@ -2483,7 +2483,7 @@ msgstr "Nombre de la etiqueta"
msgid "Tags" msgid "Tags"
msgstr "Etiquetas" msgstr "Etiquetas"
#: lib/bds/desktop/shell_live.ex:927 #: lib/bds/desktop/shell_live.ex:948
#: lib/bds/desktop/shell_live/panel_renderer.ex:54 #: lib/bds/desktop/shell_live/panel_renderer.ex:54
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Tasks" msgid "Tasks"
@@ -2497,7 +2497,7 @@ msgstr "Tecnología"
#: lib/bds/desktop/shell_live/misc_editor.ex:747 #: lib/bds/desktop/shell_live/misc_editor.ex:747
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:286 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:286
#: lib/bds/desktop/shell_live/sidebar_components.ex:524 #: lib/bds/desktop/shell_live/sidebar_components.ex:660
#: lib/bds/desktop/shell_live/sidebar_delete.ex:179 #: lib/bds/desktop/shell_live/sidebar_delete.ex:179
#: lib/bds/ui/registry.ex:134 #: lib/bds/ui/registry.ex:134
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@@ -2648,9 +2648,9 @@ msgstr "Alternar barra lateral"
#: lib/bds/desktop/shell_live/media_editor.ex:558 #: lib/bds/desktop/shell_live/media_editor.ex:558
#: lib/bds/desktop/shell_live/media_editor.ex:563 #: lib/bds/desktop/shell_live/media_editor.ex:563
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:76 #: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:76
#: lib/bds/desktop/shell_live/post_editor.ex:691 #: lib/bds/desktop/shell_live/post_editor.ex:696
#: lib/bds/desktop/shell_live/post_editor.ex:720
#: lib/bds/desktop/shell_live/post_editor.ex:725 #: lib/bds/desktop/shell_live/post_editor.ex:725
#: lib/bds/desktop/shell_live/post_editor.ex:730
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:60 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:60
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Translate" msgid "Translate"
@@ -2727,7 +2727,7 @@ msgstr "Desvincular del artículo"
#: lib/bds/desktop/shell_live/media_editor.ex:701 #: lib/bds/desktop/shell_live/media_editor.ex:701
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:10 #: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:10
#: lib/bds/desktop/shell_live/post_editor.ex:890 #: lib/bds/desktop/shell_live/post_editor.ex:895
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:7 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:7
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Unsaved" msgid "Unsaved"
@@ -2735,7 +2735,7 @@ msgstr "Sin guardar"
#: lib/bds/desktop/shell_live/import_editor.ex:867 #: lib/bds/desktop/shell_live/import_editor.ex:867
#: lib/bds/desktop/shell_live/post_editor/post_metadata.ex:166 #: lib/bds/desktop/shell_live/post_editor/post_metadata.ex:166
#: lib/bds/ui/sidebar.ex:1014 #: lib/bds/ui/sidebar.ex:1105
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Untitled" msgid "Untitled"
msgstr "Sin título" msgstr "Sin título"
@@ -2829,14 +2829,12 @@ msgid "WYSIWYG"
msgstr "WYSIWYG" msgstr "WYSIWYG"
#: lib/bds/desktop/shell_live/tab_helpers.ex:191 #: lib/bds/desktop/shell_live/tab_helpers.ex:191
#: lib/bds/ui/sidebar.ex:791
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Working tree" msgid "Working tree"
msgstr "Árbol de trabajo" msgstr "Árbol de trabajo"
#: lib/bds/desktop/shell_live/tab_helpers.ex:191 #: lib/bds/desktop/shell_live/tab_helpers.ex:191
#: lib/bds/ui/sidebar.ex:785 #: lib/bds/ui/sidebar.ex:787
#: lib/bds/ui/sidebar.ex:792
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Working tree and history" msgid "Working tree and history"
msgstr "Árbol de trabajo e historial" msgstr "Árbol de trabajo e historial"
@@ -3179,12 +3177,12 @@ msgstr "Bienvenido al asistente de IA"
msgid "Comparing database and filesystem metadata" msgid "Comparing database and filesystem metadata"
msgstr "Comparando metadatos de la base de datos y del sistema de archivos" msgstr "Comparando metadatos de la base de datos y del sistema de archivos"
#: lib/bds/desktop/shell_live.ex:650 #: lib/bds/desktop/shell_live.ex:671
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Added %{count} images to post" msgid "Added %{count} images to post"
msgstr "%{count} imágenes añadidas a la publicación" msgstr "%{count} imágenes añadidas a la publicación"
#: lib/bds/desktop/shell_live.ex:621 #: lib/bds/desktop/shell_live.ex:642
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Added %{title}" msgid "Added %{title}"
msgstr "%{title} añadido" msgstr "%{title} añadido"
@@ -3204,18 +3202,18 @@ msgstr "Guía del usuario para flujos editoriales, medios, plantillas, traducci
msgid "Image Import Concurrency" msgid "Image Import Concurrency"
msgstr "Importación simultánea de imágenes" msgstr "Importación simultánea de imágenes"
#: lib/bds/desktop/shell_live.ex:408 #: lib/bds/desktop/shell_live.ex:429
#: lib/bds/desktop/shell_live.ex:421 #: lib/bds/desktop/shell_live.ex:442
#: lib/bds/desktop/shell_live.ex:621 #: lib/bds/desktop/shell_live.ex:642
#: lib/bds/desktop/shell_live.ex:649 #: lib/bds/desktop/shell_live.ex:670
#: lib/bds/desktop/shell_live.ex:658 #: lib/bds/desktop/shell_live.ex:679
#: lib/bds/desktop/shell_live.ex:665 #: lib/bds/desktop/shell_live.ex:686
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:407 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:407
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Add Gallery Images" msgid "Add Gallery Images"
msgstr "Añadir imágenes a la galería" msgstr "Añadir imágenes a la galería"
#: lib/bds/desktop/shell_live.ex:666 #: lib/bds/desktop/shell_live.ex:687
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Failed to process %{path}: %{reason}" msgid "Failed to process %{path}: %{reason}"
msgstr "No se pudo procesar %{path}: %{reason}" msgstr "No se pudo procesar %{path}: %{reason}"
@@ -3230,12 +3228,12 @@ msgstr "Archivar"
msgid "Move this post to the archive" msgid "Move this post to the archive"
msgstr "Mover este artículo al archivo" msgstr "Mover este artículo al archivo"
#: lib/bds/desktop/shell_live/post_editor.ex:596 #: lib/bds/desktop/shell_live/post_editor.ex:601
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Post archived" msgid "Post archived"
msgstr "Artículo archivado" msgstr "Artículo archivado"
#: lib/bds/desktop/shell_live/post_editor.ex:629 #: lib/bds/desktop/shell_live/post_editor.ex:634
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Post unarchived" msgid "Post unarchived"
msgstr "Artículo restaurado" msgstr "Artículo restaurado"
@@ -3249,3 +3247,157 @@ msgstr "Restaurar este artículo como borrador"
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Unarchive" msgid "Unarchive"
msgstr "Desarchivar" msgstr "Desarchivar"
#: lib/bds/desktop/shell_live/sidebar_components.ex:531
#, elixir-autogen, elixir-format
msgid "Ahead"
msgstr "Adelante"
#: lib/bds/desktop/shell_live/sidebar_components.ex:532
#, elixir-autogen, elixir-format
msgid "Behind"
msgstr "Atrás"
#: lib/bds/desktop/shell_live/sidebar_components.ex:550
#, elixir-autogen, elixir-format
msgid "Changes"
msgstr "Cambios"
#: lib/bds/desktop/shell_live.ex:1055
#: lib/bds/desktop/shell_live.ex:1071
#: lib/bds/desktop/shell_live.ex:1077
#: lib/bds/desktop/shell_live/sidebar_components.ex:556
#, elixir-autogen, elixir-format
msgid "Commit"
msgstr "Commit"
#: lib/bds/desktop/shell_live/sidebar_components.ex:555
#, elixir-autogen, elixir-format
msgid "Commit message"
msgstr "Mensaje de commit"
#: lib/bds/desktop/shell_live.ex:1056
#, elixir-autogen, elixir-format
msgid "Commit message is required"
msgstr "Se requiere un mensaje de commit"
#: lib/bds/desktop/shell_live.ex:1120
#, elixir-autogen, elixir-format
msgid "Done"
msgstr "Listo"
#: lib/bds/desktop/shell_live.ex:1041
#: lib/bds/desktop/shell_live/sidebar_components.ex:542
#: lib/bds/desktop/shell_live/sidebar_components.ex:542
#, elixir-autogen, elixir-format
msgid "Fetch"
msgstr "Obtener"
#: lib/bds/desktop/shell_live/sidebar_components.ex:586
#, elixir-autogen, elixir-format
msgid "History"
msgstr "Historial"
#: lib/bds/desktop/shell_live.ex:1091
#: lib/bds/desktop/shell_live.ex:1099
#: lib/bds/desktop/shell_live/sidebar_components.ex:513
#, elixir-autogen, elixir-format
msgid "Initialize Git"
msgstr "Inicializar Git"
#: lib/bds/desktop/shell_live/sidebar_components.ex:536
#, elixir-autogen, elixir-format
msgid "Local only"
msgstr "Solo local"
#: lib/bds/desktop/shell_live.ex:1127
#, elixir-autogen, elixir-format
msgid "No active project"
msgstr "Sin proyecto activo"
#: lib/bds/desktop/shell_live/sidebar_components.ex:580
#, elixir-autogen, elixir-format
msgid "No changes"
msgstr "Sin cambios"
#: lib/bds/desktop/shell_live/sidebar_components.ex:616
#, elixir-autogen, elixir-format
msgid "No commits yet"
msgstr "Aún no hay commits"
#: lib/bds/desktop/shell_live/sidebar_components.ex:613
#, elixir-autogen, elixir-format
msgid "Older history available"
msgstr "Hay historial más antiguo disponible"
#: lib/bds/desktop/shell_live.ex:1044
#: lib/bds/desktop/shell_live/sidebar_components.ex:545
#: lib/bds/desktop/shell_live/sidebar_components.ex:545
#, elixir-autogen, elixir-format
msgid "Prune LFS"
msgstr "Limpiar LFS"
#: lib/bds/desktop/shell_live.ex:1042
#: lib/bds/desktop/shell_live/sidebar_components.ex:543
#: lib/bds/desktop/shell_live/sidebar_components.ex:543
#, elixir-autogen, elixir-format
msgid "Pull"
msgstr "Pull"
#: lib/bds/desktop/shell_live.ex:1043
#: lib/bds/desktop/shell_live/sidebar_components.ex:544
#: lib/bds/desktop/shell_live/sidebar_components.ex:544
#, elixir-autogen, elixir-format
msgid "Push"
msgstr "Push"
#: lib/bds/desktop/shell_live/sidebar_components.ex:509
#, elixir-autogen, elixir-format
msgid "Remote URL (optional)"
msgstr "URL remota (opcional)"
#: lib/bds/desktop/shell_live/sidebar_components.ex:537
#, elixir-autogen, elixir-format
msgid "Remote only"
msgstr "Solo remoto"
#: lib/bds/desktop/shell_live.ex:1092
#, elixir-autogen, elixir-format
msgid "Repository initialized"
msgstr "Repositorio inicializado"
#: lib/bds/desktop/shell_live/sidebar_components.ex:535
#, elixir-autogen, elixir-format
msgid "Synced"
msgstr "Sincronizado"
#: lib/bds/desktop/shell_live/sidebar_components.ex:504
#, elixir-autogen, elixir-format
msgid "This project is not a Git repository yet."
msgstr "Este proyecto aún no es un repositorio Git."
#: lib/bds/ui/sidebar.ex:869
#, elixir-autogen, elixir-format
msgid "added"
msgstr "añadido"
#: lib/bds/ui/sidebar.ex:870
#, elixir-autogen, elixir-format
msgid "deleted"
msgstr "eliminado"
#: lib/bds/ui/sidebar.ex:871
#: lib/bds/ui/sidebar.ex:874
#, elixir-autogen, elixir-format
msgid "modified"
msgstr "modificado"
#: lib/bds/ui/sidebar.ex:872
#, elixir-autogen, elixir-format
msgid "renamed"
msgstr "renombrado"
#: lib/bds/ui/sidebar.ex:873
#, elixir-autogen, elixir-format
msgid "untracked"
msgstr "sin seguimiento"

View File

@@ -5,7 +5,7 @@
msgid "Archive" msgid "Archive"
msgstr "Archives" msgstr "Archives"
#: lib/bds/rendering/labels.ex:54 #: lib/bds/rendering/labels.ex:55
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "April" msgid "April"
msgstr "avril" msgstr "avril"
@@ -15,7 +15,7 @@ msgstr "avril"
msgid "Archive calendar" msgid "Archive calendar"
msgstr "Archives" msgstr "Archives"
#: lib/bds/rendering/labels.ex:70 #: lib/bds/rendering/labels.ex:71
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "August" msgid "August"
msgstr "août" msgstr "août"
@@ -35,27 +35,27 @@ msgstr "Impossible de charger les données du calendrier."
msgid "Close calendar" msgid "Close calendar"
msgstr "Fermer le calendrier" msgstr "Fermer le calendrier"
#: lib/bds/rendering/labels.ex:86 #: lib/bds/rendering/labels.ex:87
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "December" msgid "December"
msgstr "décembre" msgstr "décembre"
#: lib/bds/rendering/labels.ex:46 #: lib/bds/rendering/labels.ex:47
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "February" msgid "February"
msgstr "février" msgstr "février"
#: lib/bds/rendering/labels.ex:42 #: lib/bds/rendering/labels.ex:43
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "January" msgid "January"
msgstr "janvier" msgstr "janvier"
#: lib/bds/rendering/labels.ex:66 #: lib/bds/rendering/labels.ex:67
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "July" msgid "July"
msgstr "juillet" msgstr "juillet"
#: lib/bds/rendering/labels.ex:62 #: lib/bds/rendering/labels.ex:63
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "June" msgid "June"
msgstr "juin" msgstr "juin"
@@ -75,22 +75,22 @@ msgstr "Lié depuis"
msgid "Loading calendar…" msgid "Loading calendar…"
msgstr "Chargement du calendrier…" msgstr "Chargement du calendrier…"
#: lib/bds/rendering/labels.ex:50 #: lib/bds/rendering/labels.ex:51
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "March" msgid "March"
msgstr "mars" msgstr "mars"
#: lib/bds/rendering/labels.ex:58 #: lib/bds/rendering/labels.ex:59
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "May" msgid "May"
msgstr "mai" msgstr "mai"
#: lib/bds/rendering/labels.ex:82 #: lib/bds/rendering/labels.ex:83
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "November" msgid "November"
msgstr "novembre" msgstr "novembre"
#: lib/bds/rendering/labels.ex:78 #: lib/bds/rendering/labels.ex:79
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "October" msgid "October"
msgstr "octobre" msgstr "octobre"
@@ -110,7 +110,7 @@ msgstr "Navigation paginée"
msgid "Search..." msgid "Search..."
msgstr "Rechercher..." msgstr "Rechercher..."
#: lib/bds/rendering/labels.ex:74 #: lib/bds/rendering/labels.ex:75
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "September" msgid "September"
msgstr "septembre" msgstr "septembre"
@@ -135,22 +135,27 @@ msgstr "plus récent"
msgid "older" msgid "older"
msgstr "plus ancien" msgstr "plus ancien"
#: lib/bds/rendering/labels.ex:31 #: lib/bds/rendering/labels.ex:32
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Back to preview home" msgid "Back to preview home"
msgstr "Retour à laccueil de laperçu" msgstr "Retour à laccueil de laperçu"
#: lib/bds/rendering/labels.ex:30 #: lib/bds/rendering/labels.ex:31
#, 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 daperçu demandée est introuvable." msgstr "La page daperçu demandée est introuvable."
#: lib/bds/rendering/labels.ex:33 #: lib/bds/rendering/labels.ex:34
#, 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:32 #: lib/bds/rendering/labels.ex:33
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "YouTube video" msgid "YouTube video"
msgstr "Vidéo YouTube" msgstr "Vidéo YouTube"
#: lib/bds/rendering/labels.ex:30
#, elixir-autogen, elixir-format
msgid "No results found"
msgstr "Aucun résultat trouvé"

View File

@@ -79,7 +79,7 @@ msgstr "Paramètres IA"
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:42 #: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:42
#: lib/bds/desktop/shell_live/overlay_manager.ex:72 #: lib/bds/desktop/shell_live/overlay_manager.ex:72
#: lib/bds/desktop/shell_live/post_editor.ex:776 #: lib/bds/desktop/shell_live/post_editor.ex:781
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:43 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:43
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "AI Suggestions" msgid "AI Suggestions"
@@ -257,14 +257,14 @@ msgid "Auto"
msgstr "Automatique" msgstr "Automatique"
#: lib/bds/desktop/shell_data.ex:98 #: lib/bds/desktop/shell_data.ex:98
#: lib/bds/desktop/shell_live.ex:409 #: lib/bds/desktop/shell_live.ex:430
#: lib/bds/desktop/shell_live/chat_editor.ex:231 #: lib/bds/desktop/shell_live/chat_editor.ex:231
#: lib/bds/desktop/shell_live/media_editor.ex:156 #: lib/bds/desktop/shell_live/media_editor.ex:156
#: lib/bds/desktop/shell_live/media_editor.ex:349 #: lib/bds/desktop/shell_live/media_editor.ex:349
#: lib/bds/desktop/shell_live/media_editor.ex:538 #: lib/bds/desktop/shell_live/media_editor.ex:538
#: lib/bds/desktop/shell_live/overlay_manager.ex:73 #: lib/bds/desktop/shell_live/overlay_manager.ex:73
#: lib/bds/desktop/shell_live/post_editor.ex:643 #: lib/bds/desktop/shell_live/post_editor.ex:648
#: lib/bds/desktop/shell_live/post_editor.ex:692 #: lib/bds/desktop/shell_live/post_editor.ex:697
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Automatic AI actions stay gated by airplane mode." msgid "Automatic AI actions stay gated by airplane mode."
msgstr "Les actions IA automatiques restent bloquées par le mode avion." msgstr "Les actions IA automatiques restent bloquées par le mode avion."
@@ -404,7 +404,7 @@ msgstr "Valeurs par défaut des catégories, options de rendu et liaison des mod
msgid "Category name is required" msgid "Category name is required"
msgstr "Le nom de la catégorie est requis" msgstr "Le nom de la catégorie est requis"
#: lib/bds/desktop/shell_live.ex:932 #: lib/bds/desktop/shell_live.ex:953
#: lib/bds/desktop/shell_live/chat_editor.ex:87 #: lib/bds/desktop/shell_live/chat_editor.ex:87
#: lib/bds/desktop/shell_live/chat_editor.ex:230 #: lib/bds/desktop/shell_live/chat_editor.ex:230
#: lib/bds/desktop/shell_live/chat_editor.ex:318 #: lib/bds/desktop/shell_live/chat_editor.ex:318
@@ -537,7 +537,7 @@ msgstr "Impossible de lire la configuration MCP %{path} : %{reason}"
msgid "Could not write MCP config %{path}: %{reason}" msgid "Could not write MCP config %{path}: %{reason}"
msgstr "Impossible d'écrire la configuration MCP %{path} : %{reason}" msgstr "Impossible d'écrire la configuration MCP %{path} : %{reason}"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:42 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:48
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Create" msgid "Create"
msgstr "Créer" msgstr "Créer"
@@ -636,12 +636,12 @@ msgstr "Mode dédition par défaut et présentation des diffs"
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:166 #: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:166
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:116 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:116
#: lib/bds/desktop/shell_live/script_editor_html/script_editor.html.heex:16 #: lib/bds/desktop/shell_live/script_editor_html/script_editor.html.heex:16
#: lib/bds/desktop/shell_live/sidebar_components.ex:515 #: lib/bds/desktop/shell_live/sidebar_components.ex:651
#: lib/bds/desktop/shell_live/sidebar_components.ex:518 #: lib/bds/desktop/shell_live/sidebar_components.ex:654
#: lib/bds/desktop/shell_live/sidebar_components.ex:521 #: lib/bds/desktop/shell_live/sidebar_components.ex:657
#: lib/bds/desktop/shell_live/sidebar_components.ex:524 #: lib/bds/desktop/shell_live/sidebar_components.ex:660
#: lib/bds/desktop/shell_live/sidebar_components.ex:527 #: lib/bds/desktop/shell_live/sidebar_components.ex:663
#: lib/bds/desktop/shell_live/sidebar_components.ex:529 #: lib/bds/desktop/shell_live/sidebar_components.ex:665
#: lib/bds/desktop/shell_live/sidebar_delete.ex:72 #: lib/bds/desktop/shell_live/sidebar_delete.ex:72
#: lib/bds/desktop/shell_live/sidebar_delete.ex:174 #: lib/bds/desktop/shell_live/sidebar_delete.ex:174
#: lib/bds/desktop/shell_live/sidebar_delete.ex:175 #: lib/bds/desktop/shell_live/sidebar_delete.ex:175
@@ -649,7 +649,7 @@ msgstr "Mode dédition par défaut et présentation des diffs"
#: lib/bds/desktop/shell_live/sidebar_delete.ex:179 #: lib/bds/desktop/shell_live/sidebar_delete.ex:179
#: lib/bds/desktop/shell_live/sidebar_delete.ex:181 #: lib/bds/desktop/shell_live/sidebar_delete.ex:181
#: lib/bds/desktop/shell_live/sidebar_delete.ex:182 #: lib/bds/desktop/shell_live/sidebar_delete.ex:182
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:58 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:70
#: lib/bds/desktop/shell_live/template_editor_html/template_editor.html.heex:15 #: lib/bds/desktop/shell_live/template_editor_html/template_editor.html.heex:15
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Delete" msgid "Delete"
@@ -665,7 +665,7 @@ msgstr "Supprimer le media"
msgid "Delete Translation" msgid "Delete Translation"
msgstr "Supprimer la traduction" msgstr "Supprimer la traduction"
#: lib/bds/desktop/shell_live/sidebar_components.ex:514 #: lib/bds/desktop/shell_live/sidebar_components.ex:650
#: lib/bds/desktop/shell_live/sidebar_delete.ex:173 #: lib/bds/desktop/shell_live/sidebar_delete.ex:173
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Delete conversation" msgid "Delete conversation"
@@ -706,9 +706,9 @@ msgstr "Détecter"
#: lib/bds/desktop/shell_live/media_editor.ex:199 #: lib/bds/desktop/shell_live/media_editor.ex:199
#: lib/bds/desktop/shell_live/media_editor.ex:205 #: lib/bds/desktop/shell_live/media_editor.ex:205
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:59 #: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:59
#: lib/bds/desktop/shell_live/post_editor.ex:642 #: lib/bds/desktop/shell_live/post_editor.ex:647
#: lib/bds/desktop/shell_live/post_editor.ex:671 #: lib/bds/desktop/shell_live/post_editor.ex:676
#: lib/bds/desktop/shell_live/post_editor.ex:677 #: lib/bds/desktop/shell_live/post_editor.ex:682
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Detect Language" msgid "Detect Language"
msgstr "Détecter la langue" msgstr "Détecter la langue"
@@ -749,7 +749,7 @@ msgid "Discard changes and restore the published version"
msgstr "Annuler les modifications et restaurer la version publiée" msgstr "Annuler les modifications et restaurer la version publiée"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:21 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:21
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:84 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:96
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Discover" msgid "Discover"
msgstr "Découvrir" msgstr "Découvrir"
@@ -991,8 +991,8 @@ msgstr "Galerie"
msgid "Generate Site" msgid "Generate Site"
msgstr "Générer le site" msgstr "Générer le site"
#: lib/bds/desktop/shell_live.ex:933 #: lib/bds/desktop/shell_live.ex:954
#: lib/bds/ui/sidebar.ex:784 #: lib/bds/ui/sidebar.ex:786
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Git" msgid "Git"
msgstr "Git" msgstr "Git"
@@ -1004,7 +1004,7 @@ msgid "Git Diff"
msgstr "Diff Git" msgstr "Diff Git"
#: lib/bds/desktop/shell_data.ex:244 #: lib/bds/desktop/shell_data.ex:244
#: lib/bds/desktop/shell_live.ex:929 #: lib/bds/desktop/shell_live.ex:950
#: lib/bds/desktop/shell_live/panel_renderer.ex:171 #: lib/bds/desktop/shell_live/panel_renderer.ex:171
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Git Log" msgid "Git Log"
@@ -1033,7 +1033,7 @@ msgstr "Hôte"
#: lib/bds/desktop/shell_data.ex:116 #: lib/bds/desktop/shell_data.ex:116
#: lib/bds/desktop/shell_live/index.html.heex:666 #: lib/bds/desktop/shell_live/index.html.heex:666
#: lib/bds/desktop/shell_live/media_editor.ex:703 #: lib/bds/desktop/shell_live/media_editor.ex:703
#: lib/bds/desktop/shell_live/post_editor.ex:894 #: lib/bds/desktop/shell_live/post_editor.ex:899
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Idle" msgid "Idle"
msgstr "Inactif" msgstr "Inactif"
@@ -1075,7 +1075,7 @@ msgstr "Images et fichiers"
#: lib/bds/desktop/shell_live/import_editor/taxonomy_editing.ex:127 #: lib/bds/desktop/shell_live/import_editor/taxonomy_editing.ex:127
#: lib/bds/desktop/shell_live/import_editor/taxonomy_editing.ex:137 #: lib/bds/desktop/shell_live/import_editor/taxonomy_editing.ex:137
#: lib/bds/desktop/shell_live/misc_editor_html/misc_editor.html.heex:182 #: lib/bds/desktop/shell_live/misc_editor_html/misc_editor.html.heex:182
#: lib/bds/desktop/shell_live/sidebar_components.ex:527 #: lib/bds/desktop/shell_live/sidebar_components.ex:663
#: lib/bds/desktop/shell_live/sidebar_delete.ex:181 #: lib/bds/desktop/shell_live/sidebar_delete.ex:181
#: lib/bds/ui/registry.ex:70 #: lib/bds/ui/registry.ex:70
#: lib/bds/ui/registry.ex:105 #: lib/bds/ui/registry.ex:105
@@ -1127,9 +1127,9 @@ msgstr "Définitions dimport"
msgid "Import failed: %{error}" msgid "Import failed: %{error}"
msgstr "Échec de limport : %{error}" msgstr "Échec de limport : %{error}"
#: lib/bds/desktop/shell_live.ex:580 #: lib/bds/desktop/shell_live.ex:601
#: lib/bds/desktop/shell_live.ex:970 #: lib/bds/desktop/shell_live.ex:991
#: lib/bds/desktop/shell_live.ex:976 #: lib/bds/desktop/shell_live.ex:997
#: lib/bds/desktop/shell_live/sidebar_create.ex:47 #: lib/bds/desktop/shell_live/sidebar_create.ex:47
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Import media" msgid "Import media"
@@ -1204,7 +1204,7 @@ msgid "Language"
msgstr "Langue" msgstr "Langue"
#: lib/bds/desktop/shell_live/media_editor.ex:206 #: lib/bds/desktop/shell_live/media_editor.ex:206
#: lib/bds/desktop/shell_live/post_editor.ex:678 #: lib/bds/desktop/shell_live/post_editor.ex:683
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Language detection failed." msgid "Language detection failed."
msgstr "La détection de la langue a échoué." msgstr "La détection de la langue a échoué."
@@ -1288,7 +1288,7 @@ msgstr "Mapper vers..."
msgid "Mapped" msgid "Mapped"
msgstr "Mappé" msgstr "Mappé"
#: lib/bds/desktop/shell_live/post_editor.ex:897 #: lib/bds/desktop/shell_live/post_editor.ex:902
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:120 #: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:120
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Markdown" msgid "Markdown"
@@ -1304,7 +1304,7 @@ msgstr "Nombre maximal darticles par page"
#: lib/bds/desktop/shell_live/media_editor.ex:494 #: lib/bds/desktop/shell_live/media_editor.ex:494
#: lib/bds/desktop/shell_live/misc_editor.ex:744 #: lib/bds/desktop/shell_live/misc_editor.ex:744
#: lib/bds/desktop/shell_live/misc_editor.ex:771 #: lib/bds/desktop/shell_live/misc_editor.ex:771
#: lib/bds/desktop/shell_live/sidebar_components.ex:518 #: lib/bds/desktop/shell_live/sidebar_components.ex:654
#: lib/bds/desktop/shell_live/sidebar_delete.ex:175 #: lib/bds/desktop/shell_live/sidebar_delete.ex:175
#: lib/bds/ui/registry.ex:30 #: lib/bds/ui/registry.ex:30
#: lib/bds/ui/registry.ex:100 #: lib/bds/ui/registry.ex:100
@@ -1328,12 +1328,12 @@ msgstr "Média enregistré"
msgid "Menu" msgid "Menu"
msgstr "Menu" msgstr "Menu"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:75 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:87
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Merge" msgid "Merge"
msgstr "Fusionner" msgstr "Fusionner"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:66 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:78
#: lib/bds/ui/sidebar.ex:747 #: lib/bds/ui/sidebar.ex:747
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Merge Tags" msgid "Merge Tags"
@@ -1424,7 +1424,7 @@ msgstr "Nouveau sous-menu"
msgid "New Template" msgid "New Template"
msgstr "Nouveau modèle" msgstr "Nouveau modèle"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:52 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:64
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "No Template" msgid "No Template"
msgstr "Aucun template" msgstr "Aucun template"
@@ -1453,12 +1453,12 @@ msgstr "Pas encore d'historique Git"
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:72 #: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:72
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:308 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:308
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:320 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:320
#: lib/bds/desktop/shell_live/sidebar_components.ex:320 #: lib/bds/desktop/shell_live/sidebar_components.ex:321
#: lib/bds/desktop/shell_live/sidebar_components.ex:380 #: lib/bds/desktop/shell_live/sidebar_components.ex:381
#: lib/bds/desktop/shell_live/sidebar_components.ex:454 #: lib/bds/desktop/shell_live/sidebar_components.ex:455
#: lib/bds/ui/sidebar.ex:198 #: lib/bds/ui/sidebar.ex:198
#: lib/bds/ui/sidebar.ex:787 #: lib/bds/ui/sidebar.ex:789
#: lib/bds/ui/sidebar.ex:805 #: lib/bds/ui/sidebar.ex:896
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "No items" msgid "No items"
msgstr "Aucun élément" msgstr "Aucun élément"
@@ -1721,7 +1721,7 @@ msgstr "Autre"
msgid "Other (%{count})" msgid "Other (%{count})"
msgstr "Autres (%{count})" msgstr "Autres (%{count})"
#: lib/bds/desktop/shell_live.ex:928 #: lib/bds/desktop/shell_live.ex:949
#: lib/bds/desktop/shell_live/panel_renderer.ex:83 #: lib/bds/desktop/shell_live/panel_renderer.ex:83
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Output" msgid "Output"
@@ -1786,17 +1786,17 @@ msgid "Persist the detected language for this media item"
msgstr "Enregistrer la langue détectée pour ce média" msgstr "Enregistrer la langue détectée pour ce média"
#: lib/bds/desktop/shell_live/misc_editor.ex:742 #: lib/bds/desktop/shell_live/misc_editor.ex:742
#: lib/bds/desktop/shell_live/post_editor.ex:474 #: lib/bds/desktop/shell_live/post_editor.ex:479
#: lib/bds/desktop/shell_live/post_editor.ex:478 #: lib/bds/desktop/shell_live/post_editor.ex:483
#: lib/bds/desktop/shell_live/post_editor.ex:513 #: lib/bds/desktop/shell_live/post_editor.ex:518
#: lib/bds/desktop/shell_live/post_editor.ex:517 #: lib/bds/desktop/shell_live/post_editor.ex:522
#: lib/bds/desktop/shell_live/post_editor.ex:552 #: lib/bds/desktop/shell_live/post_editor.ex:557
#: lib/bds/desktop/shell_live/post_editor.ex:567 #: lib/bds/desktop/shell_live/post_editor.ex:572
#: lib/bds/desktop/shell_live/post_editor.ex:596 #: lib/bds/desktop/shell_live/post_editor.ex:601
#: lib/bds/desktop/shell_live/post_editor.ex:599 #: lib/bds/desktop/shell_live/post_editor.ex:604
#: lib/bds/desktop/shell_live/post_editor.ex:629 #: lib/bds/desktop/shell_live/post_editor.ex:634
#: lib/bds/desktop/shell_live/post_editor.ex:632 #: lib/bds/desktop/shell_live/post_editor.ex:637
#: lib/bds/desktop/shell_live/sidebar_components.ex:515 #: lib/bds/desktop/shell_live/sidebar_components.ex:651
#: lib/bds/desktop/shell_live/sidebar_delete.ex:174 #: lib/bds/desktop/shell_live/sidebar_delete.ex:174
#: lib/bds/ui/registry.ex:99 #: lib/bds/ui/registry.ex:99
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@@ -1825,12 +1825,12 @@ msgstr "Modèle darticle"
msgid "Post is marked as do-not-translate but has translations" msgid "Post is marked as do-not-translate but has translations"
msgstr "L'article est marqué ne-pas-traduire mais a des traductions" msgstr "L'article est marqué ne-pas-traduire mais a des traductions"
#: lib/bds/desktop/shell_live/post_editor.ex:513 #: lib/bds/desktop/shell_live/post_editor.ex:518
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Post published" msgid "Post published"
msgstr "Article publié" msgstr "Article publié"
#: lib/bds/desktop/shell_live/post_editor.ex:474 #: lib/bds/desktop/shell_live/post_editor.ex:479
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Post saved" msgid "Post saved"
msgstr "Article enregistré" msgstr "Article enregistré"
@@ -1854,7 +1854,7 @@ msgstr "Articles (%{count})"
msgid "Preferences" msgid "Preferences"
msgstr "Préférences" msgstr "Préférences"
#: lib/bds/desktop/shell_live/post_editor.ex:898 #: lib/bds/desktop/shell_live/post_editor.ex:903
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:121 #: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:121
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Preview" msgid "Preview"
@@ -1923,7 +1923,7 @@ msgid "Publish Selected"
msgstr "Publier la sélection" msgstr "Publier la sélection"
#: lib/bds/desktop/shell_data.ex:181 #: lib/bds/desktop/shell_data.ex:181
#: lib/bds/desktop/shell_live/post_editor.ex:892 #: lib/bds/desktop/shell_live/post_editor.ex:897
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:456 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:456
#: lib/bds/ui/sidebar.ex:320 #: lib/bds/ui/sidebar.ex:320
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@@ -2121,7 +2121,7 @@ msgstr "Résolution"
msgid "Result" msgid "Result"
msgstr "Résultat" msgstr "Résultat"
#: lib/bds/desktop/shell_live/post_editor.ex:893 #: lib/bds/desktop/shell_live/post_editor.ex:898
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Reverted" msgid "Reverted"
msgstr "Restauré" msgstr "Restauré"
@@ -2161,7 +2161,7 @@ msgstr "Mode SSH"
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:312 #: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:312
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:329 #: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:329
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:342 #: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:342
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:57 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:69
#: lib/bds/desktop/shell_live/template_editor_html/template_editor.html.heex:13 #: lib/bds/desktop/shell_live/template_editor_html/template_editor.html.heex:13
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Save" msgid "Save"
@@ -2173,7 +2173,7 @@ msgid "Save Translation"
msgstr "Enregistrer la traduction" msgstr "Enregistrer la traduction"
#: lib/bds/desktop/shell_live/media_editor.ex:702 #: lib/bds/desktop/shell_live/media_editor.ex:702
#: lib/bds/desktop/shell_live/post_editor.ex:891 #: lib/bds/desktop/shell_live/post_editor.ex:896
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Saved" msgid "Saved"
msgstr "Enregistré" msgstr "Enregistré"
@@ -2184,7 +2184,7 @@ msgid "Scanning entries..."
msgstr "Analyse des entrées..." msgstr "Analyse des entrées..."
#: lib/bds/desktop/shell_live/misc_editor.ex:746 #: lib/bds/desktop/shell_live/misc_editor.ex:746
#: lib/bds/desktop/shell_live/sidebar_components.ex:521 #: lib/bds/desktop/shell_live/sidebar_components.ex:657
#: lib/bds/desktop/shell_live/sidebar_delete.ex:176 #: lib/bds/desktop/shell_live/sidebar_delete.ex:176
#: lib/bds/ui/registry.ex:133 #: lib/bds/ui/registry.ex:133
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@@ -2431,7 +2431,7 @@ msgstr "Sous-menu"
msgid "Switch project" msgid "Switch project"
msgstr "Changer de projet" msgstr "Changer de projet"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:82 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:94
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Sync" msgid "Sync"
msgstr "Synchroniser" msgstr "Synchroniser"
@@ -2468,11 +2468,11 @@ msgstr "Nom du mot-clé"
#: lib/bds/desktop/shell_live/index.html.heex:325 #: lib/bds/desktop/shell_live/index.html.heex:325
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:161 #: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:161
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:158 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:158
#: lib/bds/desktop/shell_live/tags_editor.ex:94 #: lib/bds/desktop/shell_live/tags_editor.ex:104
#: lib/bds/desktop/shell_live/tags_editor.ex:136 #: lib/bds/desktop/shell_live/tags_editor.ex:166
#: lib/bds/desktop/shell_live/tags_editor.ex:189 #: lib/bds/desktop/shell_live/tags_editor.ex:219
#: lib/bds/desktop/shell_live/tags_editor.ex:203 #: lib/bds/desktop/shell_live/tags_editor.ex:233
#: lib/bds/desktop/shell_live/tags_editor.ex:234 #: lib/bds/desktop/shell_live/tags_editor.ex:264
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:11 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:11
#: lib/bds/ui/registry.ex:54 #: lib/bds/ui/registry.ex:54
#: lib/bds/ui/registry.ex:103 #: lib/bds/ui/registry.ex:103
@@ -2483,7 +2483,7 @@ msgstr "Nom du mot-clé"
msgid "Tags" msgid "Tags"
msgstr "Tags" msgstr "Tags"
#: lib/bds/desktop/shell_live.ex:927 #: lib/bds/desktop/shell_live.ex:948
#: lib/bds/desktop/shell_live/panel_renderer.ex:54 #: lib/bds/desktop/shell_live/panel_renderer.ex:54
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Tasks" msgid "Tasks"
@@ -2497,7 +2497,7 @@ msgstr "Technologie"
#: lib/bds/desktop/shell_live/misc_editor.ex:747 #: lib/bds/desktop/shell_live/misc_editor.ex:747
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:286 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:286
#: lib/bds/desktop/shell_live/sidebar_components.ex:524 #: lib/bds/desktop/shell_live/sidebar_components.ex:660
#: lib/bds/desktop/shell_live/sidebar_delete.ex:179 #: lib/bds/desktop/shell_live/sidebar_delete.ex:179
#: lib/bds/ui/registry.ex:134 #: lib/bds/ui/registry.ex:134
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@@ -2648,9 +2648,9 @@ msgstr "Afficher ou masquer la barre latérale"
#: lib/bds/desktop/shell_live/media_editor.ex:558 #: lib/bds/desktop/shell_live/media_editor.ex:558
#: lib/bds/desktop/shell_live/media_editor.ex:563 #: lib/bds/desktop/shell_live/media_editor.ex:563
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:76 #: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:76
#: lib/bds/desktop/shell_live/post_editor.ex:691 #: lib/bds/desktop/shell_live/post_editor.ex:696
#: lib/bds/desktop/shell_live/post_editor.ex:720
#: lib/bds/desktop/shell_live/post_editor.ex:725 #: lib/bds/desktop/shell_live/post_editor.ex:725
#: lib/bds/desktop/shell_live/post_editor.ex:730
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:60 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:60
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Translate" msgid "Translate"
@@ -2727,7 +2727,7 @@ msgstr "Dissocier de l'article"
#: lib/bds/desktop/shell_live/media_editor.ex:701 #: lib/bds/desktop/shell_live/media_editor.ex:701
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:10 #: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:10
#: lib/bds/desktop/shell_live/post_editor.ex:890 #: lib/bds/desktop/shell_live/post_editor.ex:895
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:7 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:7
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Unsaved" msgid "Unsaved"
@@ -2735,7 +2735,7 @@ msgstr "Non enregistré"
#: lib/bds/desktop/shell_live/import_editor.ex:867 #: lib/bds/desktop/shell_live/import_editor.ex:867
#: lib/bds/desktop/shell_live/post_editor/post_metadata.ex:166 #: lib/bds/desktop/shell_live/post_editor/post_metadata.ex:166
#: lib/bds/ui/sidebar.ex:1014 #: lib/bds/ui/sidebar.ex:1105
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Untitled" msgid "Untitled"
msgstr "Sans titre" msgstr "Sans titre"
@@ -2829,14 +2829,12 @@ msgid "WYSIWYG"
msgstr "WYSIWYG" msgstr "WYSIWYG"
#: lib/bds/desktop/shell_live/tab_helpers.ex:191 #: lib/bds/desktop/shell_live/tab_helpers.ex:191
#: lib/bds/ui/sidebar.ex:791
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Working tree" msgid "Working tree"
msgstr "Arbre de travail" msgstr "Arbre de travail"
#: lib/bds/desktop/shell_live/tab_helpers.ex:191 #: lib/bds/desktop/shell_live/tab_helpers.ex:191
#: lib/bds/ui/sidebar.ex:785 #: lib/bds/ui/sidebar.ex:787
#: lib/bds/ui/sidebar.ex:792
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Working tree and history" msgid "Working tree and history"
msgstr "Arbre de travail et historique" msgstr "Arbre de travail et historique"
@@ -3179,12 +3177,12 @@ msgstr "Bienvenue dans lassistant IA"
msgid "Comparing database and filesystem metadata" msgid "Comparing database and filesystem metadata"
msgstr "Comparaison des métadonnées entre la base et le système de fichiers" msgstr "Comparaison des métadonnées entre la base et le système de fichiers"
#: lib/bds/desktop/shell_live.ex:650 #: lib/bds/desktop/shell_live.ex:671
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Added %{count} images to post" msgid "Added %{count} images to post"
msgstr "%{count} images ajoutées à l'article" msgstr "%{count} images ajoutées à l'article"
#: lib/bds/desktop/shell_live.ex:621 #: lib/bds/desktop/shell_live.ex:642
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Added %{title}" msgid "Added %{title}"
msgstr "%{title} ajouté" msgstr "%{title} ajouté"
@@ -3204,18 +3202,18 @@ msgstr "Guide utilisateur pour les flux éditoriaux, médias, modèles, traducti
msgid "Image Import Concurrency" msgid "Image Import Concurrency"
msgstr "Importation simultanée d'images" msgstr "Importation simultanée d'images"
#: lib/bds/desktop/shell_live.ex:408 #: lib/bds/desktop/shell_live.ex:429
#: lib/bds/desktop/shell_live.ex:421 #: lib/bds/desktop/shell_live.ex:442
#: lib/bds/desktop/shell_live.ex:621 #: lib/bds/desktop/shell_live.ex:642
#: lib/bds/desktop/shell_live.ex:649 #: lib/bds/desktop/shell_live.ex:670
#: lib/bds/desktop/shell_live.ex:658 #: lib/bds/desktop/shell_live.ex:679
#: lib/bds/desktop/shell_live.ex:665 #: lib/bds/desktop/shell_live.ex:686
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:407 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:407
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Add Gallery Images" msgid "Add Gallery Images"
msgstr "Ajouter des images à la galerie" msgstr "Ajouter des images à la galerie"
#: lib/bds/desktop/shell_live.ex:666 #: lib/bds/desktop/shell_live.ex:687
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Failed to process %{path}: %{reason}" msgid "Failed to process %{path}: %{reason}"
msgstr "Impossible de traiter %{path} : %{reason}" msgstr "Impossible de traiter %{path} : %{reason}"
@@ -3230,12 +3228,12 @@ msgstr "Archiver"
msgid "Move this post to the archive" msgid "Move this post to the archive"
msgstr "Déplacer cet article dans les archives" msgstr "Déplacer cet article dans les archives"
#: lib/bds/desktop/shell_live/post_editor.ex:596 #: lib/bds/desktop/shell_live/post_editor.ex:601
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Post archived" msgid "Post archived"
msgstr "Article archivé" msgstr "Article archivé"
#: lib/bds/desktop/shell_live/post_editor.ex:629 #: lib/bds/desktop/shell_live/post_editor.ex:634
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Post unarchived" msgid "Post unarchived"
msgstr "Article désarchivé" msgstr "Article désarchivé"
@@ -3249,3 +3247,157 @@ msgstr "Restaurer cet article en brouillon"
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Unarchive" msgid "Unarchive"
msgstr "Désarchiver" msgstr "Désarchiver"
#: lib/bds/desktop/shell_live/sidebar_components.ex:531
#, elixir-autogen, elixir-format
msgid "Ahead"
msgstr "En avance"
#: lib/bds/desktop/shell_live/sidebar_components.ex:532
#, elixir-autogen, elixir-format
msgid "Behind"
msgstr "En retard"
#: lib/bds/desktop/shell_live/sidebar_components.ex:550
#, elixir-autogen, elixir-format
msgid "Changes"
msgstr "Modifications"
#: lib/bds/desktop/shell_live.ex:1055
#: lib/bds/desktop/shell_live.ex:1071
#: lib/bds/desktop/shell_live.ex:1077
#: lib/bds/desktop/shell_live/sidebar_components.ex:556
#, elixir-autogen, elixir-format
msgid "Commit"
msgstr "Commit"
#: lib/bds/desktop/shell_live/sidebar_components.ex:555
#, elixir-autogen, elixir-format
msgid "Commit message"
msgstr "Message de commit"
#: lib/bds/desktop/shell_live.ex:1056
#, elixir-autogen, elixir-format
msgid "Commit message is required"
msgstr "Le message de commit est requis"
#: lib/bds/desktop/shell_live.ex:1120
#, elixir-autogen, elixir-format
msgid "Done"
msgstr "Terminé"
#: lib/bds/desktop/shell_live.ex:1041
#: lib/bds/desktop/shell_live/sidebar_components.ex:542
#: lib/bds/desktop/shell_live/sidebar_components.ex:542
#, elixir-autogen, elixir-format
msgid "Fetch"
msgstr "Récupérer"
#: lib/bds/desktop/shell_live/sidebar_components.ex:586
#, elixir-autogen, elixir-format
msgid "History"
msgstr "Historique"
#: lib/bds/desktop/shell_live.ex:1091
#: lib/bds/desktop/shell_live.ex:1099
#: lib/bds/desktop/shell_live/sidebar_components.ex:513
#, elixir-autogen, elixir-format
msgid "Initialize Git"
msgstr "Initialiser Git"
#: lib/bds/desktop/shell_live/sidebar_components.ex:536
#, elixir-autogen, elixir-format
msgid "Local only"
msgstr "Local uniquement"
#: lib/bds/desktop/shell_live.ex:1127
#, elixir-autogen, elixir-format
msgid "No active project"
msgstr "Aucun projet actif"
#: lib/bds/desktop/shell_live/sidebar_components.ex:580
#, elixir-autogen, elixir-format
msgid "No changes"
msgstr "Aucune modification"
#: lib/bds/desktop/shell_live/sidebar_components.ex:616
#, elixir-autogen, elixir-format
msgid "No commits yet"
msgstr "Aucun commit pour linstant"
#: lib/bds/desktop/shell_live/sidebar_components.ex:613
#, elixir-autogen, elixir-format
msgid "Older history available"
msgstr "Historique plus ancien disponible"
#: lib/bds/desktop/shell_live.ex:1044
#: lib/bds/desktop/shell_live/sidebar_components.ex:545
#: lib/bds/desktop/shell_live/sidebar_components.ex:545
#, elixir-autogen, elixir-format
msgid "Prune LFS"
msgstr "Nettoyer LFS"
#: lib/bds/desktop/shell_live.ex:1042
#: lib/bds/desktop/shell_live/sidebar_components.ex:543
#: lib/bds/desktop/shell_live/sidebar_components.ex:543
#, elixir-autogen, elixir-format
msgid "Pull"
msgstr "Pull"
#: lib/bds/desktop/shell_live.ex:1043
#: lib/bds/desktop/shell_live/sidebar_components.ex:544
#: lib/bds/desktop/shell_live/sidebar_components.ex:544
#, elixir-autogen, elixir-format
msgid "Push"
msgstr "Push"
#: lib/bds/desktop/shell_live/sidebar_components.ex:509
#, elixir-autogen, elixir-format
msgid "Remote URL (optional)"
msgstr "URL distante (facultatif)"
#: lib/bds/desktop/shell_live/sidebar_components.ex:537
#, elixir-autogen, elixir-format
msgid "Remote only"
msgstr "Distant uniquement"
#: lib/bds/desktop/shell_live.ex:1092
#, elixir-autogen, elixir-format
msgid "Repository initialized"
msgstr "Dépôt initialisé"
#: lib/bds/desktop/shell_live/sidebar_components.ex:535
#, elixir-autogen, elixir-format
msgid "Synced"
msgstr "Synchronisé"
#: lib/bds/desktop/shell_live/sidebar_components.ex:504
#, elixir-autogen, elixir-format
msgid "This project is not a Git repository yet."
msgstr "Ce projet nest pas encore un dépôt Git."
#: lib/bds/ui/sidebar.ex:869
#, elixir-autogen, elixir-format
msgid "added"
msgstr "ajouté"
#: lib/bds/ui/sidebar.ex:870
#, elixir-autogen, elixir-format
msgid "deleted"
msgstr "supprimé"
#: lib/bds/ui/sidebar.ex:871
#: lib/bds/ui/sidebar.ex:874
#, elixir-autogen, elixir-format
msgid "modified"
msgstr "modifié"
#: lib/bds/ui/sidebar.ex:872
#, elixir-autogen, elixir-format
msgid "renamed"
msgstr "renommé"
#: lib/bds/ui/sidebar.ex:873
#, elixir-autogen, elixir-format
msgid "untracked"
msgstr "non suivi"

View File

@@ -5,7 +5,7 @@
msgid "Archive" msgid "Archive"
msgstr "Archivio" msgstr "Archivio"
#: lib/bds/rendering/labels.ex:54 #: lib/bds/rendering/labels.ex:55
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "April" msgid "April"
msgstr "aprile" msgstr "aprile"
@@ -15,7 +15,7 @@ msgstr "aprile"
msgid "Archive calendar" msgid "Archive calendar"
msgstr "Archivio" msgstr "Archivio"
#: lib/bds/rendering/labels.ex:70 #: lib/bds/rendering/labels.ex:71
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "August" msgid "August"
msgstr "agosto" msgstr "agosto"
@@ -35,27 +35,27 @@ msgstr "Impossibile caricare i dati del calendario."
msgid "Close calendar" msgid "Close calendar"
msgstr "Chiudi calendario" msgstr "Chiudi calendario"
#: lib/bds/rendering/labels.ex:86 #: lib/bds/rendering/labels.ex:87
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "December" msgid "December"
msgstr "dicembre" msgstr "dicembre"
#: lib/bds/rendering/labels.ex:46 #: lib/bds/rendering/labels.ex:47
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "February" msgid "February"
msgstr "febbraio" msgstr "febbraio"
#: lib/bds/rendering/labels.ex:42 #: lib/bds/rendering/labels.ex:43
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "January" msgid "January"
msgstr "gennaio" msgstr "gennaio"
#: lib/bds/rendering/labels.ex:66 #: lib/bds/rendering/labels.ex:67
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "July" msgid "July"
msgstr "luglio" msgstr "luglio"
#: lib/bds/rendering/labels.ex:62 #: lib/bds/rendering/labels.ex:63
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "June" msgid "June"
msgstr "giugno" msgstr "giugno"
@@ -75,22 +75,22 @@ msgstr "Collegato da"
msgid "Loading calendar…" msgid "Loading calendar…"
msgstr "Caricamento calendario…" msgstr "Caricamento calendario…"
#: lib/bds/rendering/labels.ex:50 #: lib/bds/rendering/labels.ex:51
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "March" msgid "March"
msgstr "marzo" msgstr "marzo"
#: lib/bds/rendering/labels.ex:58 #: lib/bds/rendering/labels.ex:59
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "May" msgid "May"
msgstr "maggio" msgstr "maggio"
#: lib/bds/rendering/labels.ex:82 #: lib/bds/rendering/labels.ex:83
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "November" msgid "November"
msgstr "novembre" msgstr "novembre"
#: lib/bds/rendering/labels.ex:78 #: lib/bds/rendering/labels.ex:79
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "October" msgid "October"
msgstr "ottobre" msgstr "ottobre"
@@ -110,7 +110,7 @@ msgstr "Paginazione"
msgid "Search..." msgid "Search..."
msgstr "Cerca..." msgstr "Cerca..."
#: lib/bds/rendering/labels.ex:74 #: lib/bds/rendering/labels.ex:75
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "September" msgid "September"
msgstr "settembre" msgstr "settembre"
@@ -135,22 +135,27 @@ msgstr "più recente"
msgid "older" msgid "older"
msgstr "più vecchio" msgstr "più vecchio"
#: lib/bds/rendering/labels.ex:31 #: lib/bds/rendering/labels.ex:32
#, 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:30 #: lib/bds/rendering/labels.ex:31
#, 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:33 #: lib/bds/rendering/labels.ex:34
#, 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:32 #: lib/bds/rendering/labels.ex:33
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "YouTube video" msgid "YouTube video"
msgstr "Video YouTube" msgstr "Video YouTube"
#: lib/bds/rendering/labels.ex:30
#, elixir-autogen, elixir-format
msgid "No results found"
msgstr "Nessun risultato trovato"

View File

@@ -79,7 +79,7 @@ msgstr "Impostazioni IA"
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:42 #: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:42
#: lib/bds/desktop/shell_live/overlay_manager.ex:72 #: lib/bds/desktop/shell_live/overlay_manager.ex:72
#: lib/bds/desktop/shell_live/post_editor.ex:776 #: lib/bds/desktop/shell_live/post_editor.ex:781
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:43 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:43
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "AI Suggestions" msgid "AI Suggestions"
@@ -257,14 +257,14 @@ msgid "Auto"
msgstr "Automatico" msgstr "Automatico"
#: lib/bds/desktop/shell_data.ex:98 #: lib/bds/desktop/shell_data.ex:98
#: lib/bds/desktop/shell_live.ex:409 #: lib/bds/desktop/shell_live.ex:430
#: lib/bds/desktop/shell_live/chat_editor.ex:231 #: lib/bds/desktop/shell_live/chat_editor.ex:231
#: lib/bds/desktop/shell_live/media_editor.ex:156 #: lib/bds/desktop/shell_live/media_editor.ex:156
#: lib/bds/desktop/shell_live/media_editor.ex:349 #: lib/bds/desktop/shell_live/media_editor.ex:349
#: lib/bds/desktop/shell_live/media_editor.ex:538 #: lib/bds/desktop/shell_live/media_editor.ex:538
#: lib/bds/desktop/shell_live/overlay_manager.ex:73 #: lib/bds/desktop/shell_live/overlay_manager.ex:73
#: lib/bds/desktop/shell_live/post_editor.ex:643 #: lib/bds/desktop/shell_live/post_editor.ex:648
#: lib/bds/desktop/shell_live/post_editor.ex:692 #: lib/bds/desktop/shell_live/post_editor.ex:697
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Automatic AI actions stay gated by airplane mode." msgid "Automatic AI actions stay gated by airplane mode."
msgstr "Le azioni IA automatiche restano bloccate dalla modalità aereo." msgstr "Le azioni IA automatiche restano bloccate dalla modalità aereo."
@@ -404,7 +404,7 @@ msgstr "Valori predefiniti delle categorie, opzioni di rendering e collegamento
msgid "Category name is required" msgid "Category name is required"
msgstr "Il nome della categoria è obbligatorio" msgstr "Il nome della categoria è obbligatorio"
#: lib/bds/desktop/shell_live.ex:932 #: lib/bds/desktop/shell_live.ex:953
#: lib/bds/desktop/shell_live/chat_editor.ex:87 #: lib/bds/desktop/shell_live/chat_editor.ex:87
#: lib/bds/desktop/shell_live/chat_editor.ex:230 #: lib/bds/desktop/shell_live/chat_editor.ex:230
#: lib/bds/desktop/shell_live/chat_editor.ex:318 #: lib/bds/desktop/shell_live/chat_editor.ex:318
@@ -537,7 +537,7 @@ msgstr "Impossibile leggere la configurazione MCP %{path}: %{reason}"
msgid "Could not write MCP config %{path}: %{reason}" msgid "Could not write MCP config %{path}: %{reason}"
msgstr "Impossibile scrivere la configurazione MCP %{path}: %{reason}" msgstr "Impossibile scrivere la configurazione MCP %{path}: %{reason}"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:42 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:48
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Create" msgid "Create"
msgstr "Crea" msgstr "Crea"
@@ -636,12 +636,12 @@ msgstr "Modalità di modifica predefinita e presentazione dei diff"
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:166 #: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:166
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:116 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:116
#: lib/bds/desktop/shell_live/script_editor_html/script_editor.html.heex:16 #: lib/bds/desktop/shell_live/script_editor_html/script_editor.html.heex:16
#: lib/bds/desktop/shell_live/sidebar_components.ex:515 #: lib/bds/desktop/shell_live/sidebar_components.ex:651
#: lib/bds/desktop/shell_live/sidebar_components.ex:518 #: lib/bds/desktop/shell_live/sidebar_components.ex:654
#: lib/bds/desktop/shell_live/sidebar_components.ex:521 #: lib/bds/desktop/shell_live/sidebar_components.ex:657
#: lib/bds/desktop/shell_live/sidebar_components.ex:524 #: lib/bds/desktop/shell_live/sidebar_components.ex:660
#: lib/bds/desktop/shell_live/sidebar_components.ex:527 #: lib/bds/desktop/shell_live/sidebar_components.ex:663
#: lib/bds/desktop/shell_live/sidebar_components.ex:529 #: lib/bds/desktop/shell_live/sidebar_components.ex:665
#: lib/bds/desktop/shell_live/sidebar_delete.ex:72 #: lib/bds/desktop/shell_live/sidebar_delete.ex:72
#: lib/bds/desktop/shell_live/sidebar_delete.ex:174 #: lib/bds/desktop/shell_live/sidebar_delete.ex:174
#: lib/bds/desktop/shell_live/sidebar_delete.ex:175 #: lib/bds/desktop/shell_live/sidebar_delete.ex:175
@@ -649,7 +649,7 @@ msgstr "Modalità di modifica predefinita e presentazione dei diff"
#: lib/bds/desktop/shell_live/sidebar_delete.ex:179 #: lib/bds/desktop/shell_live/sidebar_delete.ex:179
#: lib/bds/desktop/shell_live/sidebar_delete.ex:181 #: lib/bds/desktop/shell_live/sidebar_delete.ex:181
#: lib/bds/desktop/shell_live/sidebar_delete.ex:182 #: lib/bds/desktop/shell_live/sidebar_delete.ex:182
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:58 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:70
#: lib/bds/desktop/shell_live/template_editor_html/template_editor.html.heex:15 #: lib/bds/desktop/shell_live/template_editor_html/template_editor.html.heex:15
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Delete" msgid "Delete"
@@ -665,7 +665,7 @@ msgstr "Elimina media"
msgid "Delete Translation" msgid "Delete Translation"
msgstr "Elimina traduzione" msgstr "Elimina traduzione"
#: lib/bds/desktop/shell_live/sidebar_components.ex:514 #: lib/bds/desktop/shell_live/sidebar_components.ex:650
#: lib/bds/desktop/shell_live/sidebar_delete.ex:173 #: lib/bds/desktop/shell_live/sidebar_delete.ex:173
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Delete conversation" msgid "Delete conversation"
@@ -706,9 +706,9 @@ msgstr "Rileva"
#: lib/bds/desktop/shell_live/media_editor.ex:199 #: lib/bds/desktop/shell_live/media_editor.ex:199
#: lib/bds/desktop/shell_live/media_editor.ex:205 #: lib/bds/desktop/shell_live/media_editor.ex:205
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:59 #: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:59
#: lib/bds/desktop/shell_live/post_editor.ex:642 #: lib/bds/desktop/shell_live/post_editor.ex:647
#: lib/bds/desktop/shell_live/post_editor.ex:671 #: lib/bds/desktop/shell_live/post_editor.ex:676
#: lib/bds/desktop/shell_live/post_editor.ex:677 #: lib/bds/desktop/shell_live/post_editor.ex:682
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Detect Language" msgid "Detect Language"
msgstr "Rileva lingua" msgstr "Rileva lingua"
@@ -749,7 +749,7 @@ msgid "Discard changes and restore the published version"
msgstr "Annulla le modifiche e ripristina la versione pubblicata" msgstr "Annulla le modifiche e ripristina la versione pubblicata"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:21 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:21
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:84 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:96
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Discover" msgid "Discover"
msgstr "Scopri" msgstr "Scopri"
@@ -991,8 +991,8 @@ msgstr "Galleria"
msgid "Generate Site" msgid "Generate Site"
msgstr "Genera sito" msgstr "Genera sito"
#: lib/bds/desktop/shell_live.ex:933 #: lib/bds/desktop/shell_live.ex:954
#: lib/bds/ui/sidebar.ex:784 #: lib/bds/ui/sidebar.ex:786
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Git" msgid "Git"
msgstr "Git" msgstr "Git"
@@ -1004,7 +1004,7 @@ msgid "Git Diff"
msgstr "Diff Git" msgstr "Diff Git"
#: lib/bds/desktop/shell_data.ex:244 #: lib/bds/desktop/shell_data.ex:244
#: lib/bds/desktop/shell_live.ex:929 #: lib/bds/desktop/shell_live.ex:950
#: lib/bds/desktop/shell_live/panel_renderer.ex:171 #: lib/bds/desktop/shell_live/panel_renderer.ex:171
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Git Log" msgid "Git Log"
@@ -1033,7 +1033,7 @@ msgstr "Host"
#: lib/bds/desktop/shell_data.ex:116 #: lib/bds/desktop/shell_data.ex:116
#: lib/bds/desktop/shell_live/index.html.heex:666 #: lib/bds/desktop/shell_live/index.html.heex:666
#: lib/bds/desktop/shell_live/media_editor.ex:703 #: lib/bds/desktop/shell_live/media_editor.ex:703
#: lib/bds/desktop/shell_live/post_editor.ex:894 #: lib/bds/desktop/shell_live/post_editor.ex:899
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Idle" msgid "Idle"
msgstr "Inattivo" msgstr "Inattivo"
@@ -1075,7 +1075,7 @@ msgstr "Immagini e file"
#: lib/bds/desktop/shell_live/import_editor/taxonomy_editing.ex:127 #: lib/bds/desktop/shell_live/import_editor/taxonomy_editing.ex:127
#: lib/bds/desktop/shell_live/import_editor/taxonomy_editing.ex:137 #: lib/bds/desktop/shell_live/import_editor/taxonomy_editing.ex:137
#: lib/bds/desktop/shell_live/misc_editor_html/misc_editor.html.heex:182 #: lib/bds/desktop/shell_live/misc_editor_html/misc_editor.html.heex:182
#: lib/bds/desktop/shell_live/sidebar_components.ex:527 #: lib/bds/desktop/shell_live/sidebar_components.ex:663
#: lib/bds/desktop/shell_live/sidebar_delete.ex:181 #: lib/bds/desktop/shell_live/sidebar_delete.ex:181
#: lib/bds/ui/registry.ex:70 #: lib/bds/ui/registry.ex:70
#: lib/bds/ui/registry.ex:105 #: lib/bds/ui/registry.ex:105
@@ -1127,9 +1127,9 @@ msgstr "Definizioni di importazione"
msgid "Import failed: %{error}" msgid "Import failed: %{error}"
msgstr "Importazione non riuscita: %{error}" msgstr "Importazione non riuscita: %{error}"
#: lib/bds/desktop/shell_live.ex:580 #: lib/bds/desktop/shell_live.ex:601
#: lib/bds/desktop/shell_live.ex:970 #: lib/bds/desktop/shell_live.ex:991
#: lib/bds/desktop/shell_live.ex:976 #: lib/bds/desktop/shell_live.ex:997
#: lib/bds/desktop/shell_live/sidebar_create.ex:47 #: lib/bds/desktop/shell_live/sidebar_create.ex:47
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Import media" msgid "Import media"
@@ -1204,7 +1204,7 @@ msgid "Language"
msgstr "Lingua" msgstr "Lingua"
#: lib/bds/desktop/shell_live/media_editor.ex:206 #: lib/bds/desktop/shell_live/media_editor.ex:206
#: lib/bds/desktop/shell_live/post_editor.ex:678 #: lib/bds/desktop/shell_live/post_editor.ex:683
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Language detection failed." msgid "Language detection failed."
msgstr "Rilevamento della lingua non riuscito." msgstr "Rilevamento della lingua non riuscito."
@@ -1288,7 +1288,7 @@ msgstr "Mappa a..."
msgid "Mapped" msgid "Mapped"
msgstr "Mappato" msgstr "Mappato"
#: lib/bds/desktop/shell_live/post_editor.ex:897 #: lib/bds/desktop/shell_live/post_editor.ex:902
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:120 #: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:120
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Markdown" msgid "Markdown"
@@ -1304,7 +1304,7 @@ msgstr "Numero massimo di post per pagina"
#: lib/bds/desktop/shell_live/media_editor.ex:494 #: lib/bds/desktop/shell_live/media_editor.ex:494
#: lib/bds/desktop/shell_live/misc_editor.ex:744 #: lib/bds/desktop/shell_live/misc_editor.ex:744
#: lib/bds/desktop/shell_live/misc_editor.ex:771 #: lib/bds/desktop/shell_live/misc_editor.ex:771
#: lib/bds/desktop/shell_live/sidebar_components.ex:518 #: lib/bds/desktop/shell_live/sidebar_components.ex:654
#: lib/bds/desktop/shell_live/sidebar_delete.ex:175 #: lib/bds/desktop/shell_live/sidebar_delete.ex:175
#: lib/bds/ui/registry.ex:30 #: lib/bds/ui/registry.ex:30
#: lib/bds/ui/registry.ex:100 #: lib/bds/ui/registry.ex:100
@@ -1328,12 +1328,12 @@ msgstr "Media salvato"
msgid "Menu" msgid "Menu"
msgstr "Menu" msgstr "Menu"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:75 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:87
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Merge" msgid "Merge"
msgstr "Unisci" msgstr "Unisci"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:66 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:78
#: lib/bds/ui/sidebar.ex:747 #: lib/bds/ui/sidebar.ex:747
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Merge Tags" msgid "Merge Tags"
@@ -1424,7 +1424,7 @@ msgstr "Nuovo sottomenu"
msgid "New Template" msgid "New Template"
msgstr "Nuovo modello" msgstr "Nuovo modello"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:52 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:64
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "No Template" msgid "No Template"
msgstr "Nessun template" msgstr "Nessun template"
@@ -1453,12 +1453,12 @@ msgstr "Nessuna cronologia Git"
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:72 #: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:72
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:308 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:308
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:320 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:320
#: lib/bds/desktop/shell_live/sidebar_components.ex:320 #: lib/bds/desktop/shell_live/sidebar_components.ex:321
#: lib/bds/desktop/shell_live/sidebar_components.ex:380 #: lib/bds/desktop/shell_live/sidebar_components.ex:381
#: lib/bds/desktop/shell_live/sidebar_components.ex:454 #: lib/bds/desktop/shell_live/sidebar_components.ex:455
#: lib/bds/ui/sidebar.ex:198 #: lib/bds/ui/sidebar.ex:198
#: lib/bds/ui/sidebar.ex:787 #: lib/bds/ui/sidebar.ex:789
#: lib/bds/ui/sidebar.ex:805 #: lib/bds/ui/sidebar.ex:896
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "No items" msgid "No items"
msgstr "Nessun elemento" msgstr "Nessun elemento"
@@ -1721,7 +1721,7 @@ msgstr "Altro"
msgid "Other (%{count})" msgid "Other (%{count})"
msgstr "Altro (%{count})" msgstr "Altro (%{count})"
#: lib/bds/desktop/shell_live.ex:928 #: lib/bds/desktop/shell_live.ex:949
#: lib/bds/desktop/shell_live/panel_renderer.ex:83 #: lib/bds/desktop/shell_live/panel_renderer.ex:83
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Output" msgid "Output"
@@ -1786,17 +1786,17 @@ msgid "Persist the detected language for this media item"
msgstr "Salva la lingua rilevata per questo media" msgstr "Salva la lingua rilevata per questo media"
#: lib/bds/desktop/shell_live/misc_editor.ex:742 #: lib/bds/desktop/shell_live/misc_editor.ex:742
#: lib/bds/desktop/shell_live/post_editor.ex:474 #: lib/bds/desktop/shell_live/post_editor.ex:479
#: lib/bds/desktop/shell_live/post_editor.ex:478 #: lib/bds/desktop/shell_live/post_editor.ex:483
#: lib/bds/desktop/shell_live/post_editor.ex:513 #: lib/bds/desktop/shell_live/post_editor.ex:518
#: lib/bds/desktop/shell_live/post_editor.ex:517 #: lib/bds/desktop/shell_live/post_editor.ex:522
#: lib/bds/desktop/shell_live/post_editor.ex:552 #: lib/bds/desktop/shell_live/post_editor.ex:557
#: lib/bds/desktop/shell_live/post_editor.ex:567 #: lib/bds/desktop/shell_live/post_editor.ex:572
#: lib/bds/desktop/shell_live/post_editor.ex:596 #: lib/bds/desktop/shell_live/post_editor.ex:601
#: lib/bds/desktop/shell_live/post_editor.ex:599 #: lib/bds/desktop/shell_live/post_editor.ex:604
#: lib/bds/desktop/shell_live/post_editor.ex:629 #: lib/bds/desktop/shell_live/post_editor.ex:634
#: lib/bds/desktop/shell_live/post_editor.ex:632 #: lib/bds/desktop/shell_live/post_editor.ex:637
#: lib/bds/desktop/shell_live/sidebar_components.ex:515 #: lib/bds/desktop/shell_live/sidebar_components.ex:651
#: lib/bds/desktop/shell_live/sidebar_delete.ex:174 #: lib/bds/desktop/shell_live/sidebar_delete.ex:174
#: lib/bds/ui/registry.ex:99 #: lib/bds/ui/registry.ex:99
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@@ -1825,12 +1825,12 @@ msgstr "Template del post"
msgid "Post is marked as do-not-translate but has translations" msgid "Post is marked as do-not-translate but has translations"
msgstr "Il post è contrassegnato come non-tradurre ma ha traduzioni" msgstr "Il post è contrassegnato come non-tradurre ma ha traduzioni"
#: lib/bds/desktop/shell_live/post_editor.ex:513 #: lib/bds/desktop/shell_live/post_editor.ex:518
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Post published" msgid "Post published"
msgstr "Articolo pubblicato" msgstr "Articolo pubblicato"
#: lib/bds/desktop/shell_live/post_editor.ex:474 #: lib/bds/desktop/shell_live/post_editor.ex:479
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Post saved" msgid "Post saved"
msgstr "Articolo salvato" msgstr "Articolo salvato"
@@ -1854,7 +1854,7 @@ msgstr "Articoli (%{count})"
msgid "Preferences" msgid "Preferences"
msgstr "Preferenze" msgstr "Preferenze"
#: lib/bds/desktop/shell_live/post_editor.ex:898 #: lib/bds/desktop/shell_live/post_editor.ex:903
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:121 #: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:121
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Preview" msgid "Preview"
@@ -1923,7 +1923,7 @@ msgid "Publish Selected"
msgstr "Pubblica selezionati" msgstr "Pubblica selezionati"
#: lib/bds/desktop/shell_data.ex:181 #: lib/bds/desktop/shell_data.ex:181
#: lib/bds/desktop/shell_live/post_editor.ex:892 #: lib/bds/desktop/shell_live/post_editor.ex:897
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:456 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:456
#: lib/bds/ui/sidebar.ex:320 #: lib/bds/ui/sidebar.ex:320
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@@ -2121,7 +2121,7 @@ msgstr "Risoluzione"
msgid "Result" msgid "Result"
msgstr "Risultato" msgstr "Risultato"
#: lib/bds/desktop/shell_live/post_editor.ex:893 #: lib/bds/desktop/shell_live/post_editor.ex:898
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Reverted" msgid "Reverted"
msgstr "Ripristinato" msgstr "Ripristinato"
@@ -2161,7 +2161,7 @@ msgstr "Modalità SSH"
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:312 #: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:312
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:329 #: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:329
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:342 #: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:342
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:57 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:69
#: lib/bds/desktop/shell_live/template_editor_html/template_editor.html.heex:13 #: lib/bds/desktop/shell_live/template_editor_html/template_editor.html.heex:13
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Save" msgid "Save"
@@ -2173,7 +2173,7 @@ msgid "Save Translation"
msgstr "Salva traduzione" msgstr "Salva traduzione"
#: lib/bds/desktop/shell_live/media_editor.ex:702 #: lib/bds/desktop/shell_live/media_editor.ex:702
#: lib/bds/desktop/shell_live/post_editor.ex:891 #: lib/bds/desktop/shell_live/post_editor.ex:896
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Saved" msgid "Saved"
msgstr "Salvato" msgstr "Salvato"
@@ -2184,7 +2184,7 @@ msgid "Scanning entries..."
msgstr "Scansione delle voci..." msgstr "Scansione delle voci..."
#: lib/bds/desktop/shell_live/misc_editor.ex:746 #: lib/bds/desktop/shell_live/misc_editor.ex:746
#: lib/bds/desktop/shell_live/sidebar_components.ex:521 #: lib/bds/desktop/shell_live/sidebar_components.ex:657
#: lib/bds/desktop/shell_live/sidebar_delete.ex:176 #: lib/bds/desktop/shell_live/sidebar_delete.ex:176
#: lib/bds/ui/registry.ex:133 #: lib/bds/ui/registry.ex:133
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@@ -2431,7 +2431,7 @@ msgstr "Sottomenu"
msgid "Switch project" msgid "Switch project"
msgstr "Cambia progetto" msgstr "Cambia progetto"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:82 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:94
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Sync" msgid "Sync"
msgstr "Sincronizza" msgstr "Sincronizza"
@@ -2468,11 +2468,11 @@ msgstr "Nome del tag"
#: lib/bds/desktop/shell_live/index.html.heex:325 #: lib/bds/desktop/shell_live/index.html.heex:325
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:161 #: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:161
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:158 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:158
#: lib/bds/desktop/shell_live/tags_editor.ex:94 #: lib/bds/desktop/shell_live/tags_editor.ex:104
#: lib/bds/desktop/shell_live/tags_editor.ex:136 #: lib/bds/desktop/shell_live/tags_editor.ex:166
#: lib/bds/desktop/shell_live/tags_editor.ex:189 #: lib/bds/desktop/shell_live/tags_editor.ex:219
#: lib/bds/desktop/shell_live/tags_editor.ex:203 #: lib/bds/desktop/shell_live/tags_editor.ex:233
#: lib/bds/desktop/shell_live/tags_editor.ex:234 #: lib/bds/desktop/shell_live/tags_editor.ex:264
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:11 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:11
#: lib/bds/ui/registry.ex:54 #: lib/bds/ui/registry.ex:54
#: lib/bds/ui/registry.ex:103 #: lib/bds/ui/registry.ex:103
@@ -2483,7 +2483,7 @@ msgstr "Nome del tag"
msgid "Tags" msgid "Tags"
msgstr "Tag" msgstr "Tag"
#: lib/bds/desktop/shell_live.ex:927 #: lib/bds/desktop/shell_live.ex:948
#: lib/bds/desktop/shell_live/panel_renderer.ex:54 #: lib/bds/desktop/shell_live/panel_renderer.ex:54
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Tasks" msgid "Tasks"
@@ -2497,7 +2497,7 @@ msgstr "Tecnologia"
#: lib/bds/desktop/shell_live/misc_editor.ex:747 #: lib/bds/desktop/shell_live/misc_editor.ex:747
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:286 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:286
#: lib/bds/desktop/shell_live/sidebar_components.ex:524 #: lib/bds/desktop/shell_live/sidebar_components.ex:660
#: lib/bds/desktop/shell_live/sidebar_delete.ex:179 #: lib/bds/desktop/shell_live/sidebar_delete.ex:179
#: lib/bds/ui/registry.ex:134 #: lib/bds/ui/registry.ex:134
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@@ -2648,9 +2648,9 @@ msgstr "Attiva/disattiva barra laterale"
#: lib/bds/desktop/shell_live/media_editor.ex:558 #: lib/bds/desktop/shell_live/media_editor.ex:558
#: lib/bds/desktop/shell_live/media_editor.ex:563 #: lib/bds/desktop/shell_live/media_editor.ex:563
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:76 #: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:76
#: lib/bds/desktop/shell_live/post_editor.ex:691 #: lib/bds/desktop/shell_live/post_editor.ex:696
#: lib/bds/desktop/shell_live/post_editor.ex:720
#: lib/bds/desktop/shell_live/post_editor.ex:725 #: lib/bds/desktop/shell_live/post_editor.ex:725
#: lib/bds/desktop/shell_live/post_editor.ex:730
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:60 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:60
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Translate" msgid "Translate"
@@ -2727,7 +2727,7 @@ msgstr "Scollega dall'articolo"
#: lib/bds/desktop/shell_live/media_editor.ex:701 #: lib/bds/desktop/shell_live/media_editor.ex:701
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:10 #: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:10
#: lib/bds/desktop/shell_live/post_editor.ex:890 #: lib/bds/desktop/shell_live/post_editor.ex:895
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:7 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:7
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Unsaved" msgid "Unsaved"
@@ -2735,7 +2735,7 @@ msgstr "Non salvato"
#: lib/bds/desktop/shell_live/import_editor.ex:867 #: lib/bds/desktop/shell_live/import_editor.ex:867
#: lib/bds/desktop/shell_live/post_editor/post_metadata.ex:166 #: lib/bds/desktop/shell_live/post_editor/post_metadata.ex:166
#: lib/bds/ui/sidebar.ex:1014 #: lib/bds/ui/sidebar.ex:1105
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Untitled" msgid "Untitled"
msgstr "Senza titolo" msgstr "Senza titolo"
@@ -2829,14 +2829,12 @@ msgid "WYSIWYG"
msgstr "WYSIWYG" msgstr "WYSIWYG"
#: lib/bds/desktop/shell_live/tab_helpers.ex:191 #: lib/bds/desktop/shell_live/tab_helpers.ex:191
#: lib/bds/ui/sidebar.ex:791
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Working tree" msgid "Working tree"
msgstr "Working tree" msgstr "Working tree"
#: lib/bds/desktop/shell_live/tab_helpers.ex:191 #: lib/bds/desktop/shell_live/tab_helpers.ex:191
#: lib/bds/ui/sidebar.ex:785 #: lib/bds/ui/sidebar.ex:787
#: lib/bds/ui/sidebar.ex:792
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Working tree and history" msgid "Working tree and history"
msgstr "Working tree e cronologia" msgstr "Working tree e cronologia"
@@ -3179,12 +3177,12 @@ msgstr "Benvenuto nellassistente IA"
msgid "Comparing database and filesystem metadata" msgid "Comparing database and filesystem metadata"
msgstr "Confronto tra i metadati del database e del filesystem" msgstr "Confronto tra i metadati del database e del filesystem"
#: lib/bds/desktop/shell_live.ex:650 #: lib/bds/desktop/shell_live.ex:671
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Added %{count} images to post" msgid "Added %{count} images to post"
msgstr "%{count} immagini aggiunte al post" msgstr "%{count} immagini aggiunte al post"
#: lib/bds/desktop/shell_live.ex:621 #: lib/bds/desktop/shell_live.ex:642
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Added %{title}" msgid "Added %{title}"
msgstr "%{title} aggiunto" msgstr "%{title} aggiunto"
@@ -3204,18 +3202,18 @@ msgstr "Guida per l'utente finale per flussi editoriali, media, modelli, traduzi
msgid "Image Import Concurrency" msgid "Image Import Concurrency"
msgstr "Importazione simultanea immagini" msgstr "Importazione simultanea immagini"
#: lib/bds/desktop/shell_live.ex:408 #: lib/bds/desktop/shell_live.ex:429
#: lib/bds/desktop/shell_live.ex:421 #: lib/bds/desktop/shell_live.ex:442
#: lib/bds/desktop/shell_live.ex:621 #: lib/bds/desktop/shell_live.ex:642
#: lib/bds/desktop/shell_live.ex:649 #: lib/bds/desktop/shell_live.ex:670
#: lib/bds/desktop/shell_live.ex:658 #: lib/bds/desktop/shell_live.ex:679
#: lib/bds/desktop/shell_live.ex:665 #: lib/bds/desktop/shell_live.ex:686
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:407 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:407
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Add Gallery Images" msgid "Add Gallery Images"
msgstr "Aggiungi immagini alla galleria" msgstr "Aggiungi immagini alla galleria"
#: lib/bds/desktop/shell_live.ex:666 #: lib/bds/desktop/shell_live.ex:687
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Failed to process %{path}: %{reason}" msgid "Failed to process %{path}: %{reason}"
msgstr "Impossibile elaborare %{path}: %{reason}" msgstr "Impossibile elaborare %{path}: %{reason}"
@@ -3230,12 +3228,12 @@ msgstr "Archivia"
msgid "Move this post to the archive" msgid "Move this post to the archive"
msgstr "Sposta questo articolo nell'archivio" msgstr "Sposta questo articolo nell'archivio"
#: lib/bds/desktop/shell_live/post_editor.ex:596 #: lib/bds/desktop/shell_live/post_editor.ex:601
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Post archived" msgid "Post archived"
msgstr "Articolo archiviato" msgstr "Articolo archiviato"
#: lib/bds/desktop/shell_live/post_editor.ex:629 #: lib/bds/desktop/shell_live/post_editor.ex:634
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Post unarchived" msgid "Post unarchived"
msgstr "Articolo ripristinato" msgstr "Articolo ripristinato"
@@ -3249,3 +3247,157 @@ msgstr "Ripristina questo articolo come bozza"
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Unarchive" msgid "Unarchive"
msgstr "Ripristina" msgstr "Ripristina"
#: lib/bds/desktop/shell_live/sidebar_components.ex:531
#, elixir-autogen, elixir-format
msgid "Ahead"
msgstr "In avanti"
#: lib/bds/desktop/shell_live/sidebar_components.ex:532
#, elixir-autogen, elixir-format
msgid "Behind"
msgstr "Indietro"
#: lib/bds/desktop/shell_live/sidebar_components.ex:550
#, elixir-autogen, elixir-format
msgid "Changes"
msgstr "Modifiche"
#: lib/bds/desktop/shell_live.ex:1055
#: lib/bds/desktop/shell_live.ex:1071
#: lib/bds/desktop/shell_live.ex:1077
#: lib/bds/desktop/shell_live/sidebar_components.ex:556
#, elixir-autogen, elixir-format
msgid "Commit"
msgstr "Commit"
#: lib/bds/desktop/shell_live/sidebar_components.ex:555
#, elixir-autogen, elixir-format
msgid "Commit message"
msgstr "Messaggio di commit"
#: lib/bds/desktop/shell_live.ex:1056
#, elixir-autogen, elixir-format
msgid "Commit message is required"
msgstr "Il messaggio di commit è obbligatorio"
#: lib/bds/desktop/shell_live.ex:1120
#, elixir-autogen, elixir-format
msgid "Done"
msgstr "Fatto"
#: lib/bds/desktop/shell_live.ex:1041
#: lib/bds/desktop/shell_live/sidebar_components.ex:542
#: lib/bds/desktop/shell_live/sidebar_components.ex:542
#, elixir-autogen, elixir-format
msgid "Fetch"
msgstr "Recupera"
#: lib/bds/desktop/shell_live/sidebar_components.ex:586
#, elixir-autogen, elixir-format
msgid "History"
msgstr "Cronologia"
#: lib/bds/desktop/shell_live.ex:1091
#: lib/bds/desktop/shell_live.ex:1099
#: lib/bds/desktop/shell_live/sidebar_components.ex:513
#, elixir-autogen, elixir-format
msgid "Initialize Git"
msgstr "Inizializza Git"
#: lib/bds/desktop/shell_live/sidebar_components.ex:536
#, elixir-autogen, elixir-format
msgid "Local only"
msgstr "Solo locale"
#: lib/bds/desktop/shell_live.ex:1127
#, elixir-autogen, elixir-format
msgid "No active project"
msgstr "Nessun progetto attivo"
#: lib/bds/desktop/shell_live/sidebar_components.ex:580
#, elixir-autogen, elixir-format
msgid "No changes"
msgstr "Nessuna modifica"
#: lib/bds/desktop/shell_live/sidebar_components.ex:616
#, elixir-autogen, elixir-format
msgid "No commits yet"
msgstr "Nessun commit ancora"
#: lib/bds/desktop/shell_live/sidebar_components.ex:613
#, elixir-autogen, elixir-format
msgid "Older history available"
msgstr "Cronologia precedente disponibile"
#: lib/bds/desktop/shell_live.ex:1044
#: lib/bds/desktop/shell_live/sidebar_components.ex:545
#: lib/bds/desktop/shell_live/sidebar_components.ex:545
#, elixir-autogen, elixir-format
msgid "Prune LFS"
msgstr "Pulisci LFS"
#: lib/bds/desktop/shell_live.ex:1042
#: lib/bds/desktop/shell_live/sidebar_components.ex:543
#: lib/bds/desktop/shell_live/sidebar_components.ex:543
#, elixir-autogen, elixir-format
msgid "Pull"
msgstr "Pull"
#: lib/bds/desktop/shell_live.ex:1043
#: lib/bds/desktop/shell_live/sidebar_components.ex:544
#: lib/bds/desktop/shell_live/sidebar_components.ex:544
#, elixir-autogen, elixir-format
msgid "Push"
msgstr "Push"
#: lib/bds/desktop/shell_live/sidebar_components.ex:509
#, elixir-autogen, elixir-format
msgid "Remote URL (optional)"
msgstr "URL remota (opzionale)"
#: lib/bds/desktop/shell_live/sidebar_components.ex:537
#, elixir-autogen, elixir-format
msgid "Remote only"
msgstr "Solo remoto"
#: lib/bds/desktop/shell_live.ex:1092
#, elixir-autogen, elixir-format
msgid "Repository initialized"
msgstr "Repository inizializzato"
#: lib/bds/desktop/shell_live/sidebar_components.ex:535
#, elixir-autogen, elixir-format
msgid "Synced"
msgstr "Sincronizzato"
#: lib/bds/desktop/shell_live/sidebar_components.ex:504
#, elixir-autogen, elixir-format
msgid "This project is not a Git repository yet."
msgstr "Questo progetto non è ancora un repository Git."
#: lib/bds/ui/sidebar.ex:869
#, elixir-autogen, elixir-format
msgid "added"
msgstr "aggiunto"
#: lib/bds/ui/sidebar.ex:870
#, elixir-autogen, elixir-format
msgid "deleted"
msgstr "eliminato"
#: lib/bds/ui/sidebar.ex:871
#: lib/bds/ui/sidebar.ex:874
#, elixir-autogen, elixir-format
msgid "modified"
msgstr "modificato"
#: lib/bds/ui/sidebar.ex:872
#, elixir-autogen, elixir-format
msgid "renamed"
msgstr "rinominato"
#: lib/bds/ui/sidebar.ex:873
#, elixir-autogen, elixir-format
msgid "untracked"
msgstr "non tracciato"

View File

@@ -18,7 +18,7 @@ msgstr ""
msgid "Archive" msgid "Archive"
msgstr "" msgstr ""
#: lib/bds/rendering/labels.ex:54 #: lib/bds/rendering/labels.ex:55
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "April" msgid "April"
msgstr "" msgstr ""
@@ -28,7 +28,7 @@ msgstr ""
msgid "Archive calendar" msgid "Archive calendar"
msgstr "" msgstr ""
#: lib/bds/rendering/labels.ex:70 #: lib/bds/rendering/labels.ex:71
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "August" msgid "August"
msgstr "" msgstr ""
@@ -48,27 +48,27 @@ msgstr ""
msgid "Close calendar" msgid "Close calendar"
msgstr "" msgstr ""
#: lib/bds/rendering/labels.ex:86 #: lib/bds/rendering/labels.ex:87
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "December" msgid "December"
msgstr "" msgstr ""
#: lib/bds/rendering/labels.ex:46 #: lib/bds/rendering/labels.ex:47
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "February" msgid "February"
msgstr "" msgstr ""
#: lib/bds/rendering/labels.ex:42 #: lib/bds/rendering/labels.ex:43
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "January" msgid "January"
msgstr "" msgstr ""
#: lib/bds/rendering/labels.ex:66 #: lib/bds/rendering/labels.ex:67
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "July" msgid "July"
msgstr "" msgstr ""
#: lib/bds/rendering/labels.ex:62 #: lib/bds/rendering/labels.ex:63
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "June" msgid "June"
msgstr "" msgstr ""
@@ -88,22 +88,22 @@ msgstr ""
msgid "Loading calendar…" msgid "Loading calendar…"
msgstr "" msgstr ""
#: lib/bds/rendering/labels.ex:50 #: lib/bds/rendering/labels.ex:51
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "March" msgid "March"
msgstr "" msgstr ""
#: lib/bds/rendering/labels.ex:58 #: lib/bds/rendering/labels.ex:59
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "May" msgid "May"
msgstr "" msgstr ""
#: lib/bds/rendering/labels.ex:82 #: lib/bds/rendering/labels.ex:83
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "November" msgid "November"
msgstr "" msgstr ""
#: lib/bds/rendering/labels.ex:78 #: lib/bds/rendering/labels.ex:79
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "October" msgid "October"
msgstr "" msgstr ""
@@ -123,7 +123,7 @@ msgstr ""
msgid "Search..." msgid "Search..."
msgstr "" msgstr ""
#: lib/bds/rendering/labels.ex:74 #: lib/bds/rendering/labels.ex:75
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "September" msgid "September"
msgstr "" msgstr ""
@@ -148,22 +148,27 @@ msgstr ""
msgid "older" msgid "older"
msgstr "" msgstr ""
#: lib/bds/rendering/labels.ex:31 #: lib/bds/rendering/labels.ex:32
#, 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:30 #: lib/bds/rendering/labels.ex:31
#, 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:33 #: lib/bds/rendering/labels.ex:34
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Vimeo video" msgid "Vimeo video"
msgstr "" msgstr ""
#: lib/bds/rendering/labels.ex:32 #: lib/bds/rendering/labels.ex:33
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "YouTube video" msgid "YouTube video"
msgstr "" msgstr ""
#: lib/bds/rendering/labels.ex:30
#, elixir-autogen, elixir-format
msgid "No results found"
msgstr ""

View File

@@ -92,7 +92,7 @@ msgstr ""
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:42 #: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:42
#: lib/bds/desktop/shell_live/overlay_manager.ex:72 #: lib/bds/desktop/shell_live/overlay_manager.ex:72
#: lib/bds/desktop/shell_live/post_editor.ex:776 #: lib/bds/desktop/shell_live/post_editor.ex:781
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:43 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:43
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "AI Suggestions" msgid "AI Suggestions"
@@ -270,14 +270,14 @@ msgid "Auto"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_data.ex:98 #: lib/bds/desktop/shell_data.ex:98
#: lib/bds/desktop/shell_live.ex:409 #: lib/bds/desktop/shell_live.ex:430
#: lib/bds/desktop/shell_live/chat_editor.ex:231 #: lib/bds/desktop/shell_live/chat_editor.ex:231
#: lib/bds/desktop/shell_live/media_editor.ex:156 #: lib/bds/desktop/shell_live/media_editor.ex:156
#: lib/bds/desktop/shell_live/media_editor.ex:349 #: lib/bds/desktop/shell_live/media_editor.ex:349
#: lib/bds/desktop/shell_live/media_editor.ex:538 #: lib/bds/desktop/shell_live/media_editor.ex:538
#: lib/bds/desktop/shell_live/overlay_manager.ex:73 #: lib/bds/desktop/shell_live/overlay_manager.ex:73
#: lib/bds/desktop/shell_live/post_editor.ex:643 #: lib/bds/desktop/shell_live/post_editor.ex:648
#: lib/bds/desktop/shell_live/post_editor.ex:692 #: lib/bds/desktop/shell_live/post_editor.ex:697
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Automatic AI actions stay gated by airplane mode." msgid "Automatic AI actions stay gated by airplane mode."
msgstr "" msgstr ""
@@ -417,7 +417,7 @@ msgstr ""
msgid "Category name is required" msgid "Category name is required"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live.ex:932 #: lib/bds/desktop/shell_live.ex:953
#: lib/bds/desktop/shell_live/chat_editor.ex:87 #: lib/bds/desktop/shell_live/chat_editor.ex:87
#: lib/bds/desktop/shell_live/chat_editor.ex:230 #: lib/bds/desktop/shell_live/chat_editor.ex:230
#: lib/bds/desktop/shell_live/chat_editor.ex:318 #: lib/bds/desktop/shell_live/chat_editor.ex:318
@@ -550,7 +550,7 @@ msgstr ""
msgid "Could not write MCP config %{path}: %{reason}" msgid "Could not write MCP config %{path}: %{reason}"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:42 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:48
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Create" msgid "Create"
msgstr "" msgstr ""
@@ -649,12 +649,12 @@ msgstr ""
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:166 #: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:166
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:116 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:116
#: lib/bds/desktop/shell_live/script_editor_html/script_editor.html.heex:16 #: lib/bds/desktop/shell_live/script_editor_html/script_editor.html.heex:16
#: lib/bds/desktop/shell_live/sidebar_components.ex:515 #: lib/bds/desktop/shell_live/sidebar_components.ex:651
#: lib/bds/desktop/shell_live/sidebar_components.ex:518 #: lib/bds/desktop/shell_live/sidebar_components.ex:654
#: lib/bds/desktop/shell_live/sidebar_components.ex:521 #: lib/bds/desktop/shell_live/sidebar_components.ex:657
#: lib/bds/desktop/shell_live/sidebar_components.ex:524 #: lib/bds/desktop/shell_live/sidebar_components.ex:660
#: lib/bds/desktop/shell_live/sidebar_components.ex:527 #: lib/bds/desktop/shell_live/sidebar_components.ex:663
#: lib/bds/desktop/shell_live/sidebar_components.ex:529 #: lib/bds/desktop/shell_live/sidebar_components.ex:665
#: lib/bds/desktop/shell_live/sidebar_delete.ex:72 #: lib/bds/desktop/shell_live/sidebar_delete.ex:72
#: lib/bds/desktop/shell_live/sidebar_delete.ex:174 #: lib/bds/desktop/shell_live/sidebar_delete.ex:174
#: lib/bds/desktop/shell_live/sidebar_delete.ex:175 #: lib/bds/desktop/shell_live/sidebar_delete.ex:175
@@ -662,7 +662,7 @@ msgstr ""
#: lib/bds/desktop/shell_live/sidebar_delete.ex:179 #: lib/bds/desktop/shell_live/sidebar_delete.ex:179
#: lib/bds/desktop/shell_live/sidebar_delete.ex:181 #: lib/bds/desktop/shell_live/sidebar_delete.ex:181
#: lib/bds/desktop/shell_live/sidebar_delete.ex:182 #: lib/bds/desktop/shell_live/sidebar_delete.ex:182
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:58 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:70
#: lib/bds/desktop/shell_live/template_editor_html/template_editor.html.heex:15 #: lib/bds/desktop/shell_live/template_editor_html/template_editor.html.heex:15
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Delete" msgid "Delete"
@@ -678,7 +678,7 @@ msgstr ""
msgid "Delete Translation" msgid "Delete Translation"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:514 #: lib/bds/desktop/shell_live/sidebar_components.ex:650
#: lib/bds/desktop/shell_live/sidebar_delete.ex:173 #: lib/bds/desktop/shell_live/sidebar_delete.ex:173
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Delete conversation" msgid "Delete conversation"
@@ -719,9 +719,9 @@ msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:199 #: lib/bds/desktop/shell_live/media_editor.ex:199
#: lib/bds/desktop/shell_live/media_editor.ex:205 #: lib/bds/desktop/shell_live/media_editor.ex:205
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:59 #: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:59
#: lib/bds/desktop/shell_live/post_editor.ex:642 #: lib/bds/desktop/shell_live/post_editor.ex:647
#: lib/bds/desktop/shell_live/post_editor.ex:671 #: lib/bds/desktop/shell_live/post_editor.ex:676
#: lib/bds/desktop/shell_live/post_editor.ex:677 #: lib/bds/desktop/shell_live/post_editor.ex:682
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Detect Language" msgid "Detect Language"
msgstr "" msgstr ""
@@ -762,7 +762,7 @@ msgid "Discard changes and restore the published version"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:21 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:21
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:84 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:96
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Discover" msgid "Discover"
msgstr "" msgstr ""
@@ -1004,8 +1004,8 @@ msgstr ""
msgid "Generate Site" msgid "Generate Site"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live.ex:933 #: lib/bds/desktop/shell_live.ex:954
#: lib/bds/ui/sidebar.ex:784 #: lib/bds/ui/sidebar.ex:786
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Git" msgid "Git"
msgstr "" msgstr ""
@@ -1017,7 +1017,7 @@ msgid "Git Diff"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_data.ex:244 #: lib/bds/desktop/shell_data.ex:244
#: lib/bds/desktop/shell_live.ex:929 #: lib/bds/desktop/shell_live.ex:950
#: lib/bds/desktop/shell_live/panel_renderer.ex:171 #: lib/bds/desktop/shell_live/panel_renderer.ex:171
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Git Log" msgid "Git Log"
@@ -1046,7 +1046,7 @@ msgstr ""
#: lib/bds/desktop/shell_data.ex:116 #: lib/bds/desktop/shell_data.ex:116
#: lib/bds/desktop/shell_live/index.html.heex:666 #: lib/bds/desktop/shell_live/index.html.heex:666
#: lib/bds/desktop/shell_live/media_editor.ex:703 #: lib/bds/desktop/shell_live/media_editor.ex:703
#: lib/bds/desktop/shell_live/post_editor.ex:894 #: lib/bds/desktop/shell_live/post_editor.ex:899
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Idle" msgid "Idle"
msgstr "" msgstr ""
@@ -1088,7 +1088,7 @@ msgstr ""
#: lib/bds/desktop/shell_live/import_editor/taxonomy_editing.ex:127 #: lib/bds/desktop/shell_live/import_editor/taxonomy_editing.ex:127
#: lib/bds/desktop/shell_live/import_editor/taxonomy_editing.ex:137 #: lib/bds/desktop/shell_live/import_editor/taxonomy_editing.ex:137
#: lib/bds/desktop/shell_live/misc_editor_html/misc_editor.html.heex:182 #: lib/bds/desktop/shell_live/misc_editor_html/misc_editor.html.heex:182
#: lib/bds/desktop/shell_live/sidebar_components.ex:527 #: lib/bds/desktop/shell_live/sidebar_components.ex:663
#: lib/bds/desktop/shell_live/sidebar_delete.ex:181 #: lib/bds/desktop/shell_live/sidebar_delete.ex:181
#: lib/bds/ui/registry.ex:70 #: lib/bds/ui/registry.ex:70
#: lib/bds/ui/registry.ex:105 #: lib/bds/ui/registry.ex:105
@@ -1140,9 +1140,9 @@ msgstr ""
msgid "Import failed: %{error}" msgid "Import failed: %{error}"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live.ex:580 #: lib/bds/desktop/shell_live.ex:601
#: lib/bds/desktop/shell_live.ex:970 #: lib/bds/desktop/shell_live.ex:991
#: lib/bds/desktop/shell_live.ex:976 #: lib/bds/desktop/shell_live.ex:997
#: lib/bds/desktop/shell_live/sidebar_create.ex:47 #: lib/bds/desktop/shell_live/sidebar_create.ex:47
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Import media" msgid "Import media"
@@ -1217,7 +1217,7 @@ msgid "Language"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:206 #: lib/bds/desktop/shell_live/media_editor.ex:206
#: lib/bds/desktop/shell_live/post_editor.ex:678 #: lib/bds/desktop/shell_live/post_editor.ex:683
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Language detection failed." msgid "Language detection failed."
msgstr "" msgstr ""
@@ -1301,7 +1301,7 @@ msgstr ""
msgid "Mapped" msgid "Mapped"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:897 #: lib/bds/desktop/shell_live/post_editor.ex:902
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:120 #: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:120
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Markdown" msgid "Markdown"
@@ -1317,7 +1317,7 @@ msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:494 #: lib/bds/desktop/shell_live/media_editor.ex:494
#: lib/bds/desktop/shell_live/misc_editor.ex:744 #: lib/bds/desktop/shell_live/misc_editor.ex:744
#: lib/bds/desktop/shell_live/misc_editor.ex:771 #: lib/bds/desktop/shell_live/misc_editor.ex:771
#: lib/bds/desktop/shell_live/sidebar_components.ex:518 #: lib/bds/desktop/shell_live/sidebar_components.ex:654
#: lib/bds/desktop/shell_live/sidebar_delete.ex:175 #: lib/bds/desktop/shell_live/sidebar_delete.ex:175
#: lib/bds/ui/registry.ex:30 #: lib/bds/ui/registry.ex:30
#: lib/bds/ui/registry.ex:100 #: lib/bds/ui/registry.ex:100
@@ -1341,12 +1341,12 @@ msgstr ""
msgid "Menu" msgid "Menu"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:75 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:87
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Merge" msgid "Merge"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:66 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:78
#: lib/bds/ui/sidebar.ex:747 #: lib/bds/ui/sidebar.ex:747
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Merge Tags" msgid "Merge Tags"
@@ -1437,7 +1437,7 @@ msgstr ""
msgid "New Template" msgid "New Template"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:52 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:64
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "No Template" msgid "No Template"
msgstr "" msgstr ""
@@ -1466,12 +1466,12 @@ msgstr ""
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:72 #: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:72
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:308 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:308
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:320 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:320
#: lib/bds/desktop/shell_live/sidebar_components.ex:320 #: lib/bds/desktop/shell_live/sidebar_components.ex:321
#: lib/bds/desktop/shell_live/sidebar_components.ex:380 #: lib/bds/desktop/shell_live/sidebar_components.ex:381
#: lib/bds/desktop/shell_live/sidebar_components.ex:454 #: lib/bds/desktop/shell_live/sidebar_components.ex:455
#: lib/bds/ui/sidebar.ex:198 #: lib/bds/ui/sidebar.ex:198
#: lib/bds/ui/sidebar.ex:787 #: lib/bds/ui/sidebar.ex:789
#: lib/bds/ui/sidebar.ex:805 #: lib/bds/ui/sidebar.ex:896
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "No items" msgid "No items"
msgstr "" msgstr ""
@@ -1734,7 +1734,7 @@ msgstr ""
msgid "Other (%{count})" msgid "Other (%{count})"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live.ex:928 #: lib/bds/desktop/shell_live.ex:949
#: lib/bds/desktop/shell_live/panel_renderer.ex:83 #: lib/bds/desktop/shell_live/panel_renderer.ex:83
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Output" msgid "Output"
@@ -1799,17 +1799,17 @@ msgid "Persist the detected language for this media item"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:742 #: lib/bds/desktop/shell_live/misc_editor.ex:742
#: lib/bds/desktop/shell_live/post_editor.ex:474 #: lib/bds/desktop/shell_live/post_editor.ex:479
#: lib/bds/desktop/shell_live/post_editor.ex:478 #: lib/bds/desktop/shell_live/post_editor.ex:483
#: lib/bds/desktop/shell_live/post_editor.ex:513 #: lib/bds/desktop/shell_live/post_editor.ex:518
#: lib/bds/desktop/shell_live/post_editor.ex:517 #: lib/bds/desktop/shell_live/post_editor.ex:522
#: lib/bds/desktop/shell_live/post_editor.ex:552 #: lib/bds/desktop/shell_live/post_editor.ex:557
#: lib/bds/desktop/shell_live/post_editor.ex:567 #: lib/bds/desktop/shell_live/post_editor.ex:572
#: lib/bds/desktop/shell_live/post_editor.ex:596 #: lib/bds/desktop/shell_live/post_editor.ex:601
#: lib/bds/desktop/shell_live/post_editor.ex:599 #: lib/bds/desktop/shell_live/post_editor.ex:604
#: lib/bds/desktop/shell_live/post_editor.ex:629 #: lib/bds/desktop/shell_live/post_editor.ex:634
#: lib/bds/desktop/shell_live/post_editor.ex:632 #: lib/bds/desktop/shell_live/post_editor.ex:637
#: lib/bds/desktop/shell_live/sidebar_components.ex:515 #: lib/bds/desktop/shell_live/sidebar_components.ex:651
#: lib/bds/desktop/shell_live/sidebar_delete.ex:174 #: lib/bds/desktop/shell_live/sidebar_delete.ex:174
#: lib/bds/ui/registry.ex:99 #: lib/bds/ui/registry.ex:99
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@@ -1838,12 +1838,12 @@ msgstr ""
msgid "Post is marked as do-not-translate but has translations" msgid "Post is marked as do-not-translate but has translations"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:513 #: lib/bds/desktop/shell_live/post_editor.ex:518
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Post published" msgid "Post published"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:474 #: lib/bds/desktop/shell_live/post_editor.ex:479
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Post saved" msgid "Post saved"
msgstr "" msgstr ""
@@ -1867,7 +1867,7 @@ msgstr ""
msgid "Preferences" msgid "Preferences"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:898 #: lib/bds/desktop/shell_live/post_editor.ex:903
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:121 #: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:121
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Preview" msgid "Preview"
@@ -1936,7 +1936,7 @@ msgid "Publish Selected"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_data.ex:181 #: lib/bds/desktop/shell_data.ex:181
#: lib/bds/desktop/shell_live/post_editor.ex:892 #: lib/bds/desktop/shell_live/post_editor.ex:897
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:456 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:456
#: lib/bds/ui/sidebar.ex:320 #: lib/bds/ui/sidebar.ex:320
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@@ -2134,7 +2134,7 @@ msgstr ""
msgid "Result" msgid "Result"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:893 #: lib/bds/desktop/shell_live/post_editor.ex:898
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Reverted" msgid "Reverted"
msgstr "" msgstr ""
@@ -2174,7 +2174,7 @@ msgstr ""
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:312 #: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:312
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:329 #: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:329
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:342 #: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:342
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:57 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:69
#: lib/bds/desktop/shell_live/template_editor_html/template_editor.html.heex:13 #: lib/bds/desktop/shell_live/template_editor_html/template_editor.html.heex:13
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Save" msgid "Save"
@@ -2186,7 +2186,7 @@ msgid "Save Translation"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:702 #: lib/bds/desktop/shell_live/media_editor.ex:702
#: lib/bds/desktop/shell_live/post_editor.ex:891 #: lib/bds/desktop/shell_live/post_editor.ex:896
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Saved" msgid "Saved"
msgstr "" msgstr ""
@@ -2197,7 +2197,7 @@ msgid "Scanning entries..."
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:746 #: lib/bds/desktop/shell_live/misc_editor.ex:746
#: lib/bds/desktop/shell_live/sidebar_components.ex:521 #: lib/bds/desktop/shell_live/sidebar_components.ex:657
#: lib/bds/desktop/shell_live/sidebar_delete.ex:176 #: lib/bds/desktop/shell_live/sidebar_delete.ex:176
#: lib/bds/ui/registry.ex:133 #: lib/bds/ui/registry.ex:133
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@@ -2444,7 +2444,7 @@ msgstr ""
msgid "Switch project" msgid "Switch project"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:82 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:94
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Sync" msgid "Sync"
msgstr "" msgstr ""
@@ -2481,11 +2481,11 @@ msgstr ""
#: lib/bds/desktop/shell_live/index.html.heex:325 #: lib/bds/desktop/shell_live/index.html.heex:325
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:161 #: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:161
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:158 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:158
#: lib/bds/desktop/shell_live/tags_editor.ex:94 #: lib/bds/desktop/shell_live/tags_editor.ex:104
#: lib/bds/desktop/shell_live/tags_editor.ex:136 #: lib/bds/desktop/shell_live/tags_editor.ex:166
#: lib/bds/desktop/shell_live/tags_editor.ex:189 #: lib/bds/desktop/shell_live/tags_editor.ex:219
#: lib/bds/desktop/shell_live/tags_editor.ex:203 #: lib/bds/desktop/shell_live/tags_editor.ex:233
#: lib/bds/desktop/shell_live/tags_editor.ex:234 #: lib/bds/desktop/shell_live/tags_editor.ex:264
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:11 #: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:11
#: lib/bds/ui/registry.ex:54 #: lib/bds/ui/registry.ex:54
#: lib/bds/ui/registry.ex:103 #: lib/bds/ui/registry.ex:103
@@ -2496,7 +2496,7 @@ msgstr ""
msgid "Tags" msgid "Tags"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live.ex:927 #: lib/bds/desktop/shell_live.ex:948
#: lib/bds/desktop/shell_live/panel_renderer.ex:54 #: lib/bds/desktop/shell_live/panel_renderer.ex:54
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Tasks" msgid "Tasks"
@@ -2510,7 +2510,7 @@ msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:747 #: lib/bds/desktop/shell_live/misc_editor.ex:747
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:286 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:286
#: lib/bds/desktop/shell_live/sidebar_components.ex:524 #: lib/bds/desktop/shell_live/sidebar_components.ex:660
#: lib/bds/desktop/shell_live/sidebar_delete.ex:179 #: lib/bds/desktop/shell_live/sidebar_delete.ex:179
#: lib/bds/ui/registry.ex:134 #: lib/bds/ui/registry.ex:134
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@@ -2661,9 +2661,9 @@ msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:558 #: lib/bds/desktop/shell_live/media_editor.ex:558
#: lib/bds/desktop/shell_live/media_editor.ex:563 #: lib/bds/desktop/shell_live/media_editor.ex:563
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:76 #: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:76
#: lib/bds/desktop/shell_live/post_editor.ex:691 #: lib/bds/desktop/shell_live/post_editor.ex:696
#: lib/bds/desktop/shell_live/post_editor.ex:720
#: lib/bds/desktop/shell_live/post_editor.ex:725 #: lib/bds/desktop/shell_live/post_editor.ex:725
#: lib/bds/desktop/shell_live/post_editor.ex:730
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:60 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:60
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Translate" msgid "Translate"
@@ -2740,7 +2740,7 @@ msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:701 #: lib/bds/desktop/shell_live/media_editor.ex:701
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:10 #: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:10
#: lib/bds/desktop/shell_live/post_editor.ex:890 #: lib/bds/desktop/shell_live/post_editor.ex:895
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:7 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:7
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Unsaved" msgid "Unsaved"
@@ -2748,7 +2748,7 @@ msgstr ""
#: lib/bds/desktop/shell_live/import_editor.ex:867 #: lib/bds/desktop/shell_live/import_editor.ex:867
#: lib/bds/desktop/shell_live/post_editor/post_metadata.ex:166 #: lib/bds/desktop/shell_live/post_editor/post_metadata.ex:166
#: lib/bds/ui/sidebar.ex:1014 #: lib/bds/ui/sidebar.ex:1105
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Untitled" msgid "Untitled"
msgstr "" msgstr ""
@@ -2842,14 +2842,12 @@ msgid "WYSIWYG"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/tab_helpers.ex:191 #: lib/bds/desktop/shell_live/tab_helpers.ex:191
#: lib/bds/ui/sidebar.ex:791
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Working tree" msgid "Working tree"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/tab_helpers.ex:191 #: lib/bds/desktop/shell_live/tab_helpers.ex:191
#: lib/bds/ui/sidebar.ex:785 #: lib/bds/ui/sidebar.ex:787
#: lib/bds/ui/sidebar.ex:792
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Working tree and history" msgid "Working tree and history"
msgstr "" msgstr ""
@@ -3192,12 +3190,12 @@ msgstr ""
msgid "Comparing database and filesystem metadata" msgid "Comparing database and filesystem metadata"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live.ex:650 #: lib/bds/desktop/shell_live.ex:671
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Added %{count} images to post" msgid "Added %{count} images to post"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live.ex:621 #: lib/bds/desktop/shell_live.ex:642
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Added %{title}" msgid "Added %{title}"
msgstr "" msgstr ""
@@ -3217,18 +3215,18 @@ msgstr ""
msgid "Image Import Concurrency" msgid "Image Import Concurrency"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live.ex:408 #: lib/bds/desktop/shell_live.ex:429
#: lib/bds/desktop/shell_live.ex:421 #: lib/bds/desktop/shell_live.ex:442
#: lib/bds/desktop/shell_live.ex:621 #: lib/bds/desktop/shell_live.ex:642
#: lib/bds/desktop/shell_live.ex:649 #: lib/bds/desktop/shell_live.ex:670
#: lib/bds/desktop/shell_live.ex:658 #: lib/bds/desktop/shell_live.ex:679
#: lib/bds/desktop/shell_live.ex:665 #: lib/bds/desktop/shell_live.ex:686
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:407 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:407
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Add Gallery Images" msgid "Add Gallery Images"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live.ex:666 #: lib/bds/desktop/shell_live.ex:687
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Failed to process %{path}: %{reason}" msgid "Failed to process %{path}: %{reason}"
msgstr "" msgstr ""
@@ -3243,12 +3241,12 @@ msgstr ""
msgid "Move this post to the archive" msgid "Move this post to the archive"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:596 #: lib/bds/desktop/shell_live/post_editor.ex:601
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Post archived" msgid "Post archived"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:629 #: lib/bds/desktop/shell_live/post_editor.ex:634
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Post unarchived" msgid "Post unarchived"
msgstr "" msgstr ""
@@ -3262,3 +3260,157 @@ msgstr ""
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Unarchive" msgid "Unarchive"
msgstr "" msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:531
#, elixir-autogen, elixir-format
msgid "Ahead"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:532
#, elixir-autogen, elixir-format
msgid "Behind"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:550
#, elixir-autogen, elixir-format
msgid "Changes"
msgstr ""
#: lib/bds/desktop/shell_live.ex:1055
#: lib/bds/desktop/shell_live.ex:1071
#: lib/bds/desktop/shell_live.ex:1077
#: lib/bds/desktop/shell_live/sidebar_components.ex:556
#, elixir-autogen, elixir-format
msgid "Commit"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:555
#, elixir-autogen, elixir-format
msgid "Commit message"
msgstr ""
#: lib/bds/desktop/shell_live.ex:1056
#, elixir-autogen, elixir-format
msgid "Commit message is required"
msgstr ""
#: lib/bds/desktop/shell_live.ex:1120
#, elixir-autogen, elixir-format
msgid "Done"
msgstr ""
#: lib/bds/desktop/shell_live.ex:1041
#: lib/bds/desktop/shell_live/sidebar_components.ex:542
#: lib/bds/desktop/shell_live/sidebar_components.ex:542
#, elixir-autogen, elixir-format
msgid "Fetch"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:586
#, elixir-autogen, elixir-format
msgid "History"
msgstr ""
#: lib/bds/desktop/shell_live.ex:1091
#: lib/bds/desktop/shell_live.ex:1099
#: lib/bds/desktop/shell_live/sidebar_components.ex:513
#, elixir-autogen, elixir-format
msgid "Initialize Git"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:536
#, elixir-autogen, elixir-format
msgid "Local only"
msgstr ""
#: lib/bds/desktop/shell_live.ex:1127
#, elixir-autogen, elixir-format
msgid "No active project"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:580
#, elixir-autogen, elixir-format
msgid "No changes"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:616
#, elixir-autogen, elixir-format
msgid "No commits yet"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:613
#, elixir-autogen, elixir-format
msgid "Older history available"
msgstr ""
#: lib/bds/desktop/shell_live.ex:1044
#: lib/bds/desktop/shell_live/sidebar_components.ex:545
#: lib/bds/desktop/shell_live/sidebar_components.ex:545
#, elixir-autogen, elixir-format
msgid "Prune LFS"
msgstr ""
#: lib/bds/desktop/shell_live.ex:1042
#: lib/bds/desktop/shell_live/sidebar_components.ex:543
#: lib/bds/desktop/shell_live/sidebar_components.ex:543
#, elixir-autogen, elixir-format
msgid "Pull"
msgstr ""
#: lib/bds/desktop/shell_live.ex:1043
#: lib/bds/desktop/shell_live/sidebar_components.ex:544
#: lib/bds/desktop/shell_live/sidebar_components.ex:544
#, elixir-autogen, elixir-format
msgid "Push"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:509
#, elixir-autogen, elixir-format
msgid "Remote URL (optional)"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:537
#, elixir-autogen, elixir-format
msgid "Remote only"
msgstr ""
#: lib/bds/desktop/shell_live.ex:1092
#, elixir-autogen, elixir-format
msgid "Repository initialized"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:535
#, elixir-autogen, elixir-format
msgid "Synced"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:504
#, elixir-autogen, elixir-format
msgid "This project is not a Git repository yet."
msgstr ""
#: lib/bds/ui/sidebar.ex:869
#, elixir-autogen, elixir-format
msgid "added"
msgstr ""
#: lib/bds/ui/sidebar.ex:870
#, elixir-autogen, elixir-format
msgid "deleted"
msgstr ""
#: lib/bds/ui/sidebar.ex:871
#: lib/bds/ui/sidebar.ex:874
#, elixir-autogen, elixir-format
msgid "modified"
msgstr ""
#: lib/bds/ui/sidebar.ex:872
#, elixir-autogen, elixir-format
msgid "renamed"
msgstr ""
#: lib/bds/ui/sidebar.ex:873
#, elixir-autogen, elixir-format
msgid "untracked"
msgstr ""

View File

@@ -0,0 +1,65 @@
/* Styling for the self-contained PagefindUI search widget. */
.pagefind-ui {
display: block;
font-size: 0.95rem;
}
.pagefind-ui__form {
margin: 0 0 0.75rem 0;
}
.pagefind-ui__search-input {
width: 100%;
padding: 0.5rem 0.75rem;
border: 1px solid var(--pico-form-element-border-color, #ccc);
border-radius: 0.375rem;
font: inherit;
box-sizing: border-box;
}
.pagefind-ui__results {
display: block;
}
.pagefind-ui__message {
margin: 0.5rem 0;
opacity: 0.7;
}
.pagefind-ui__result-list {
list-style: none;
margin: 0;
padding: 0;
}
.pagefind-ui__result {
padding: 0.5rem 0;
border-bottom: 1px solid var(--pico-muted-border-color, #eee);
}
.pagefind-ui__result:last-child {
border-bottom: none;
}
.pagefind-ui__result-link {
display: block;
font-weight: 600;
text-decoration: none;
}
.pagefind-ui__result-link:hover {
text-decoration: underline;
}
.pagefind-ui__result-excerpt {
margin: 0.25rem 0 0 0;
opacity: 0.8;
line-height: 1.4;
}
.pagefind-ui__result-excerpt mark {
background: var(--pico-mark-background-color, #ffe08a);
color: inherit;
padding: 0 0.1em;
border-radius: 0.15em;
}

View File

@@ -0,0 +1,272 @@
/*
* Self-contained client-side search UI for generated blog output.
*
* Exposes a global `PagefindUI` constructor that the bundled
* `search-runtime.js` instantiates. It fetches the per-language fragment
* index (`index.json`) co-located with this script, performs full-text
* matching over the indexed post fragments, and renders ranked results.
*
* No external/CDN dependencies — everything needed ships in this file.
*/
(function () {
"use strict";
// Resolve the sibling index.json relative to this script's own URL so the
// same bundle works for every language directory (e.g. /pagefind/,
// /de/pagefind/) without baking the path in at generation time.
var scriptSrc = (document.currentScript && document.currentScript.src) || "";
function resolveIndexUrl(src) {
try {
return new URL("index.json", src).href;
} catch (err) {
return "index.json";
}
}
function tokenize(value) {
return (value || "")
.toLowerCase()
.split(/[^\p{L}\p{N}]+/u)
.filter(function (token) {
return token.length > 0;
});
}
function PagefindUI(options) {
options = options || {};
this.element =
typeof options.element === "string"
? document.querySelector(options.element)
: options.element;
if (!this.element) {
return;
}
var translations = options.translations || {};
this.placeholder = translations.placeholder || "Search";
this.zeroResults = translations.zero_results || "No results found";
this.indexUrl = options.indexUrl || resolveIndexUrl(scriptSrc);
this.pages = null;
this.loadPromise = null;
this.render();
}
PagefindUI.prototype.render = function () {
var self = this;
this.element.classList.add("pagefind-ui");
this.element.innerHTML = "";
var form = document.createElement("form");
form.className = "pagefind-ui__form";
form.setAttribute("role", "search");
form.addEventListener("submit", function (event) {
event.preventDefault();
});
var input = document.createElement("input");
input.type = "search";
input.className = "pagefind-ui__search-input";
input.setAttribute("autocomplete", "off");
input.placeholder = this.placeholder;
input.setAttribute("aria-label", this.placeholder);
var results = document.createElement("div");
results.className = "pagefind-ui__results";
form.appendChild(input);
this.element.appendChild(form);
this.element.appendChild(results);
this.input = input;
this.results = results;
var debounce = null;
input.addEventListener("input", function () {
window.clearTimeout(debounce);
debounce = window.setTimeout(function () {
self.search(input.value);
}, 120);
});
};
PagefindUI.prototype.load = function () {
if (this.pages) {
return Promise.resolve(this.pages);
}
if (this.loadPromise) {
return this.loadPromise;
}
var self = this;
this.loadPromise = fetch(this.indexUrl, { credentials: "same-origin" })
.then(function (response) {
if (!response.ok) {
throw new Error("pagefind index request failed: " + response.status);
}
return response.json();
})
.then(function (data) {
self.pages = (data && data.pages) || [];
return self.pages;
})
.catch(function () {
self.pages = [];
return self.pages;
});
return this.loadPromise;
};
PagefindUI.prototype.search = function (query) {
var self = this;
var terms = tokenize(query);
if (terms.length === 0) {
this.results.innerHTML = "";
return;
}
this.load().then(function (pages) {
var matches = [];
for (var i = 0; i < pages.length; i++) {
var page = pages[i];
var title = (page.title || "").toLowerCase();
var body = (page.text || "").toLowerCase();
var score = 0;
var matchedAll = true;
for (var t = 0; t < terms.length; t++) {
var term = terms[t];
var bodyHits = body.split(term).length - 1;
var titleHits = title.split(term).length - 1;
if (bodyHits === 0 && titleHits === 0) {
matchedAll = false;
break;
}
// Title matches are weighted more heavily than body matches.
score += bodyHits + titleHits * 5;
}
if (matchedAll) {
matches.push({ page: page, score: score });
}
}
matches.sort(function (a, b) {
return b.score - a.score;
});
self.renderResults(matches.slice(0, 10), terms);
});
};
PagefindUI.prototype.renderResults = function (matches, terms) {
this.results.innerHTML = "";
if (matches.length === 0) {
var message = document.createElement("p");
message.className = "pagefind-ui__message";
message.textContent = this.zeroResults;
this.results.appendChild(message);
return;
}
var list = document.createElement("ol");
list.className = "pagefind-ui__result-list";
for (var i = 0; i < matches.length; i++) {
var page = matches[i].page;
var item = document.createElement("li");
item.className = "pagefind-ui__result";
var link = document.createElement("a");
link.className = "pagefind-ui__result-link";
link.href = page.url;
link.textContent = page.title || page.url;
var excerpt = document.createElement("p");
excerpt.className = "pagefind-ui__result-excerpt";
buildExcerpt(excerpt, page.text || "", terms);
item.appendChild(link);
item.appendChild(excerpt);
list.appendChild(item);
}
this.results.appendChild(list);
};
// Build an excerpt around the first matching term and highlight all terms
// with <mark>, using safe text nodes (never innerHTML on untrusted text).
function buildExcerpt(target, text, terms) {
var lower = text.toLowerCase();
var firstHit = -1;
for (var t = 0; t < terms.length; t++) {
var idx = lower.indexOf(terms[t]);
if (idx !== -1 && (firstHit === -1 || idx < firstHit)) {
firstHit = idx;
}
}
var start = firstHit === -1 ? 0 : Math.max(0, firstHit - 60);
var snippet = text.slice(start, start + 220);
if (start > 0) {
snippet = "…" + snippet;
}
if (start + 220 < text.length) {
snippet = snippet + "…";
}
highlightInto(target, snippet, terms);
}
function highlightInto(target, snippet, terms) {
var escaped = terms
.map(function (term) {
return term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
})
.filter(function (term) {
return term.length > 0;
});
if (escaped.length === 0) {
target.appendChild(document.createTextNode(snippet));
return;
}
var pattern = new RegExp("(" + escaped.join("|") + ")", "giu");
var lastIndex = 0;
var match;
while ((match = pattern.exec(snippet)) !== null) {
if (match.index > lastIndex) {
target.appendChild(
document.createTextNode(snippet.slice(lastIndex, match.index))
);
}
var mark = document.createElement("mark");
mark.textContent = match[0];
target.appendChild(mark);
lastIndex = match.index + match[0].length;
// Guard against zero-length matches looping forever.
if (match.index === pattern.lastIndex) {
pattern.lastIndex++;
}
}
if (lastIndex < snippet.length) {
target.appendChild(document.createTextNode(snippet.slice(lastIndex)));
}
}
window.PagefindUI = PagefindUI;
})();

View File

@@ -15,11 +15,12 @@
} }
initialized = true; initialized = true;
var placeholder = root.getAttribute('data-search-placeholder') || 'Search...'; var placeholder = root.getAttribute('data-search-placeholder') || 'Search...';
var zeroResults = root.getAttribute('data-search-no-results') || 'No results found';
new PagefindUI({ new PagefindUI({
element: root, element: root,
showSubResults: true, showSubResults: true,
showImages: false, showImages: false,
translations: { placeholder: placeholder } translations: { placeholder: placeholder, zero_results: zeroResults }
}); });
var input = root.querySelector('input'); var input = root.querySelector('input');
if (input) { if (input) {

View File

@@ -0,0 +1,33 @@
defmodule BDS.Repo.Migrations.ConvertEmbeddingVectorToBlob do
use Ecto.Migration
# Embedding vectors are now persisted as a packed little-endian Float32 BLOB
# (VectorCacheInDb invariant) instead of JSON text. The table is a rebuildable
# cache and the previous lexical vectors are incompatible with the neural
# model, so we drop and recreate it; rows are re-embedded on next index.
def up do
drop table(:embedding_keys)
create_embedding_keys(:binary)
end
def down do
drop table(:embedding_keys)
create_embedding_keys(:text)
end
defp create_embedding_keys(vector_type) do
create table(:embedding_keys, primary_key: false) do
add :label, :integer, primary_key: true
add :post_id, references(:posts, column: :id, type: :string, on_delete: :delete_all),
null: false
add :project_id, references(:projects, type: :string, on_delete: :delete_all), null: false
add :content_hash, :string, null: false
add :vector, vector_type
end
create index(:embedding_keys, [:post_id])
create index(:embedding_keys, [:project_id])
end
end

View File

@@ -15,7 +15,7 @@
</svg> </svg>
</button> </button>
<div class="blog-search-panel" data-blog-search-panel hidden> <div class="blog-search-panel" data-blog-search-panel hidden>
<div id="blog-search" data-blog-search-root data-search-placeholder="{{ labels.search_placeholder }}"></div> <div id="blog-search" data-blog-search-root data-search-placeholder="{{ labels.search_placeholder }}" data-search-no-results="{{ labels.search_no_results }}"></div>
</div> </div>
</div> </div>
</nav> </nav>
@@ -35,7 +35,7 @@
</svg> </svg>
</button> </button>
<div class="blog-search-panel" data-blog-search-panel hidden> <div class="blog-search-panel" data-blog-search-panel hidden>
<div id="blog-search" data-blog-search-root data-search-placeholder="{{ labels.search_placeholder }}"></div> <div id="blog-search" data-blog-search-root data-search-placeholder="{{ labels.search_placeholder }}" data-search-no-results="{{ labels.search_no_results }}"></div>
</div> </div>
</div> </div>
{% endif %} {% endif %}

View File

@@ -48,6 +48,9 @@ value EmbeddingModel {
-- Lazy-loaded: pipeline created on first embedding request, not at startup -- Lazy-loaded: pipeline created on first embedding request, not at startup
-- Text preprocessing: prefix all input with "query: " (e5 convention) -- Text preprocessing: prefix all input with "query: " (e5 convention)
-- Pooling: mean pooling + L2 normalization -- Pooling: mean pooling + L2 normalization
-- Loaded on-device via Bumblebee+EXLA; the canonical e5 weights come from
-- the "intfloat/multilingual-e5-small" repository, surfaced under the
-- "Xenova/multilingual-e5-small" model_id identifier.
model_id: String -- "Xenova/multilingual-e5-small" model_id: String -- "Xenova/multilingual-e5-small"
dimensions: Integer -- 384 dimensions: Integer -- 384
} }
@@ -60,7 +63,7 @@ value EmbeddingVector {
-- ─── Entities ─────────────────────────────────────────────── -- ─── Entities ───────────────────────────────────────────────
entity EmbeddingKey { entity EmbeddingKey {
label: Integer -- HNSW label for USearch label: Integer -- HNSW node label / id
post: post/Post post: post/Post
content_hash: String -- SHA-256 of "{title}\n\n{content}" content_hash: String -- SHA-256 of "{title}\n\n{content}"
vector: EmbeddingVector vector: EmbeddingVector
@@ -72,9 +75,11 @@ entity DismissedDuplicatePair {
-- IDs stored in canonical order (sorted) for dedup -- IDs stored in canonical order (sorted) for dedup
} }
-- ─── USearch HNSW Index ───────────────────────────────────── -- ─── HNSW Index ─────────────────────────────────────────────
config { config {
-- HNSW approximate-nearest-neighbour index (hnswlib). USearch has no Elixir
-- binding; hnswlib provides the same HNSW algorithm and parameters.
model_id: String = "Xenova/multilingual-e5-small" model_id: String = "Xenova/multilingual-e5-small"
embedding_dimensions: Integer = 384 embedding_dimensions: Integer = 384
hnsw_metric: String = "cosine" hnsw_metric: String = "cosine"
@@ -83,7 +88,10 @@ config {
hnsw_expansion_search: Integer = 64 -- efSearch hnsw_expansion_search: Integer = 64 -- efSearch
debounce_persist: Duration = 5.seconds debounce_persist: Duration = 5.seconds
-- Index file: {userData}/projects/{projectId}/embeddings.usearch -- Index file: {userData}/projects/{projectId}/embeddings.usearch
-- Key mapping is persisted alongside the embedding records -- Key mapping (label → post_id) persisted in a sidecar (.meta.json) next
-- to the index file, plus the source-of-truth rows in embedding_keys
batch_size: Integer = 16 -- texts per batched inference run
sequence_length: Integer = 256 -- max tokens per input (truncated)
} }
-- ─── Gating ───────────────────────────────────────────────── -- ─── Gating ─────────────────────────────────────────────────
@@ -107,7 +115,7 @@ rule EmbedPost {
let existing = EmbeddingKey{post: post} let existing = EmbeddingKey{post: post}
if not exists existing or existing.content_hash != hash: if not exists existing or existing.content_hash != hash:
-- Compute embedding vector via local model -- Compute embedding vector via local model
-- Upsert into USearch index + embedding_keys DB table -- Upsert into HNSW index + embedding_keys DB table
-- Debounced index save (5s) -- Debounced index save (5s)
ensures: EmbeddingKeyUpdated(post) ensures: EmbeddingKeyUpdated(post)
} }
@@ -146,9 +154,9 @@ rule IndexUnindexed {
rule FindSimilar { rule FindSimilar {
when: FindSimilarRequested(post, limit) when: FindSimilarRequested(post, limit)
requires: semantic_similarity_enabled requires: semantic_similarity_enabled
-- HNSW approximate nearest neighbor search via USearch -- HNSW approximate nearest neighbor search (hnswlib)
-- Searches index for (limit + 1) neighbors, excludes self -- Searches index for (limit + 1) neighbors, excludes self
-- Converts USearch cosine distance to similarity: max(0, 1 - distance) -- Converts HNSW cosine distance to similarity: max(0, 1 - distance)
-- Returns ranked list sorted by descending similarity -- Returns ranked list sorted by descending similarity
ensures: SimilarPostsResult(post, ranked_matches) ensures: SimilarPostsResult(post, ranked_matches)
} }
@@ -157,7 +165,7 @@ rule ComputeSimilarities {
when: ComputeSimilaritiesRequested(source_post, target_post_ids) when: ComputeSimilaritiesRequested(source_post, target_post_ids)
requires: semantic_similarity_enabled requires: semantic_similarity_enabled
-- Exact pairwise cosine similarity between source vector and each target vector -- Exact pairwise cosine similarity between source vector and each target vector
-- Uses in-memory vector cache, NOT USearch search -- Uses in-memory vector cache, NOT the HNSW index
-- Returns map of post_id -> similarity score -- Returns map of post_id -> similarity score
-- Used by InsertPostLinkModal to rank FTS search results -- Used by InsertPostLinkModal to rank FTS search results
ensures: SimilarityScoresResult(source_post, scores) ensures: SimilarityScoresResult(source_post, scores)
@@ -202,7 +210,7 @@ invariant ContentHashSkipsUnchanged {
} }
invariant DebouncedPersistence { invariant DebouncedPersistence {
-- USearch index persistence is debounced at 5 seconds -- HNSW index persistence is debounced at 5 seconds
-- Prevents excessive disk I/O during bulk operations -- Prevents excessive disk I/O during bulk operations
-- Index also force-saved on project switch and app shutdown -- Index also force-saved on project switch and app shutdown
} }
@@ -221,6 +229,19 @@ invariant RealNeuralModel {
-- This is only achievable with the trained multilingual transformer model. -- This is only achievable with the trained multilingual transformer model.
} }
invariant NativeAcceleratedExecution {
-- Model execution MUST use the platform's native hardware acceleration
-- where available (GPU/Metal/Neural Engine on Apple Silicon, CUDA on
-- NVIDIA, etc.), and otherwise fall back to optimised native CPU execution.
-- Inference MUST be batched: batch_size inputs are run per compiled
-- inference pass and inputs are truncated to a bounded sequence_length, so
-- (re)indexing many posts is not serialised one document at a time.
-- Current implementation: Bumblebee + EXLA, which is native CPU on Apple
-- Silicon (XLA has no Metal backend); neighbour search is HNSW (hnswlib).
-- Apple GPU acceleration via EMLX/MLX is tracked as a follow-up
-- (SPECGAPS A1-14c).
}
invariant ModelCaching { invariant ModelCaching {
-- Model files (~100 MB) downloaded from Hugging Face Hub on first use -- Model files (~100 MB) downloaded from Hugging Face Hub on first use
-- Cached in app data directory, persists across sessions -- Cached in app data directory, persists across sessions
@@ -228,7 +249,7 @@ invariant ModelCaching {
} }
invariant ProjectIsolation { invariant ProjectIsolation {
-- Each project has its own USearch index file and embedding_keys rows -- Each project has its own HNSW index file and embedding_keys rows
-- On project switch: save current index, load new project's index -- On project switch: save current index, load new project's index
-- Model pipeline shared across projects (not reloaded) -- Model pipeline shared across projects (not reloaded)
} }

View File

@@ -286,15 +286,16 @@ defmodule BDS.AITest do
end end
test "model_capabilities does not create atoms from unknown capability keys" do test "model_capabilities does not create atoms from unknown capability keys" do
before = :erlang.system_info(:atom_count) fictive_key = "csm001_fictive_#{:erlang.unique_integer()}"
another_key = "another_unknown_#{:erlang.unique_integer()}"
malicious_caps = malicious_caps =
Jason.encode!(%{ Jason.encode!(%{
"supports_attachment" => true, "supports_attachment" => true,
"supports_tool_calls" => false, "supports_tool_calls" => false,
"disables_reasoning" => false, "disables_reasoning" => false,
"csm001_fictive_#{:erlang.unique_integer()}" => true, fictive_key => true,
"another_unknown_#{:erlang.unique_integer()}" => 42 another_key => 42
}) })
:ok = BDS.AI.SettingsStore.put_setting("ai.model_capabilities.csm-test-model", malicious_caps) :ok = BDS.AI.SettingsStore.put_setting("ai.model_capabilities.csm-test-model", malicious_caps)
@@ -303,8 +304,9 @@ defmodule BDS.AITest do
assert result.supports_attachment == true assert result.supports_attachment == true
assert result.supports_tool_calls == false assert result.supports_tool_calls == false
after_count = :erlang.system_info(:atom_count) # Unknown keys must remain as strings, not converted to atoms
assert after_count == before assert_raise ArgumentError, fn -> String.to_existing_atom(fictive_key) end
assert_raise ArgumentError, fn -> String.to_existing_atom(another_key) end
end end
test "put_endpoint, get_endpoint, and delete_endpoint manage encrypted endpoint settings" do test "put_endpoint, get_endpoint, and delete_endpoint manage encrypted endpoint settings" do

View File

@@ -31,7 +31,7 @@ defmodule BDS.CSM013BangRenderingTest do
assert {:error, _reason} = result assert {:error, _reason} = result
end end
test "render_post_page returns {:error, _} for broken template source", %{project: project} do test "publish_template rejects broken template source", %{project: project} do
{:ok, template} = {:ok, template} =
BDS.Templates.create_template(%{ BDS.Templates.create_template(%{
project_id: project.id, project_id: project.id,
@@ -40,30 +40,7 @@ defmodule BDS.CSM013BangRenderingTest do
content: "{% if true %}unclosed if" content: "{% if true %}unclosed if"
}) })
{:ok, published_template} = BDS.Templates.publish_template(template.id) assert {:error, {:invalid_liquid, _reason}} = BDS.Templates.publish_template(template.id)
{:ok, post} =
BDS.Posts.create_post(%{
project_id: project.id,
title: "Test Post",
content: "Body",
language: "en",
template_slug: published_template.slug
})
{:ok, published_post} = BDS.Posts.publish_post(post.id)
result =
Rendering.render_post_page(project.id, published_template.slug, %{
id: published_post.id,
title: published_post.title,
content: published_post.content || "",
slug: published_post.slug,
language: "en",
template_slug: published_post.template_slug
})
assert {:error, _reason} = result
end end
end end

View File

@@ -7,7 +7,7 @@ defmodule BDS.CSM025HardcodedLanguagesTest do
%{language: main, blog_languages: languages} %{language: main, blog_languages: languages}
end end
defp html(path), do: {path, "<p>hello</p>"} defp html(path), do: {path, "<article data-pagefind-body><p>hello</p></article>"}
describe "build_outputs/2 derives language prefixes from plan" do describe "build_outputs/2 derives language prefixes from plan" do
test "no hardcoded language prefixes in source" do test "no hardcoded language prefixes in source" do

View File

@@ -34,9 +34,13 @@ defmodule BDS.CSM033BatchInsertsTest do
"expected ON CONFLICT upsert clause" "expected ON CONFLICT upsert clause"
end end
test "compute_key_data is used instead of individual Repo.insert_or_update", %{source: source} do test "build_key_rows computes rows for batched upsert instead of individual Repo.insert_or_update",
assert source =~ "compute_key_data(post, existing_key, next_label)", %{source: source} do
"expected compute_key_data helper for row computation" assert source =~ "build_key_rows(posts, existing_keys",
"expected build_key_rows helper for batched row computation"
assert source =~ "embed_many(",
"expected batched embedding via embed_many"
end end
end end

View File

@@ -44,32 +44,32 @@ defmodule BDS.Desktop.AutomationTest do
assert :ok = Automation.click(session, "[data-testid='toggle-sidebar']") assert :ok = Automation.click(session, "[data-testid='toggle-sidebar']")
snapshot = Automation.snapshot(session) snapshot = await(session, &(&1.sidebar_visible == false))
assert snapshot.sidebar_visible == false assert snapshot.sidebar_visible == false
assert :ok = Automation.press(session, "Meta+B") assert :ok = Automation.press(session, "Meta+B")
snapshot = Automation.snapshot(session) snapshot = await(session, &(&1.sidebar_visible == true))
assert snapshot.sidebar_visible == true assert snapshot.sidebar_visible == true
assert :ok = Automation.press(session, "Meta+J") assert :ok = Automation.press(session, "Meta+J")
snapshot = Automation.snapshot(session) snapshot = await(session, &(&1.panel_visible == true))
assert snapshot.panel_visible == true assert snapshot.panel_visible == true
assert :ok = Automation.press(session, "Meta+J") assert :ok = Automation.press(session, "Meta+J")
snapshot = Automation.snapshot(session) snapshot = await(session, &(&1.panel_visible == false))
assert snapshot.panel_visible == false assert snapshot.panel_visible == false
assert :ok = Automation.press(session, "Meta+,") assert :ok = Automation.press(session, "Meta+,")
snapshot = Automation.snapshot(session) snapshot = await(session, &(&1.editor_title == "Settings"))
assert snapshot.editor_title == "Settings" assert snapshot.editor_title == "Settings"
assert :ok = Automation.click(session, "[data-testid='toggle-assistant']") assert :ok = Automation.click(session, "[data-testid='toggle-assistant']")
snapshot = Automation.snapshot(session) snapshot = await(session, &(&1.assistant_visible == true))
assert snapshot.assistant_visible == true assert snapshot.assistant_visible == true
assert snapshot.panel_visible == false assert snapshot.panel_visible == false
@@ -92,7 +92,7 @@ defmodule BDS.Desktop.AutomationTest do
assert :ok = Automation.drag(session, "[data-resize='sidebar']", 90) assert :ok = Automation.drag(session, "[data-resize='sidebar']", 90)
snapshot = Automation.snapshot(session) snapshot = await(session, &(&1.sidebar_width >= 360 and &1.sidebar_width <= 380))
assert snapshot.sidebar_width >= 360 assert snapshot.sidebar_width >= 360
assert snapshot.sidebar_width <= 380 assert snapshot.sidebar_width <= 380
@@ -100,7 +100,7 @@ defmodule BDS.Desktop.AutomationTest do
assert :ok = Automation.reload(session) assert :ok = Automation.reload(session)
snapshot = Automation.snapshot(session) snapshot = await(session, &(&1.sidebar_visible == true and &1.sidebar_width >= resized_width - 2))
assert snapshot.sidebar_visible == true assert snapshot.sidebar_visible == true
assert snapshot.sidebar_width >= resized_width - 2 assert snapshot.sidebar_width >= resized_width - 2
assert snapshot.sidebar_width <= resized_width + 2 assert snapshot.sidebar_width <= resized_width + 2
@@ -140,12 +140,12 @@ defmodule BDS.Desktop.AutomationTest do
assert :ok = Automation.native_menu_action(session, "toggle_sidebar") assert :ok = Automation.native_menu_action(session, "toggle_sidebar")
snapshot = Automation.snapshot(session) snapshot = await(session, &(&1.sidebar_visible == false))
assert snapshot.sidebar_visible == false assert snapshot.sidebar_visible == false
assert :ok = Automation.native_menu_action(session, "edit_preferences") assert :ok = Automation.native_menu_action(session, "edit_preferences")
snapshot = Automation.snapshot(session) snapshot = await(session, &(&1.editor_title == "Settings"))
assert snapshot.editor_title == "Settings" assert snapshot.editor_title == "Settings"
end end
@@ -175,6 +175,24 @@ defmodule BDS.Desktop.AutomationTest do
end end
end end
# Polls snapshots until the predicate holds (or times out), returning the
# last snapshot. UI transitions after a keypress/click/menu action are
# asynchronous, so a single immediate snapshot can race under CPU load.
defp await(session, fun, timeout \\ 5_000)
defp await(session, _fun, timeout) when timeout <= 0, do: Automation.snapshot(session)
defp await(session, fun, timeout) do
snapshot = Automation.snapshot(session)
if fun.(snapshot) do
snapshot
else
Process.sleep(50)
await(session, fun, timeout - 50)
end
end
defp wait_until(fun, timeout \\ 5_000) defp wait_until(fun, timeout \\ 5_000)
defp wait_until(fun, timeout) when timeout <= 0, do: fun.() defp wait_until(fun, timeout) when timeout <= 0, do: fun.()

View File

@@ -241,7 +241,8 @@ defmodule BDS.Desktop.ShellLiveTest do
edit_draft: %{"name" => "news", "color" => "#3b82f6", "post_template_slug" => ""}, edit_draft: %{"name" => "news", "color" => "#3b82f6", "post_template_slug" => ""},
selected: ["news", "updates"], selected: ["news", "updates"],
merge_target: "news", merge_target: "news",
templates: [%{slug: "post-template", title: "Post Template"}] templates: [%{slug: "post-template", title: "Post Template"}],
colour_presets: BDS.Desktop.ShellLive.TagsEditor.colour_presets()
} }
} }
end end
@@ -283,17 +284,44 @@ defmodule BDS.Desktop.ShellLiveTest do
@tag :phase3 @tag :phase3
test "phase 3 editors and shared surfaces render utility-owned layouts" do test "phase 3 editors and shared surfaces render utility-owned layouts" do
post_html = render_component(&BDS.Desktop.ShellLive.PostEditor.render/1, phase3_post_editor_assigns()) post_html =
media_html = render_component(&BDS.Desktop.ShellLive.MediaEditor.render/1, phase3_media_editor_assigns()) render_component(&BDS.Desktop.ShellLive.PostEditor.render/1, phase3_post_editor_assigns())
script_html = render_component(&BDS.Desktop.ShellLive.ScriptEditor.render/1, phase3_script_editor_assigns())
template_html = render_component(&BDS.Desktop.ShellLive.TemplateEditor.render/1, phase3_template_editor_assigns()) media_html =
chat_html = render_component(&BDS.Desktop.ShellLive.ChatEditor.render/1, phase3_chat_editor_assigns()) render_component(&BDS.Desktop.ShellLive.MediaEditor.render/1, phase3_media_editor_assigns())
menu_html = render_component(&BDS.Desktop.ShellLive.MenuEditor.render/1, phase3_menu_editor_assigns())
settings_html = render_component(&BDS.Desktop.ShellLive.SettingsEditor.render/1, phase3_settings_editor_assigns()) script_html =
tags_html = render_component(&BDS.Desktop.ShellLive.TagsEditor.render/1, phase3_tags_editor_assigns()) render_component(
&BDS.Desktop.ShellLive.ScriptEditor.render/1,
phase3_script_editor_assigns()
)
template_html =
render_component(
&BDS.Desktop.ShellLive.TemplateEditor.render/1,
phase3_template_editor_assigns()
)
chat_html =
render_component(&BDS.Desktop.ShellLive.ChatEditor.render/1, phase3_chat_editor_assigns())
menu_html =
render_component(&BDS.Desktop.ShellLive.MenuEditor.render/1, phase3_menu_editor_assigns())
settings_html =
render_component(
&BDS.Desktop.ShellLive.SettingsEditor.render/1,
phase3_settings_editor_assigns()
)
tags_html =
render_component(&BDS.Desktop.ShellLive.TagsEditor.render/1, phase3_tags_editor_assigns())
assert post_html =~ "post-editor ui-editor-shell flex h-full min-h-0 flex-col" assert post_html =~ "post-editor ui-editor-shell flex h-full min-h-0 flex-col"
assert post_html =~ "editor-header ui-editor-header flex shrink-0 items-start justify-between gap-3"
assert post_html =~
"editor-header ui-editor-header flex shrink-0 items-start justify-between gap-3"
assert post_html =~ "editor-field ui-field-stack flex flex-col gap-1.5" assert post_html =~ "editor-field ui-field-stack flex flex-col gap-1.5"
assert post_html =~ "editor-toolbar ui-toolbar flex items-center gap-3" assert post_html =~ "editor-toolbar ui-toolbar flex items-center gap-3"
@@ -312,7 +340,9 @@ defmodule BDS.Desktop.ShellLiveTest do
assert menu_html =~ "menu-editor-view ui-editor-shell flex h-full min-h-0 flex-col p-4" assert menu_html =~ "menu-editor-view ui-editor-shell flex h-full min-h-0 flex-col p-4"
assert menu_html =~ "menu-editor-toolbar ui-toolbar flex flex-wrap items-center gap-2" assert menu_html =~ "menu-editor-toolbar ui-toolbar flex flex-wrap items-center gap-2"
assert settings_html =~ "settings-view-shell ui-editor-shell flex h-full min-h-0 flex-col overflow-hidden" assert settings_html =~
"settings-view-shell ui-editor-shell flex h-full min-h-0 flex-col overflow-hidden"
assert settings_html =~ "settings-header flex shrink-0 items-center justify-between gap-3" assert settings_html =~ "settings-header flex shrink-0 items-center justify-between gap-3"
assert tags_html =~ "tags-view-shell flex h-full min-h-0 flex-col overflow-hidden" assert tags_html =~ "tags-view-shell flex h-full min-h-0 flex-col overflow-hidden"
@@ -324,12 +354,32 @@ defmodule BDS.Desktop.ShellLiveTest do
conn = Plug.Conn.put_private(build_conn(), :phoenix_endpoint, BDS.Desktop.Endpoint) conn = Plug.Conn.put_private(build_conn(), :phoenix_endpoint, BDS.Desktop.Endpoint)
{:ok, view, _shell_html} = live_isolated(conn, BDS.Desktop.ShellLive) {:ok, view, _shell_html} = live_isolated(conn, BDS.Desktop.ShellLive)
post_html = render_component(&BDS.Desktop.ShellLive.PostEditor.render/1, phase3_post_editor_assigns()) post_html =
media_html = render_component(&BDS.Desktop.ShellLive.MediaEditor.render/1, phase3_media_editor_assigns()) render_component(&BDS.Desktop.ShellLive.PostEditor.render/1, phase3_post_editor_assigns())
script_html = render_component(&BDS.Desktop.ShellLive.ScriptEditor.render/1, phase3_script_editor_assigns())
template_html = render_component(&BDS.Desktop.ShellLive.TemplateEditor.render/1, phase3_template_editor_assigns()) media_html =
settings_html = render_component(&BDS.Desktop.ShellLive.SettingsEditor.render/1, phase3_settings_editor_assigns()) render_component(&BDS.Desktop.ShellLive.MediaEditor.render/1, phase3_media_editor_assigns())
tags_html = render_component(&BDS.Desktop.ShellLive.TagsEditor.render/1, phase3_tags_editor_assigns())
script_html =
render_component(
&BDS.Desktop.ShellLive.ScriptEditor.render/1,
phase3_script_editor_assigns()
)
template_html =
render_component(
&BDS.Desktop.ShellLive.TemplateEditor.render/1,
phase3_template_editor_assigns()
)
settings_html =
render_component(
&BDS.Desktop.ShellLive.SettingsEditor.render/1,
phase3_settings_editor_assigns()
)
tags_html =
render_component(&BDS.Desktop.ShellLive.TagsEditor.render/1, phase3_tags_editor_assigns())
panel_html = panel_html =
render_component(&BDS.Desktop.ShellLive.PanelRenderer.render_panel_body/1, %{ render_component(&BDS.Desktop.ShellLive.PanelRenderer.render_panel_body/1, %{
@@ -354,7 +404,9 @@ defmodule BDS.Desktop.ShellLiveTest do
assert script_html =~ ~s(class="status-badge ui-badge) assert script_html =~ ~s(class="status-badge ui-badge)
assert script_html =~ ~s(class="ui-input") assert script_html =~ ~s(class="ui-input")
assert template_html =~ ~s(class="secondary templates-save-button ui-button ui-button-secondary) assert template_html =~
~s(class="secondary templates-save-button ui-button ui-button-secondary)
assert template_html =~ ~s(class="status-badge ui-badge) assert template_html =~ ~s(class="status-badge ui-badge)
assert template_html =~ ~s(class="ui-input") assert template_html =~ ~s(class="ui-input")
@@ -382,27 +434,44 @@ defmodule BDS.Desktop.ShellLiveTest do
conn = Plug.Conn.put_private(build_conn(), :phoenix_endpoint, BDS.Desktop.Endpoint) conn = Plug.Conn.put_private(build_conn(), :phoenix_endpoint, BDS.Desktop.Endpoint)
{:ok, _view, shell_html} = live_isolated(conn, BDS.Desktop.ShellLive) {:ok, _view, shell_html} = live_isolated(conn, BDS.Desktop.ShellLive)
media_html = render_component(&BDS.Desktop.ShellLive.MediaEditor.render/1, phase3_media_editor_assigns()) media_html =
chat_html = render_component(&BDS.Desktop.ShellLive.ChatEditor.render/1, phase3_chat_editor_assigns()) render_component(&BDS.Desktop.ShellLive.MediaEditor.render/1, phase3_media_editor_assigns())
menu_html = render_component(&BDS.Desktop.ShellLive.MenuEditor.render/1, phase3_menu_editor_assigns())
chat_html =
render_component(&BDS.Desktop.ShellLive.ChatEditor.render/1, phase3_chat_editor_assigns())
menu_html =
render_component(&BDS.Desktop.ShellLive.MenuEditor.render/1, phase3_menu_editor_assigns())
assert shell_html =~ ~s(class="assistant-sidebar-context flex shrink-0 flex-col gap-2") assert shell_html =~ ~s(class="assistant-sidebar-context flex shrink-0 flex-col gap-2")
assert shell_html =~ ~s(class="assistant-sidebar-prompt min-h-[8rem] w-full resize-y") assert shell_html =~ ~s(class="assistant-sidebar-prompt min-h-[8rem] w-full resize-y")
assert shell_html =~ ~s(class="assistant-sidebar-start-button ui-button ui-button-primary") assert shell_html =~ ~s(class="assistant-sidebar-start-button ui-button ui-button-primary")
assert shell_html =~ ~s(class="assistant-sidebar-welcome min-h-0 flex-1 overflow-auto") assert shell_html =~ ~s(class="assistant-sidebar-welcome min-h-0 flex-1 overflow-auto")
assert media_html =~ "class=\"editor-content media-editor grid min-h-0 flex-1 gap-4 overflow-auto p-4 xl:grid-cols-[minmax(320px,1fr)_minmax(0,1.2fr)]\"" assert media_html =~
"class=\"editor-content media-editor grid min-h-0 flex-1 gap-4 overflow-auto p-4 xl:grid-cols-[minmax(320px,1fr)_minmax(0,1.2fr)]\""
assert media_html =~ "class=\"media-preview flex min-h-[16rem] items-center justify-center\"" assert media_html =~ "class=\"media-preview flex min-h-[16rem] items-center justify-center\""
assert media_html =~ ~s(class="media-details min-w-0") assert media_html =~ ~s(class="media-details min-w-0")
assert chat_html =~ ~s(class="chat-panel ui-editor-shell flex h-full min-h-0 flex-col") assert chat_html =~ ~s(class="chat-panel ui-editor-shell flex h-full min-h-0 flex-col")
assert chat_html =~ ~s(class="chat-model-selector-button chat-model-selector-inline ui-button ui-button-secondary inline-flex items-center gap-2")
assert chat_html =~ ~s(class="chat-input-container ui-field-stack flex shrink-0 flex-col gap-3") assert chat_html =~
~s(class="chat-model-selector-button chat-model-selector-inline ui-button ui-button-secondary inline-flex items-center gap-2")
assert chat_html =~
~s(class="chat-input-container ui-field-stack flex shrink-0 flex-col gap-3")
assert chat_html =~ ~s(class="chat-input-wrapper flex items-end gap-2") assert chat_html =~ ~s(class="chat-input-wrapper flex items-end gap-2")
assert menu_html =~ ~s(class="menu-editor-view ui-editor-shell flex h-full min-h-0 flex-col p-4") assert menu_html =~
assert menu_html =~ ~s(class="menu-editor-toolbar ui-toolbar flex flex-wrap items-center gap-2") ~s(class="menu-editor-view ui-editor-shell flex h-full min-h-0 flex-col p-4")
assert menu_html =~ ~s(class="menu-editor-empty flex min-h-0 flex-1 items-center justify-center")
assert menu_html =~
~s(class="menu-editor-toolbar ui-toolbar flex flex-wrap items-center gap-2")
assert menu_html =~
~s(class="menu-editor-empty flex min-h-0 flex-1 items-center justify-center")
end end
alias BDS.Persistence alias BDS.Persistence
@@ -1071,7 +1140,10 @@ defmodule BDS.Desktop.ShellLiveTest do
assert html =~ ~s(data-testid="sidebar-shell") assert html =~ ~s(data-testid="sidebar-shell")
assert html =~ ~s(data-testid="status-bar") assert html =~ ~s(data-testid="status-bar")
assert html =~ ~s(data-testid="status-task-button") assert html =~ ~s(data-testid="status-task-button")
assert html =~ ~s(class="panel-shell flex min-h-0 shrink-0 flex-col overflow-hidden is-hidden")
assert html =~
~s(class="panel-shell flex min-h-0 shrink-0 flex-col overflow-hidden is-hidden")
assert html =~ ~s(data-testid="activity-button") assert html =~ ~s(data-testid="activity-button")
assert html =~ ~s(data-view="posts") assert html =~ ~s(data-view="posts")
assert html =~ ~s(data-view="media") assert html =~ ~s(data-view="media")
@@ -1103,7 +1175,10 @@ defmodule BDS.Desktop.ShellLiveTest do
|> render_click() |> render_click()
assert html =~ ~s(data-region="panel") assert html =~ ~s(data-region="panel")
refute html =~ ~s(class="panel-shell flex min-h-0 shrink-0 flex-col overflow-hidden is-hidden")
refute html =~
~s(class="panel-shell flex min-h-0 shrink-0 flex-col overflow-hidden is-hidden")
assert html =~ ~s(data-testid="panel-close") assert html =~ ~s(data-testid="panel-close")
html = html =
@@ -1111,7 +1186,8 @@ defmodule BDS.Desktop.ShellLiveTest do
|> element("[data-testid='panel-close']") |> element("[data-testid='panel-close']")
|> render_click() |> render_click()
assert html =~ ~s(class="panel-shell flex min-h-0 shrink-0 flex-col overflow-hidden is-hidden") assert html =~
~s(class="panel-shell flex min-h-0 shrink-0 flex-col overflow-hidden is-hidden")
html = html =
view view
@@ -2034,13 +2110,17 @@ defmodule BDS.Desktop.ShellLiveTest do
{:ok, view, html} = live_isolated(build_conn(), BDS.Desktop.ShellLive) {:ok, view, html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
assert html =~ ~s(data-testid="sidebar-shell") assert html =~ ~s(data-testid="sidebar-shell")
assert html =~ ~s(class="panel-shell flex min-h-0 shrink-0 flex-col overflow-hidden is-hidden")
assert html =~
~s(class="panel-shell flex min-h-0 shrink-0 flex-col overflow-hidden is-hidden")
html = render_keydown(view, "shortcut", %{key: "b", meta: true}) html = render_keydown(view, "shortcut", %{key: "b", meta: true})
assert html =~ ~s(class="sidebar-shell flex min-w-0 shrink-0 overflow-hidden is-hidden") assert html =~ ~s(class="sidebar-shell flex min-w-0 shrink-0 overflow-hidden is-hidden")
html = render_keydown(view, "shortcut", %{key: "j", meta: true}) html = render_keydown(view, "shortcut", %{key: "j", meta: true})
refute html =~ ~s(class="panel-shell flex min-h-0 shrink-0 flex-col overflow-hidden is-hidden")
refute html =~
~s(class="panel-shell flex min-h-0 shrink-0 flex-col overflow-hidden is-hidden")
html = render_keydown(view, "shortcut", %{key: "2", meta: true}) html = render_keydown(view, "shortcut", %{key: "2", meta: true})
assert html =~ ~s(data-view="media") assert html =~ ~s(data-view="media")
@@ -2275,14 +2355,17 @@ defmodule BDS.Desktop.ShellLiveTest do
assert html =~ "Add published posts" assert html =~ "Add published posts"
html = render_click(view, "select_panel_tab", %{"tab" => "output"}) html = render_click(view, "select_panel_tab", %{"tab" => "output"})
refute html =~ ~s(class="panel-shell flex min-h-0 shrink-0 flex-col overflow-hidden is-hidden")
refute html =~
~s(class="panel-shell flex min-h-0 shrink-0 flex-col overflow-hidden is-hidden")
html = html =
view view
|> element("[data-testid='status-task-button']") |> element("[data-testid='status-task-button']")
|> render_click() |> render_click()
refute html =~ ~s(class="panel-shell flex min-h-0 shrink-0 flex-col overflow-hidden is-hidden") refute html =~
~s(class="panel-shell flex min-h-0 shrink-0 flex-col overflow-hidden is-hidden")
assert Regex.match?( assert Regex.match?(
~r/<button class="panel-tab [^"]*ui-tab[^"]*active" type="button" phx-click="select_panel_tab" phx-value-tab="tasks">/, ~r/<button class="panel-tab [^"]*ui-tab[^"]*active" type="button" phx-click="select_panel_tab" phx-value-tab="tasks">/,
@@ -3384,13 +3467,22 @@ defmodule BDS.Desktop.ShellLiveTest do
"subtitle" => "published" "subtitle" => "published"
}) })
assert published_script_html =~ ~s(class="scripts-view-shell ui-editor-shell flex h-full min-h-0 flex-col") assert published_script_html =~
~s(class="scripts-view-shell ui-editor-shell flex h-full min-h-0 flex-col")
assert published_script_html =~ ~s(data-testid="script-editor") assert published_script_html =~ ~s(data-testid="script-editor")
assert published_script_html =~ ~s(data-testid="script-status-badge") assert published_script_html =~ ~s(data-testid="script-status-badge")
assert published_script_html =~ ~s(class="status-badge ui-badge status-published") assert published_script_html =~ ~s(class="status-badge ui-badge status-published")
assert published_script_html =~ ~s(class="secondary scripts-save-button ui-button ui-button-secondary")
assert published_script_html =~ ~s(class="secondary scripts-run-button ui-button ui-button-secondary") assert published_script_html =~
assert published_script_html =~ ~s(class="secondary scripts-check-button ui-button ui-button-secondary") ~s(class="secondary scripts-save-button ui-button ui-button-secondary")
assert published_script_html =~
~s(class="secondary scripts-run-button ui-button ui-button-secondary")
assert published_script_html =~
~s(class="secondary scripts-check-button ui-button ui-button-secondary")
assert published_script_html =~ "published" assert published_script_html =~ "published"
assert published_script_html =~ "published script" assert published_script_html =~ "published script"
@@ -3405,12 +3497,19 @@ defmodule BDS.Desktop.ShellLiveTest do
"subtitle" => "published" "subtitle" => "published"
}) })
assert published_template_html =~ ~s(class="templates-view-shell ui-editor-shell flex h-full min-h-0 flex-col") assert published_template_html =~
~s(class="templates-view-shell ui-editor-shell flex h-full min-h-0 flex-col")
assert published_template_html =~ ~s(data-testid="template-editor") assert published_template_html =~ ~s(data-testid="template-editor")
assert published_template_html =~ ~s(data-testid="template-status-badge") assert published_template_html =~ ~s(data-testid="template-status-badge")
assert published_template_html =~ ~s(class="status-badge ui-badge status-published") assert published_template_html =~ ~s(class="status-badge ui-badge status-published")
assert published_template_html =~ ~s(class="secondary templates-save-button ui-button ui-button-secondary")
assert published_template_html =~ ~s(class="secondary templates-validate-button ui-button ui-button-secondary") assert published_template_html =~
~s(class="secondary templates-save-button ui-button ui-button-secondary")
assert published_template_html =~
~s(class="secondary templates-validate-button ui-button ui-button-secondary")
assert published_template_html =~ "published" assert published_template_html =~ "published"
assert published_template_html =~ "published template" assert published_template_html =~ "published template"
@@ -3600,6 +3699,7 @@ defmodule BDS.Desktop.ShellLiveTest do
assert html =~ assert html =~
"class=\"editor-content media-editor grid min-h-0 flex-1 gap-4 overflow-auto p-4 xl:grid-cols-[minmax(320px,1fr)_minmax(0,1.2fr)]\"" "class=\"editor-content media-editor grid min-h-0 flex-1 gap-4 overflow-auto p-4 xl:grid-cols-[minmax(320px,1fr)_minmax(0,1.2fr)]\""
assert html =~ ~s(class="quick-actions-wrapper relative") assert html =~ ~s(class="quick-actions-wrapper relative")
refute html =~ ~s(class="media-editor-form") refute html =~ ~s(class="media-editor-form")
@@ -3624,7 +3724,10 @@ defmodule BDS.Desktop.ShellLiveTest do
|> render_click() |> render_click()
assert html =~ ~s(class="translation-modal-backdrop") assert html =~ ~s(class="translation-modal-backdrop")
assert html =~ ~s(class="translation-modal flex max-h-[80vh] w-full max-w-2xl flex-col overflow-hidden")
assert html =~
~s(class="translation-modal flex max-h-[80vh] w-full max-w-2xl flex-col overflow-hidden")
assert html =~ ~s(name="media_translation[title]") assert html =~ ~s(name="media_translation[title]")
assert html =~ ~s(name="media_translation[alt]") assert html =~ ~s(name="media_translation[alt]")
assert html =~ ~s(name="media_translation[caption]") assert html =~ ~s(name="media_translation[caption]")
@@ -3712,7 +3815,9 @@ defmodule BDS.Desktop.ShellLiveTest do
"subtitle" => "Project settings" "subtitle" => "Project settings"
}) })
assert settings_html =~ ~s(class="settings-view-shell ui-editor-shell flex h-full min-h-0 flex-col overflow-hidden") assert settings_html =~
~s(class="settings-view-shell ui-editor-shell flex h-full min-h-0 flex-col overflow-hidden")
assert settings_html =~ ~s(class="setting-section ui-section-card") assert settings_html =~ ~s(class="setting-section ui-section-card")
refute settings_html =~ "Desktop workbench content routed through the Elixir shell." refute settings_html =~ "Desktop workbench content routed through the Elixir shell."
@@ -3748,7 +3853,9 @@ defmodule BDS.Desktop.ShellLiveTest do
"subtitle" => script.slug "subtitle" => script.slug
}) })
assert script_html =~ ~s(class="scripts-view-shell ui-editor-shell flex h-full min-h-0 flex-col") assert script_html =~
~s(class="scripts-view-shell ui-editor-shell flex h-full min-h-0 flex-col")
assert script_html =~ "scripts-monaco" assert script_html =~ "scripts-monaco"
assert script_html =~ ~s(data-monaco-language="lua") assert script_html =~ ~s(data-monaco-language="lua")
assert script_html =~ ~s(data-monaco-word-wrap="on") assert script_html =~ ~s(data-monaco-word-wrap="on")
@@ -3763,7 +3870,9 @@ defmodule BDS.Desktop.ShellLiveTest do
"subtitle" => template.slug "subtitle" => template.slug
}) })
assert template_html =~ ~s(class="templates-view-shell ui-editor-shell flex h-full min-h-0 flex-col") assert template_html =~
~s(class="templates-view-shell ui-editor-shell flex h-full min-h-0 flex-col")
assert template_html =~ "templates-monaco" assert template_html =~ "templates-monaco"
assert template_html =~ ~s(data-monaco-language="liquid") assert template_html =~ ~s(data-monaco-language="liquid")
assert template_html =~ ~s(data-monaco-word-wrap="on") assert template_html =~ ~s(data-monaco-word-wrap="on")
@@ -3779,7 +3888,10 @@ defmodule BDS.Desktop.ShellLiveTest do
}) })
assert chat_html =~ ~s(class="chat-panel ui-editor-shell flex h-full min-h-0 flex-col") assert chat_html =~ ~s(class="chat-panel ui-editor-shell flex h-full min-h-0 flex-col")
assert chat_html =~ ~s(class="chat-input-container ui-field-stack flex shrink-0 flex-col gap-3")
assert chat_html =~
~s(class="chat-input-container ui-field-stack flex shrink-0 flex-col gap-3")
refute chat_html =~ "Desktop workbench content routed through the Elixir shell." refute chat_html =~ "Desktop workbench content routed through the Elixir shell."
end end
@@ -3799,7 +3911,10 @@ defmodule BDS.Desktop.ShellLiveTest do
assert html =~ ~s(data-testid="chat-model-selector-button") assert html =~ ~s(data-testid="chat-model-selector-button")
assert html =~ ~s(class="chat-panel-title-main") assert html =~ ~s(class="chat-panel-title-main")
assert html =~ ~s(class="chat-model-selector-wrap relative shrink-0") assert html =~ ~s(class="chat-model-selector-wrap relative shrink-0")
assert html =~ ~s(class="chat-model-selector-button chat-model-selector-inline ui-button ui-button-secondary inline-flex items-center gap-2")
assert html =~
~s(class="chat-model-selector-button chat-model-selector-inline ui-button ui-button-secondary inline-flex items-center gap-2")
refute html =~ ~s(class="chat-panel-header-actions") refute html =~ ~s(class="chat-panel-header-actions")
css = desktop_css_source() css = desktop_css_source()
@@ -3847,7 +3962,9 @@ defmodule BDS.Desktop.ShellLiveTest do
|> element("[data-testid='chat-model-selector-button']") |> element("[data-testid='chat-model-selector-button']")
|> render_click() |> render_click()
assert selector_html =~ ~s(class="chat-model-selector-menu ui-dropdown-menu absolute right-0 top-full z-10 mt-2 flex min-w-56 flex-col") assert selector_html =~
~s(class="chat-model-selector-menu ui-dropdown-menu absolute right-0 top-full z-10 mt-2 flex min-w-56 flex-col")
assert selector_html =~ ~s(data-testid="chat-model-selector-option") assert selector_html =~ ~s(data-testid="chat-model-selector-option")
assert selector_html =~ "llama-current" assert selector_html =~ "llama-current"
@@ -4699,7 +4816,9 @@ defmodule BDS.Desktop.ShellLiveTest do
{user_index, _length} = :binary.match(html, "Newest question") {user_index, _length} = :binary.match(html, "Newest question")
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_and_await(view, { send_and_await(view, {
:chat_tool_call, :chat_tool_call,
@@ -4919,6 +5038,74 @@ defmodule BDS.Desktop.ShellLiveTest do
refute html =~ ~s(<pre><code>) refute html =~ ~s(<pre><code>)
end end
test "git sidebar wires branch, changes, history, and action buttons to BDS.Git", %{
temp_dir: temp_dir
} do
posts_dir = Path.join(temp_dir, "posts")
File.mkdir_p!(posts_dir)
tracked = Path.join(posts_dir, "tracked.md")
File.write!(tracked, "Old content\n")
init_git_repo!(temp_dir, "seed commit")
File.write!(tracked, "New content\n")
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
html =
view
|> element("[data-testid='activity-button'][data-view='git']")
|> render_click()
assert html =~ ~s(class="git-sidebar")
assert html =~ ~s(data-testid="git-branch")
assert html =~ "master"
assert html =~ ~s(data-testid="git-status-file")
assert html =~ "posts/tracked.md"
assert html =~ ~s(data-testid="git-commit-form")
assert html =~ ~s(data-testid="git-action-fetch")
assert html =~ ~s(data-testid="git-action-pull")
assert html =~ ~s(data-testid="git-action-push")
assert html =~ ~s(data-testid="git-action-prune-lfs")
assert html =~ ~s(data-testid="git-history-entry")
assert html =~ "seed commit"
end
test "git sidebar commit stages working tree and reloads to a clean state", %{
project: project,
temp_dir: temp_dir
} do
posts_dir = Path.join(temp_dir, "posts")
File.mkdir_p!(posts_dir)
File.write!(Path.join(posts_dir, "seed.md"), "Seed\n")
init_git_repo!(temp_dir, "seed commit")
File.write!(Path.join(posts_dir, "added.md"), "Brand new\n")
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
view
|> element("[data-testid='activity-button'][data-view='git']")
|> render_click()
html = render_submit(view, "git_commit", %{"git" => %{"message" => "Add new post"}})
refute html =~ "posts/added.md"
assert {:ok, %{commits: commits}} = BDS.Git.history(project.id, "master")
assert Enum.any?(commits, &(&1.subject == "Add new post"))
end
test "git sidebar shows the initialize control when the project is not a repo" do
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
html =
view
|> element("[data-testid='activity-button'][data-view='git']")
|> render_click()
assert html =~ ~s(class="git-sidebar")
assert html =~ ~s(data-testid="git-initialize")
end
test "settings sidebar categories render the full old-app section model and target the requested section" do test "settings sidebar categories render the full old-app section model and target the requested section" do
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive) {:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)

View File

@@ -256,6 +256,53 @@ defmodule BDS.DesktopTest do
assert_receive :window_quit_requested assert_receive :window_quit_requested
end end
test "the app owns final termination instead of delegating to Desktop.Window/System.halt" do
# Desktop.Window.quit/0 routes through System.halt/1, which runs the wx C++
# static destructors on exit and crashes on macOS. The app-owned shutdown
# must terminate the OS process directly with SIGKILL instead.
assert BDS.Desktop.Shutdown.quit_module() == BDS.Desktop.Shutdown
end
test "hard quit terminates the BEAM OS process with SIGKILL rather than libc exit" do
previous_kill = Application.get_env(:bds, :desktop_os_kill_fun)
test_pid = self()
Application.put_env(:bds, :desktop_os_kill_fun, fn os_pid ->
send(test_pid, {:os_killed, to_string(os_pid)})
:ok
end)
on_exit(fn -> restore_env(:desktop_os_kill_fun, previous_kill) end)
assert :ok = BDS.Desktop.Shutdown.quit()
assert_receive {:os_killed, beam_pid}
assert beam_pid == to_string(:os.getpid())
end
test "hard quit kills heart before itself so the app is not relaunched" do
previous_kill = Application.get_env(:bds, :desktop_os_kill_fun)
test_pid = self()
{:ok, fake_heart} =
Agent.start(fn -> :ok end, name: :heart)
Application.put_env(:bds, :desktop_os_kill_fun, fn os_pid ->
send(test_pid, {:os_killed, to_string(os_pid)})
:ok
end)
on_exit(fn ->
restore_env(:desktop_os_kill_fun, previous_kill)
if Process.alive?(fake_heart), do: Agent.stop(fake_heart)
end)
assert :ok = BDS.Desktop.Shutdown.quit()
# heart has no linked port here, so only the BEAM itself is killed, and the
# call must not crash while inspecting the heart process.
assert_receive {:os_killed, beam_pid}
assert beam_pid == to_string(:os.getpid())
end
test "desktop root html is a LiveView shell and references the generated asset entrypoints" do test "desktop root html is a LiveView shell and references the generated asset entrypoints" do
conn = conn(:get, "/?k=#{Desktop.Auth.login_key()}") conn = conn(:get, "/?k=#{Desktop.Auth.login_key()}")
conn = BDS.Desktop.Endpoint.call(conn, BDS.Desktop.Endpoint.init([])) conn = BDS.Desktop.Endpoint.call(conn, BDS.Desktop.Endpoint.init([]))

View File

@@ -0,0 +1,39 @@
defmodule BDS.Embeddings.Backends.NeuralTest do
use ExUnit.Case, async: false
alias BDS.Embeddings.Backends.Neural
setup do
previous = Application.get_env(:bds, :embeddings)
on_exit(fn ->
if previous == nil do
Application.delete_env(:bds, :embeddings)
else
Application.put_env(:bds, :embeddings, previous)
end
end)
:ok
end
test "reports the configured spec model id and dimensions without loading the model" do
Application.put_env(:bds, :embeddings,
backend: Neural,
model_id: "Xenova/multilingual-e5-small",
model_repo: "intfloat/multilingual-e5-small",
dimensions: 384
)
assert %{model_id: "Xenova/multilingual-e5-small", dimensions: 384} = Neural.model_info()
end
test "implements the embeddings backend behaviour" do
behaviours =
Neural.module_info(:attributes)
|> Keyword.get_values(:behaviour)
|> List.flatten()
assert BDS.Embeddings.Backend in behaviours
end
end

View File

@@ -15,6 +15,62 @@ defmodule BDS.EmbeddingsTest do
end end
end end
defmodule BatchRecordingBackend do
@behaviour BDS.Embeddings.Backend
@recorder :embeddings_batch_recorder
@impl true
def model_info do
%{model_id: "batch/multilingual-e5-small", dimensions: 384}
end
@impl true
def embed(text, opts) do
BDS.Embeddings.Backends.InApp.embed(text, opts)
end
@impl true
def embed_many(texts, opts) do
Agent.update(@recorder, fn sizes -> [length(texts) | sizes] end)
BDS.Embeddings.Backends.InApp.embed_many(texts, opts)
end
end
defmodule CountingBackend do
@behaviour BDS.Embeddings.Backend
@counter :embeddings_force_counter
@impl true
def model_info, do: %{model_id: "counting/multilingual-e5-small", dimensions: 384}
@impl true
def embed(text, opts) do
Agent.update(@counter, &(&1 + 1))
BDS.Embeddings.Backends.InApp.embed(text, opts)
end
@impl true
def embed_many(texts, opts) do
Agent.update(@counter, &(&1 + length(texts)))
BDS.Embeddings.Backends.InApp.embed_many(texts, opts)
end
end
defmodule FailingBackend do
@behaviour BDS.Embeddings.Backend
@impl true
def model_info, do: %{model_id: "failing/multilingual-e5-small", dimensions: 384}
@impl true
def embed(_text, _opts), do: {:error, :model_unavailable}
@impl true
def embed_many(_texts, _opts), do: {:error, :model_unavailable}
end
setup do setup do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo) :ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
@@ -297,30 +353,104 @@ defmodule BDS.EmbeddingsTest do
assert {:ok, _indexed} = BDS.Embeddings.index_unindexed(project.id) assert {:ok, _indexed} = BDS.Embeddings.index_unindexed(project.id)
# Persistence is debounced (5s); force it to disk to assert the files.
:ok = BDS.Embeddings.Index.flush(project.id)
index_path = BDS.Embeddings.index_path(project.id) index_path = BDS.Embeddings.index_path(project.id)
assert File.exists?(index_path) assert File.exists?(index_path)
assert File.exists?(index_path <> ".meta.json")
refute String.starts_with?(index_path, BDS.Projects.project_data_dir(project)) refute String.starts_with?(index_path, BDS.Projects.project_data_dir(project))
cache_root = Application.fetch_env!(:bds, :project_cache_root) |> Path.expand() cache_root = Application.fetch_env!(:bds, :project_cache_root) |> Path.expand()
assert index_path == Path.join([cache_root, "projects", project.id, "embeddings.usearch"]) assert index_path == Path.join([cache_root, "projects", project.id, "embeddings.usearch"])
snapshot = index_path |> File.read!() |> Jason.decode!() # The sidecar carries the dimension and the label→post_id mapping.
assert snapshot["project_id"] == project.id meta = (index_path <> ".meta.json") |> File.read!() |> Jason.decode!()
assert snapshot["model_id"] == "fake/multilingual-e5-small" assert meta["dim"] == 384
assert snapshot["dimensions"] == 384 post_ids = Enum.map(meta["labels"], fn [_label, post_id] -> post_id end)
assert snapshot["entries"][alpha.id]["label"] != nil assert alpha.id in post_ids
assert snapshot["entries"][alpha.id]["content_hash"] != nil assert beta.id in post_ids
assert Enum.any?(snapshot["entries"][alpha.id]["neighbors"], fn neighbor -> # The HNSW index answers nearest-neighbour queries.
neighbor["post_id"] == beta.id assert {:ok, [neighbor]} = BDS.Embeddings.find_similar(alpha.id, 1)
end) assert neighbor.post_id == beta.id
end end
test "embedding index uses the app-internal persisted file name", %{project: project} do test "embedding index uses the app-internal persisted file name", %{project: project} do
assert BDS.Embeddings.index_path(project.id) =~ "/embeddings.usearch" assert BDS.Embeddings.index_path(project.id) =~ "/embeddings.usearch"
end end
test "stored embedding vectors are packed Float32 BLOBs, not JSON text", %{project: project} do
assert {:ok, _metadata} =
BDS.Metadata.update_project_metadata(project.id, %{semantic_similarity_enabled: true})
assert {:ok, post} =
BDS.Posts.create_post(%{
project_id: project.id,
title: "Blob",
content: "space rocket orbit mission galaxy",
language: "en"
})
assert {:ok, post} = BDS.Posts.publish_post(post.id)
assert {:ok, _indexed} = BDS.Embeddings.index_unindexed(project.id)
key = BDS.Repo.get_by!(BDS.Embeddings.Key, project_id: project.id, post_id: post.id)
assert is_binary(key.vector)
# 384 dimensions * 4 bytes per little-endian Float32 (VectorCacheInDb).
assert byte_size(key.vector) == 384 * 4
refute String.starts_with?(key.vector, "[")
decoded = for <<value::float-32-little <- key.vector>>, do: value
assert length(decoded) == 384
# The packed vector still drives similarity queries.
assert {:ok, scores} = BDS.Embeddings.compute_similarities(post.id, [post.id])
assert is_map(scores)
end
test "rebuilding embeds posts in batches instead of one at a time", %{project: project} do
assert {:ok, _metadata} =
BDS.Metadata.update_project_metadata(project.id, %{semantic_similarity_enabled: true})
for index <- 1..5 do
assert {:ok, post} =
BDS.Posts.create_post(%{
project_id: project.id,
title: "Batch #{index}",
content: "space rocket orbit mission galaxy #{index}",
language: "en"
})
assert {:ok, _post} = BDS.Posts.publish_post(post.id)
end
# Simulate the post-migration state where the vector cache is empty, so the
# rebuild has to (re)embed every post.
BDS.Repo.delete_all(BDS.Embeddings.Key)
{:ok, _recorder} = Agent.start_link(fn -> [] end, name: :embeddings_batch_recorder)
Application.put_env(:bds, :embeddings,
backend: BatchRecordingBackend,
model_id: "batch/multilingual-e5-small",
dimensions: 384,
batch_size: 3
)
assert {:ok, rebuilt} = BDS.Embeddings.reindex_all(project.id)
assert length(rebuilt) == 5
batch_sizes = Agent.get(:embeddings_batch_recorder, & &1)
# 5 pending posts at batch_size 3 → one batch of 3 and one of 2, never
# one-at-a-time.
assert Enum.sort(batch_sizes, :desc) == [3, 2]
assert Enum.max(batch_sizes) > 1
end
test "reindex_all rebuilds stored embeddings for the whole project", %{project: project} do test "reindex_all rebuilds stored embeddings for the whole project", %{project: project} do
assert {:ok, _metadata} = assert {:ok, _metadata} =
BDS.Metadata.update_project_metadata(project.id, %{semantic_similarity_enabled: true}) BDS.Metadata.update_project_metadata(project.id, %{semantic_similarity_enabled: true})
@@ -351,43 +481,147 @@ defmodule BDS.EmbeddingsTest do
refreshed_key = BDS.Repo.get_by!(BDS.Embeddings.Key, project_id: project.id, post_id: post.id) refreshed_key = BDS.Repo.get_by!(BDS.Embeddings.Key, project_id: project.id, post_id: post.id)
assert refreshed_key.content_hash == stale_key.content_hash assert refreshed_key.content_hash == stale_key.content_hash
:ok = BDS.Embeddings.Index.flush(project.id)
assert File.exists?(BDS.Embeddings.index_path(project.id)) assert File.exists?(BDS.Embeddings.index_path(project.id))
end end
test "sync_post refreshes snapshot drift when the embedding hash is already current", %{ test "similarity queries keep working when sync_post finds the embedding already current", %{
project: project project: project
} do } do
assert {:ok, _metadata} = assert {:ok, _metadata} =
BDS.Metadata.update_project_metadata(project.id, %{semantic_similarity_enabled: true}) BDS.Metadata.update_project_metadata(project.id, %{semantic_similarity_enabled: true})
assert {:ok, alpha} =
BDS.Posts.create_post(%{
project_id: project.id,
title: "Alpha",
content: "space rocket orbit mission galaxy",
language: "en"
})
assert {:ok, beta} =
BDS.Posts.create_post(%{
project_id: project.id,
title: "Beta",
content: "rocket launch orbit mission station",
language: "en"
})
assert {:ok, alpha} = BDS.Posts.publish_post(alpha.id)
assert {:ok, _beta} = BDS.Posts.publish_post(beta.id)
assert {:ok, _indexed} = BDS.Embeddings.index_unindexed(project.id)
# Re-syncing with an unchanged content hash is a no-op for the index...
assert :ok = BDS.Embeddings.sync_post(alpha.id)
# ...and nearest-neighbour queries still resolve through the HNSW index.
assert {:ok, [neighbor]} = BDS.Embeddings.find_similar(alpha.id, 1)
assert neighbor.post_id == beta.id
end
test "find_similar rebuilds the HNSW index on demand when none is loaded", %{project: project} do
assert {:ok, _metadata} =
BDS.Metadata.update_project_metadata(project.id, %{semantic_similarity_enabled: true})
assert {:ok, alpha} =
BDS.Posts.create_post(%{
project_id: project.id,
title: "Alpha",
content: "space rocket orbit mission galaxy",
language: "en"
})
assert {:ok, beta} =
BDS.Posts.create_post(%{
project_id: project.id,
title: "Beta",
content: "rocket launch orbit mission station",
language: "en"
})
assert {:ok, _alpha} = BDS.Posts.publish_post(alpha.id)
assert {:ok, _beta} = BDS.Posts.publish_post(beta.id)
assert {:ok, _indexed} = BDS.Embeddings.index_unindexed(project.id)
# Drop the in-memory index and remove the persisted files, then query: it
# must self-heal by rebuilding from the DB vectors.
:ok = BDS.Embeddings.Index.forget(project.id)
File.rm_rf!(BDS.Projects.project_cache_dir(project.id))
assert {:ok, similar} = BDS.Embeddings.find_similar(alpha.id, 1)
assert [%{post_id: post_id}] = similar
assert post_id == beta.id
end
test "explicit rebuild re-embeds every post even when content is unchanged", %{project: project} do
assert {:ok, _metadata} =
BDS.Metadata.update_project_metadata(project.id, %{semantic_similarity_enabled: true})
{:ok, _agent} = Agent.start_link(fn -> 0 end, name: :embeddings_force_counter)
Application.put_env(:bds, :embeddings,
backend: CountingBackend,
model_id: "counting/multilingual-e5-small",
dimensions: 384,
batch_size: 16
)
for index <- 1..3 do
assert {:ok, post} = assert {:ok, post} =
BDS.Posts.create_post(%{ BDS.Posts.create_post(%{
project_id: project.id, project_id: project.id,
title: "Snapshot Repair", title: "Force #{index}",
content: "space rocket orbit mission galaxy #{index}",
language: "en"
})
assert {:ok, _post} = BDS.Posts.publish_post(post.id)
end
# Ignore embeds triggered while creating/publishing.
Agent.update(:embeddings_force_counter, fn _count -> 0 end)
# index_unindexed honours the content_hash skip: nothing to re-embed.
assert {:ok, _indexed} = BDS.Embeddings.index_unindexed(project.id)
assert Agent.get(:embeddings_force_counter, & &1) == 0
# An explicit rebuild re-embeds all three regardless (ReindexAll).
assert {:ok, rebuilt} = BDS.Embeddings.reindex_all(project.id)
assert length(rebuilt) == 3
assert Agent.get(:embeddings_force_counter, & &1) == 3
end
test "embedding operations degrade gracefully when the model is unavailable", %{
project: project
} do
assert {:ok, _metadata} =
BDS.Metadata.update_project_metadata(project.id, %{semantic_similarity_enabled: true})
Application.put_env(:bds, :embeddings,
backend: FailingBackend,
model_id: "failing/multilingual-e5-small",
dimensions: 384
)
# Saving a post must not crash even though embedding fails; it is just left
# unindexed.
assert {:ok, post} =
BDS.Posts.create_post(%{
project_id: project.id,
title: "Offline",
content: "space rocket orbit mission galaxy", content: "space rocket orbit mission galaxy",
language: "en" language: "en"
}) })
assert {:ok, post} = BDS.Posts.publish_post(post.id) assert {:ok, post} = BDS.Posts.publish_post(post.id)
assert {:ok, _indexed} = BDS.Embeddings.index_unindexed(project.id) assert BDS.Repo.get_by(BDS.Embeddings.Key, project_id: project.id, post_id: post.id) == nil
key = BDS.Repo.get_by!(BDS.Embeddings.Key, project_id: project.id, post_id: post.id) # Explicit (re)index operations surface a clean error instead of crashing.
index_path = BDS.Embeddings.index_path(project.id) assert {:error, :model_unavailable} = BDS.Embeddings.reindex_all(project.id)
assert {:error, :model_unavailable} = BDS.Embeddings.index_unindexed(project.id)
snapshot = index_path |> File.read!() |> Jason.decode!() # Queries stay safe.
assert {:ok, []} = BDS.Embeddings.find_similar(post.id, 5)
drifted_snapshot =
put_in(snapshot, ["entries", post.id, "content_hash"], "stale-snapshot-hash")
File.write!(index_path, Jason.encode!(drifted_snapshot))
refute Enum.any?(BDS.Embeddings.diff_reports(project.id), &(&1.entity_id == post.id))
assert :ok = BDS.Embeddings.sync_post(post.id)
repaired_snapshot = index_path |> File.read!() |> Jason.decode!()
assert get_in(repaired_snapshot, ["entries", post.id, "content_hash"]) == key.content_hash
refute Enum.any?(BDS.Embeddings.diff_reports(project.id), &(&1.entity_id == post.id))
end end
end end

View File

@@ -0,0 +1,91 @@
defmodule BDS.Generation.PagefindTest do
use ExUnit.Case, async: true
alias BDS.Generation.Pagefind
defp plan(language, blog_languages), do: %{language: language, blog_languages: blog_languages}
defp index_for(outputs, relative_path) do
{_path, content} = Enum.find(outputs, fn {path, _} -> path == relative_path end)
Jason.decode!(content)
end
defp content_for(outputs, relative_path) do
{_path, content} = Enum.find(outputs, fn {path, _} -> path == relative_path end)
content
end
test "indexes only pages marked with data-pagefind-body and scopes text to that region" do
post_html = """
<html><head><title>My First Post</title></head>
<body>
<nav>Home Archive Contact ignored-nav-term</nav>
<article data-pagefind-body><h1>My First Post</h1><p>Searchable elixir content.</p></article>
<footer>footer-term</footer>
</body></html>
"""
list_html = "<html><head><title>Home</title></head><body><ul>list page</ul></body></html>"
outputs =
Pagefind.build_outputs(plan("en", ["en"]), [
{"posts/first.html", post_html},
{"index.html", list_html}
])
index = index_for(outputs, "pagefind/index.json")
assert index["language"] == "en"
# Only the marked post page is indexed; the unmarked list page is excluded.
urls = Enum.map(index["pages"], & &1["url"])
assert "/posts/first.html" in urls
refute "/index.html" in urls
page = Enum.find(index["pages"], &(&1["url"] == "/posts/first.html"))
assert page["title"] == "My First Post"
# Body text is scoped to data-pagefind-body — nav/footer text is excluded.
assert page["text"] =~ "Searchable elixir content"
refute page["text"] =~ "ignored-nav-term"
refute page["text"] =~ "footer-term"
end
test "builds a separate index per language scoped by route prefix" do
en_post =
"<html><head><title>EN</title></head><body><article data-pagefind-body>english body</article></body></html>"
de_post =
"<html><head><title>DE</title></head><body><article data-pagefind-body>deutscher text</article></body></html>"
outputs =
Pagefind.build_outputs(plan("en", ["en", "de"]), [
{"posts/first.html", en_post},
{"de/posts/first.html", de_post}
])
en_index = index_for(outputs, "pagefind/index.json")
de_index = index_for(outputs, "de/pagefind/index.json")
assert en_index["language"] == "en"
assert Enum.map(en_index["pages"], & &1["url"]) == ["/posts/first.html"]
assert de_index["language"] == "de"
assert Enum.map(de_index["pages"], & &1["url"]) == ["/de/posts/first.html"]
end
test "emits a functional PagefindUI script and stylesheet per language" do
outputs =
Pagefind.build_outputs(plan("en", ["en"]), [
{"posts/first.html",
"<html><head><title>T</title></head><body><article data-pagefind-body>b</article></body></html>"}
])
js = content_for(outputs, "pagefind/pagefind-ui.js")
css = content_for(outputs, "pagefind/pagefind-ui.css")
# Real UI defines the PagefindUI global the search runtime instantiates.
assert js =~ "PagefindUI"
refute js =~ "window.bDSPagefind"
# Stylesheet carries real UI selectors, not a one-liner stub.
assert css =~ ".pagefind-ui__results"
end
end

View File

@@ -252,7 +252,7 @@ defmodule BDS.GenerationTest do
title: "Post View", title: "Post View",
kind: :post, kind: :post,
content: content:
"<article class=\"post-template\"><h1>{{ post.title }}</h1><div class=\"body\">{{ post.content }}</div></article>" "<article class=\"post-template\" data-pagefind-body><h1>{{ post.title }}</h1><div class=\"body\">{{ post.content }}</div></article>"
}) })
assert {:ok, published_post_template} = BDS.Templates.publish_template(post_template.id) assert {:ok, published_post_template} = BDS.Templates.publish_template(post_template.id)

View File

@@ -61,8 +61,8 @@ defmodule BDS.GitTest do
"git", ["rev-parse", "--abbrev-ref", "HEAD"], _opts -> "git", ["rev-parse", "--abbrev-ref", "HEAD"], _opts ->
{"main\n", 0} {"main\n", 0}
"git", ["log", "--format=%H%x09%s", "main"], _opts -> "git", ["log", "--date=short", "--format=%H%x09%an%x09%ad%x09%s", "main"], _opts ->
{"a1\tLocal commit\nb2\tShared commit\n", 0} {"a1\tAda\t2026-01-01\tLocal commit\nb2\tBabbage\t2026-01-02\tShared commit\n", 0}
"git", ["log", "--format=%H", "origin/main"], _opts -> "git", ["log", "--format=%H", "origin/main"], _opts ->
{"b2\nc3\n", 0} {"b2\nc3\n", 0}
@@ -83,7 +83,11 @@ defmodule BDS.GitTest do
assert diff.unstaged_diff == "unstaged diff" assert diff.unstaged_diff == "unstaged diff"
assert {:ok, history} = Git.history(project.id, "main", runner: runner) assert {:ok, history} = Git.history(project.id, "main", runner: runner)
assert Enum.find(history.commits, &(&1.hash == "a1")).sync_status.kind == :local_only a1 = Enum.find(history.commits, &(&1.hash == "a1"))
assert a1.sync_status.kind == :local_only
assert a1.author == "Ada"
assert a1.date == "2026-01-01"
assert a1.subject == "Local commit"
assert Enum.find(history.commits, &(&1.hash == "b2")).sync_status.kind == :both assert Enum.find(history.commits, &(&1.hash == "b2")).sync_status.kind == :both
assert Enum.find(history.commits, &(&1.hash == "c3")).sync_status.kind == :remote_only assert Enum.find(history.commits, &(&1.hash == "c3")).sync_status.kind == :remote_only

View File

@@ -395,6 +395,8 @@ defmodule BDS.MaintenanceTest do
assert {:ok, _indexed} = BDS.Embeddings.index_unindexed(project.id) assert {:ok, _indexed} = BDS.Embeddings.index_unindexed(project.id)
index_path = BDS.Embeddings.index_path(project.id) index_path = BDS.Embeddings.index_path(project.id)
# Index persistence is debounced (5s); force it to assert the file.
:ok = BDS.Embeddings.Index.flush(project.id)
assert File.exists?(index_path) assert File.exists?(index_path)
Repo.delete_all(from key in BDS.Embeddings.Key, where: key.project_id == ^project.id) Repo.delete_all(from key in BDS.Embeddings.Key, where: key.project_id == ^project.id)
@@ -416,6 +418,8 @@ defmodule BDS.MaintenanceTest do
assert post.id in rebuilt_post_ids assert post.id in rebuilt_post_ids
assert Repo.get_by(BDS.Embeddings.Key, project_id: project.id, post_id: post.id) != nil assert Repo.get_by(BDS.Embeddings.Key, project_id: project.id, post_id: post.id) != nil
:ok = BDS.Embeddings.Index.flush(project.id)
assert File.exists?(index_path) assert File.exists?(index_path)
end end

View File

@@ -236,6 +236,9 @@ defmodule BDS.MetadataTest do
assert metadata.semantic_similarity_enabled == true assert metadata.semantic_similarity_enabled == true
assert BDS.Repo.get_by(BDS.Embeddings.Key, project_id: project.id, post_id: post.id) != nil assert BDS.Repo.get_by(BDS.Embeddings.Key, project_id: project.id, post_id: post.id) != nil
# Index persistence is debounced (5s); force it to assert the file.
:ok = BDS.Embeddings.Index.flush(project.id)
assert File.exists?(BDS.Embeddings.index_path(project.id)) assert File.exists?(BDS.Embeddings.index_path(project.id))
end end

View File

@@ -706,6 +706,44 @@ defmodule BDS.PreviewTest do
assert :ok = BDS.Preview.stop_preview(project.id) assert :ok = BDS.Preview.stop_preview(project.id)
end end
test "stop_preview drains an inflight request before completing", %{project: project} do
assert {:ok, _metadata} =
Metadata.update_project_metadata(project.id, %{
main_language: "en",
blog_languages: ["en"]
})
assert {:ok, server} = BDS.Preview.start_preview(project.id)
# Open a raw connection and hold it open without sending the request line,
# so the server has a request task blocked in recv (an inflight request).
{:ok, socket} =
:gen_tcp.connect(to_charlist(server.host), server.port, [
:binary,
packet: :raw,
active: false
])
# Give the acceptor time to accept the connection and register the task.
Process.sleep(100)
test_pid = self()
spawn(fn ->
send(test_pid, {:stopped, BDS.Preview.stop_preview(project.id)})
end)
# Shutdown must not complete while the request is still inflight.
refute_receive {:stopped, _}, 300
# Completing the request lets the server drain and finish shutting down.
:ok = :gen_tcp.send(socket, "GET / HTTP/1.1\r\nhost: localhost\r\n\r\n")
assert_receive {:stopped, :ok}, 2_000
:gen_tcp.close(socket)
end
test "preview query params can override the rendered theme for generated and draft pages", %{ test "preview query params can override the rendered theme for generated and draft pages", %{
project: project project: project
} do } do

View File

@@ -126,6 +126,35 @@ defmodule BDS.ScriptsTest do
refute File.exists?(Path.join(temp_dir, published.file_path)) refute File.exists?(Path.join(temp_dir, published.file_path))
end end
test "publish_script rejects invalid Lua syntax", %{project: project} do
assert {:ok, script} =
BDS.Scripts.create_script(%{
project_id: project.id,
title: "Bad Script",
kind: :utility,
content: "function main( missing end"
})
assert {:error, {:invalid_script, _reason}} = BDS.Scripts.publish_script(script.id)
reloaded = Repo.get!(Script, script.id)
assert reloaded.status == :draft
assert reloaded.content == "function main( missing end"
end
test "publish_script allows valid Lua syntax", %{project: project} do
assert {:ok, script} =
BDS.Scripts.create_script(%{
project_id: project.id,
title: "Good Script",
kind: :utility,
content: "function main() return 42 end"
})
assert {:ok, published} = BDS.Scripts.publish_script(script.id)
assert published.status == :published
end
test "rebuild_scripts_from_files recreates published scripts from disk", %{ test "rebuild_scripts_from_files recreates published scripts from disk", %{
project: project, project: project,
temp_dir: temp_dir temp_dir: temp_dir

View File

@@ -229,6 +229,13 @@ defmodule BDS.TagsTest do
] = Jason.decode!(File.read!(tags_path)) ] = Jason.decode!(File.read!(tags_path))
end end
test "colour_presets contains exactly 17 unique valid hex colours" do
presets = BDS.Desktop.ShellLive.TagsEditor.colour_presets()
assert length(presets) == 17
assert length(Enum.uniq(presets)) == 17
assert Enum.all?(presets, &Regex.match?(~r/^#[0-9a-fA-F]{6}$/, &1))
end
defp errors_on(changeset) do defp errors_on(changeset) do
Ecto.Changeset.traverse_errors(changeset, fn {message, opts} -> Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
Regex.replace(~r"%{(\w+)}", message, fn _, key -> Regex.replace(~r"%{(\w+)}", message, fn _, key ->

View File

@@ -29,7 +29,7 @@ defmodule BDS.TemplatesTest do
assert template.status == :draft assert template.status == :draft
assert template.enabled == true assert template.enabled == true
assert template.version == 1 assert template.version == 1
assert template.file_path == "" assert template.file_path == "templates/article-view.liquid"
assert template.content == "<article>{{ content }}</article>" assert template.content == "<article>{{ content }}</article>"
assert {:ok, duplicate} = assert {:ok, duplicate} =
@@ -43,6 +43,28 @@ defmodule BDS.TemplatesTest do
assert duplicate.slug == "article-view-2" assert duplicate.slug == "article-view-2"
end end
test "create_template writes template file to disk", %{project: project, temp_dir: temp_dir} do
assert {:ok, template} =
BDS.Templates.create_template(%{
project_id: project.id,
title: "My Layout",
kind: :post,
content: "<main>{{ content }}</main>"
})
assert template.file_path == "templates/my-layout.liquid"
full_path = Path.join(temp_dir, template.file_path)
assert File.exists?(full_path)
contents = File.read!(full_path)
assert contents =~ "---\nid: #{template.id}\n"
assert contents =~ "slug: my-layout\n"
assert contents =~ "title: My Layout\n"
assert contents =~ "kind: post\n"
assert contents =~ "\n---\n<main>{{ content }}</main>\n"
end
test "publish_template writes a liquid file with frontmatter and clears draft content", %{ test "publish_template writes a liquid file with frontmatter and clears draft content", %{
project: project, project: project,
temp_dir: temp_dir temp_dir: temp_dir
@@ -264,6 +286,35 @@ defmodule BDS.TemplatesTest do
assert reloaded_tag.post_template_slug == "feature-view" assert reloaded_tag.post_template_slug == "feature-view"
end end
test "publish_template rejects invalid Liquid syntax", %{project: project} do
assert {:ok, template} =
BDS.Templates.create_template(%{
project_id: project.id,
title: "Bad Template",
kind: :post,
content: "{% for item in items %}unclosed"
})
assert {:error, {:invalid_liquid, _reason}} = BDS.Templates.publish_template(template.id)
reloaded = Repo.get!(BDS.Templates.Template, template.id)
assert reloaded.status == :draft
assert reloaded.content == "{% for item in items %}unclosed"
end
test "publish_template allows valid Liquid syntax", %{project: project} do
assert {:ok, template} =
BDS.Templates.create_template(%{
project_id: project.id,
title: "Good Template",
kind: :post,
content: "{% for item in items %}{{ item }}{% endfor %}"
})
assert {:ok, published} = BDS.Templates.publish_template(template.id)
assert published.status == :published
end
test "rebuild_templates_from_files recreates published templates from disk", %{ test "rebuild_templates_from_files recreates published templates from disk", %{
project: project, project: project,
temp_dir: temp_dir temp_dir: temp_dir