fix: issue #2 auto-translate only on manual save now

This commit is contained in:
2026-07-05 19:22:44 +02:00
parent d85654d4b2
commit c735c1afa7
6 changed files with 125 additions and 44 deletions

View File

@@ -503,7 +503,9 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
active_language = socket.assigns.active_language
draft = component_current_draft(socket, post, metadata, active_language)
case persist(post, draft, active_language, metadata, :save) do
action = if keep_draft?, do: :auto_save, else: :save
case persist(post, draft, active_language, metadata, action) do
{:ok, record} ->
refreshed_post = Posts.get_post!(post.id)
_refreshed_form = persisted_form(refreshed_post, metadata, active_language)

View File

@@ -17,7 +17,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor.Persistence do
canonical_language
) do
post
|> save_canonical_draft(draft)
|> save_canonical_draft(draft, action)
|> maybe_publish_post(post.id, action)
else
post.id
@@ -65,8 +65,10 @@ defmodule BDS.Desktop.ShellLive.PostEditor.Persistence do
else: dgettext("ui", "Delete this unpublished draft")
end
defp save_canonical_draft(%Post{id: post_id}, draft) do
Posts.update_post(post_id, %{
defp save_canonical_draft(%Post{id: post_id}, draft, action) do
Posts.update_post(
post_id,
%{
title: blank_to_nil(Map.get(draft, "title")),
excerpt: blank_to_nil(Map.get(draft, "excerpt")),
content: blank_to_nil(Map.get(draft, "content")),
@@ -76,7 +78,9 @@ defmodule BDS.Desktop.ShellLive.PostEditor.Persistence do
language: blank_to_nil(Map.get(draft, "language")),
do_not_translate: Map.get(draft, "do_not_translate", false),
template_slug: blank_to_nil(Map.get(draft, "template_slug"))
})
},
auto_translate: action != :auto_save
)
end
defp save_translation_draft(post_id, language, draft) do

View File

@@ -106,7 +106,6 @@ defmodule BDS.Posts do
{:ok, post} ->
:ok = Embeddings.sync_post(post)
:ok = Search.sync_post(post)
:ok = AutoTranslation.maybe_schedule(post)
{:ok, post}
error ->
@@ -114,9 +113,9 @@ defmodule BDS.Posts do
end
end
@spec update_post(String.t(), attrs()) ::
@spec update_post(String.t(), attrs(), keyword()) ::
{:ok, Post.t()} | {:error, :not_found | Ecto.Changeset.t()}
def update_post(post_id, attrs) do
def update_post(post_id, attrs, opts \\ []) do
case Repo.get(Post, post_id) do
nil ->
{:error, :not_found}
@@ -145,7 +144,13 @@ defmodule BDS.Posts do
:ok = Embeddings.sync_post(updated_post)
:ok = PostLinks.sync_post_links(updated_post)
:ok = Search.sync_post(updated_post)
# Auto-translation only runs on an explicit manual save from the
# editor, never on post creation or autosave.
if Keyword.get(opts, :auto_translate, false) do
:ok = AutoTranslation.maybe_schedule(updated_post)
end
{:ok, updated_post}
error ->

View File

@@ -231,6 +231,8 @@ rule PostTranslateAction {
rule PostAutoTranslateOnSave {
when: PostSaved(post_id)
-- Only an explicit manual save (or publish) triggers this. Auto-saves and
-- post creation (sidebar, deeplink/bookmarklet, import, scripting) never do.
-- Gate: airplane mode check + auto_translate not disabled (doNotTranslate=false)
-- For each configured blog language missing a translation:
-- Enqueue background translation task (title model)

View File

@@ -192,8 +192,9 @@ invariant FtsIncludesTranslations {
-- Distilled from: lib/bds/posts/auto_translation.ex
--
-- Two entry points share one translation primitive:
-- 1. ScheduleAutoTranslation - reactive, fired after a post is created or
-- updated. One background task per missing language produces a DRAFT
-- 1. ScheduleAutoTranslation - reactive, fired only by an explicit manual
-- save (or publish) in the post editor — never by post creation or
-- auto-save. One background task per missing language produces a DRAFT
-- translation, then cascades to the post's linked media.
-- 2. FillMissingTranslations - batch maintenance action. Scans every
-- published post, AUTO-PUBLISHES the generated translations, fills linked
@@ -210,7 +211,9 @@ surface AutoTranslationControlSurface {
facing _: TranslationOperator
provides:
-- Reactive trigger: emitted by post create/update side effects.
-- Reactive trigger: emitted only when a post is updated with
-- auto_translate enabled (the editor's manual save/publish path);
-- post creation and auto-save never emit it.
PostSavedForAutoTranslation(post)
-- Batch trigger: "fill missing translations" maintenance action.
FillMissingTranslationsRequested(project)

View File

@@ -143,7 +143,7 @@ defmodule BDS.PostTranslationsTest do
assert {:ok, []} = Posts.list_post_translations(post.id)
end
test "create_post enqueues and completes auto-translation tasks for missing project languages",
test "create_post does not enqueue auto-translation tasks",
%{project: project} do
configure_auto_translation_test_runtime()
@@ -162,21 +162,79 @@ defmodule BDS.PostTranslationsTest do
language: "en"
})
translations = wait_for_post_translations(post.id, ["de", "fr"])
assert Enum.map(translations, & &1.language) == ["de", "fr"]
assert Enum.all?(translations, &(&1.title == "Hallo Welt"))
assert Enum.all?(translations, &(&1.excerpt == "Kurze Zusammenfassung"))
assert Enum.all?(translations, &(&1.content == "# Hallo Welt\n\nUbersetzter Inhalt"))
tasks = wait_for_ai_tasks(2)
assert Enum.any?(tasks, &(&1.name == "Auto-translate Post to de" and &1.status == :completed))
assert Enum.any?(tasks, &(&1.name == "Auto-translate Post to fr" and &1.status == :completed))
assert Enum.all?(tasks, &(&1.group_name == "AI"))
assert ai_tasks() == []
assert {:ok, []} = Posts.list_post_translations(post.id)
end
test "update_post auto-translates missing languages and cascades linked media translations",
test "update_post without auto_translate does not enqueue auto-translation tasks",
%{project: project} do
configure_auto_translation_test_runtime()
assert {:ok, _metadata} =
Metadata.update_project_metadata(project.id, %{
main_language: "en",
blog_languages: ["en", "de"]
})
assert {:ok, post} =
Posts.create_post(%{
project_id: project.id,
title: "Autosaved",
content: "Source body",
language: "en"
})
assert {:ok, _updated_post} = Posts.update_post(post.id, %{content: "Autosaved body"})
assert ai_tasks() == []
assert {:ok, []} = Posts.list_post_translations(post.id)
end
test "editor persist schedules auto-translation on manual save but not on auto-save",
%{project: project} do
configure_auto_translation_test_runtime()
assert {:ok, metadata} =
Metadata.update_project_metadata(project.id, %{
main_language: "en",
blog_languages: ["en", "de"]
})
assert {:ok, post} =
Posts.create_post(%{
project_id: project.id,
title: "Editor Post",
content: "Source body",
language: "en"
})
draft = %{
"title" => "Editor Post",
"excerpt" => "",
"content" => "Edited body",
"tags" => "",
"categories" => "",
"author" => "",
"language" => "en",
"do_not_translate" => false,
"template_slug" => ""
}
alias BDS.Desktop.ShellLive.PostEditor.Persistence
assert {:ok, _post} = Persistence.persist(post, draft, "en", metadata, :auto_save)
assert ai_tasks() == []
assert {:ok, _post} = Persistence.persist(post, draft, "en", metadata, :save)
[translation] = wait_for_post_translations(post.id, ["de"])
assert translation.language == "de"
tasks = wait_for_ai_tasks(1)
assert Enum.any?(tasks, &(&1.name == "Auto-translate Post to de" and &1.status == :completed))
end
test "update_post with auto_translate translates missing languages and cascades linked media translations",
%{project: project, temp_dir: temp_dir} do
configure_auto_translation_test_runtime()
@@ -221,10 +279,14 @@ defmodule BDS.PostTranslationsTest do
])
assert {:ok, _updated_post} =
Posts.update_post(post.id, %{
Posts.update_post(
post.id,
%{
do_not_translate: false,
content: "Updated source body"
})
},
auto_translate: true
)
[translation] = wait_for_post_translations(post.id, ["de"])
assert translation.language == "de"
@@ -427,8 +489,6 @@ defmodule BDS.PostTranslationsTest do
end
end
defp wait_for_ai_tasks(count, attempts \\ 100)
test "do_not_translate guard prevents translation upsert via the API", %{
project: project
} do
@@ -453,14 +513,19 @@ defmodule BDS.PostTranslationsTest do
{"cannot add translations when do_not_translate is true", []}
end
defp ai_tasks do
BDS.Tasks.list_tasks()
|> Enum.filter(&(&1.group_name == "AI"))
end
defp wait_for_ai_tasks(count, attempts \\ 100)
defp wait_for_ai_tasks(_count, 0) do
flunk("AI tasks did not reach expected state")
end
defp wait_for_ai_tasks(count, attempts) do
tasks =
BDS.Tasks.list_tasks()
|> Enum.filter(&(&1.group_name == "AI"))
tasks = ai_tasks()
if length(tasks) >= count and Enum.all?(tasks, &(&1.status == :completed)) do
tasks