diff --git a/lib/pleroma/akkoma/translators/deepl.ex b/lib/pleroma/akkoma/translators/deepl.ex index 881a7c71a..f93fb7e59 100644 --- a/lib/pleroma/akkoma/translators/deepl.ex +++ b/lib/pleroma/akkoma/translators/deepl.ex @@ -40,8 +40,9 @@ def languages do end @impl Pleroma.Akkoma.Translator - def translate(string, to_language) do - with {:ok, %{status: 200} = response} <- do_request(api_key(), tier(), string, to_language), + def translate(string, from_language, to_language) do + with {:ok, %{status: 200} = response} <- + do_request(api_key(), tier(), string, from_language, to_language), {:ok, body} <- Jason.decode(response.body) do %{"translations" => [%{"text" => translated, "detected_source_language" => detected}]} = body @@ -57,7 +58,7 @@ def translate(string, to_language) do end end - defp do_request(api_key, tier, string, to_language) do + defp do_request(api_key, tier, string, from_language, to_language) do HTTP.post( base_url(tier) <> "translate", URI.encode_query( @@ -65,7 +66,8 @@ defp do_request(api_key, tier, string, to_language) do text: string, target_lang: to_language, tag_handling: "html" - }, + } + |> maybe_add_source(from_language), :rfc3986 ), [ @@ -75,6 +77,9 @@ defp do_request(api_key, tier, string, to_language) do ) end + defp maybe_add_source(opts, nil), do: opts + defp maybe_add_source(opts, lang), do: Map.put(opts, :source_lang, lang) + defp do_languages() do HTTP.get( base_url(tier()) <> "languages?type=target", diff --git a/lib/pleroma/akkoma/translators/libre_translate.ex b/lib/pleroma/akkoma/translators/libre_translate.ex index f0a431932..319907c2f 100644 --- a/lib/pleroma/akkoma/translators/libre_translate.ex +++ b/lib/pleroma/akkoma/translators/libre_translate.ex @@ -30,10 +30,17 @@ def languages do end @impl Pleroma.Akkoma.Translator - def translate(string, to_language) do - with {:ok, %{status: 200} = response} <- do_request(string, to_language), + def translate(string, from_language, to_language) do + with {:ok, %{status: 200} = response} <- do_request(string, from_language, to_language), {:ok, body} <- Jason.decode(response.body) do - %{"translatedText" => translated, "detectedLanguage" => %{"language" => detected}} = body + %{"translatedText" => translated} = body + + detected = + if Map.has_key?(body, "detectedLanguage") do + get_in(body, ["detectedLanguage", "language"]) + else + from_language + end {:ok, detected, translated} else @@ -46,7 +53,7 @@ def translate(string, to_language) do end end - defp do_request(string, to_language) do + defp do_request(string, from_language, to_language) do url = URI.parse(url()) url = %{url | path: "/translate"} @@ -54,7 +61,7 @@ defp do_request(string, to_language) do to_string(url), Jason.encode!(%{ q: string, - source: "auto", + source: if(is_nil(from_language), do: "auto", else: from_language), target: to_language, format: "html", api_key: api_key() diff --git a/lib/pleroma/akkoma/translators/translator.ex b/lib/pleroma/akkoma/translators/translator.ex index aa1b1a8cc..aa49b0655 100644 --- a/lib/pleroma/akkoma/translators/translator.ex +++ b/lib/pleroma/akkoma/translators/translator.ex @@ -1,4 +1,5 @@ defmodule Pleroma.Akkoma.Translator do - @callback translate(String.t(), String.t()) :: {:ok, String.t(), String.t()} | {:error, any()} + @callback translate(String.t(), String.t() | nil, String.t()) :: + {:ok, String.t(), String.t()} | {:error, any()} @callback languages() :: {:ok, [%{name: String.t(), code: String.t()}]} | {:error, any()} end diff --git a/lib/pleroma/web/api_spec/operations/status_operation.ex b/lib/pleroma/web/api_spec/operations/status_operation.ex index 04a7bf5db..5332c9dca 100644 --- a/lib/pleroma/web/api_spec/operations/status_operation.ex +++ b/lib/pleroma/web/api_spec/operations/status_operation.ex @@ -413,7 +413,7 @@ def translate_operation do description: "View the translation of a given status", operationId: "StatusController.translation", security: [%{"oAuth" => ["read:statuses"]}], - parameters: [id_param(), language_param()], + parameters: [id_param(), language_param(), source_language_param()], responses: %{ 200 => Operation.response("Translation", "application/json", translation()), 400 => Operation.response("Error", "application/json", ApiError), @@ -572,6 +572,10 @@ defp language_param do Operation.parameter(:language, :path, :string, "ISO 639 language code", example: "en") end + defp source_language_param do + Operation.parameter(:from, :query, :string, "ISO 639 language code", example: "en") + end + defp status_response do Operation.response("Status", "application/json", Status) end diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex index 09e7daf19..41fbd7acf 100644 --- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@ -422,7 +422,7 @@ def bookmarks(%{assigns: %{user: user}} = conn, params) do end @doc "GET /api/v1/statuses/:id/translations/:language" - def translate(%{assigns: %{user: user}} = conn, %{id: id, language: language}) do + def translate(%{assigns: %{user: user}} = conn, %{id: id, language: language} = params) do with {:enabled, true} <- {:enabled, Config.get([:translator, :enabled])}, %Activity{} = activity <- Activity.get_by_id_with_object(id), {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)}, @@ -431,6 +431,7 @@ def translate(%{assigns: %{user: user}} = conn, %{id: id, language: language}) d fetch_or_translate( activity.id, activity.object.data["content"], + Map.get(params, :from, nil), language, translation_module ) do @@ -449,16 +450,20 @@ def translate(%{assigns: %{user: user}} = conn, %{id: id, language: language}) d end end - defp fetch_or_translate(status_id, text, language, translation_module) do - @cachex.fetch!(:translations_cache, "translations:#{status_id}:#{language}", fn _ -> - value = translation_module.translate(text, language) + defp fetch_or_translate(status_id, text, source_language, target_language, translation_module) do + @cachex.fetch!( + :translations_cache, + "translations:#{status_id}:#{source_language}:#{target_language}", + fn _ -> + value = translation_module.translate(text, source_language, target_language) - with {:ok, _, _} <- value do - value - else - _ -> {:ignore, value} + with {:ok, _, _} <- value do + value + else + _ -> {:ignore, value} + end end - end) + ) end defp put_application(params, %{assigns: %{token: %Token{user: %User{} = user} = token}} = _conn) do diff --git a/test/pleroma/translators/deepl_test.exs b/test/pleroma/translators/deepl_test.exs index 4323c8355..58f23fe26 100644 --- a/test/pleroma/translators/deepl_test.exs +++ b/test/pleroma/translators/deepl_test.exs @@ -60,7 +60,7 @@ test "should work with the free tier" do } end) - assert {:ok, "ja", "I will crush you"} = DeepL.translate("ギュギュ握りつぶしちゃうぞ", "en") + assert {:ok, "ja", "I will crush you"} = DeepL.translate("ギュギュ握りつぶしちゃうぞ", nil, "en") end test "should work with the pro tier" do @@ -85,7 +85,33 @@ test "should work with the pro tier" do } end) - assert {:ok, "ja", "I will crush you"} = DeepL.translate("ギュギュ握りつぶしちゃうぞ", "en") + assert {:ok, "ja", "I will crush you"} = DeepL.translate("ギュギュ握りつぶしちゃうぞ", nil, "en") + end + + test "should assign source language if set" do + clear_config([:deepl, :tier], :pro) + + Tesla.Mock.mock(fn + %{method: :post, url: "https://api.deepl.com/v2/translate"} = env -> + auth_header = Enum.find(env.headers, fn {k, _v} -> k == "authorization" end) + assert {"authorization", "DeepL-Auth-Key deepl_api_key"} = auth_header + assert String.contains?(env.body, "source_lang=ja") + + %Tesla.Env{ + status: 200, + body: + Jason.encode!(%{ + translations: [ + %{ + "text" => "I will crush you", + "detected_source_language" => "ja" + } + ] + }) + } + end) + + assert {:ok, "ja", "I will crush you"} = DeepL.translate("ギュギュ握りつぶしちゃうぞ", "ja", "en") end test "should gracefully fail if the API errors" do @@ -99,7 +125,8 @@ test "should gracefully fail if the API errors" do } end) - assert {:error, "DeepL request failed (code 403)"} = DeepL.translate("ギュギュ握りつぶしちゃうぞ", "en") + assert {:error, "DeepL request failed (code 403)"} = + DeepL.translate("ギュギュ握りつぶしちゃうぞ", nil, "en") end end end diff --git a/test/pleroma/translators/libre_translate_test.exs b/test/pleroma/translators/libre_translate_test.exs index ee71eef35..d28d9278a 100644 --- a/test/pleroma/translators/libre_translate_test.exs +++ b/test/pleroma/translators/libre_translate_test.exs @@ -36,7 +36,7 @@ test "should list supported languages" do test "should work without an API key" do Tesla.Mock.mock(fn %{method: :post, url: "http://libre.translate/translate"} = env -> - assert {:ok, %{"api_key" => nil}} = Jason.decode(env.body) + assert {:ok, %{"api_key" => nil, "source" => "auto"}} = Jason.decode(env.body) %Tesla.Env{ status: 200, @@ -51,7 +51,8 @@ test "should work without an API key" do } end) - assert {:ok, "ja", "I will crush you"} = LibreTranslate.translate("ギュギュ握りつぶしちゃうぞ", "en") + assert {:ok, "ja", "I will crush you"} = + LibreTranslate.translate("ギュギュ握りつぶしちゃうぞ", nil, "en") end test "should work with an API key" do @@ -74,7 +75,8 @@ test "should work with an API key" do } end) - assert {:ok, "ja", "I will crush you"} = LibreTranslate.translate("ギュギュ握りつぶしちゃうぞ", "en") + assert {:ok, "ja", "I will crush you"} = + LibreTranslate.translate("ギュギュ握りつぶしちゃうぞ", nil, "en") end test "should gracefully handle API key errors" do @@ -92,7 +94,25 @@ test "should gracefully handle API key errors" do end) assert {:error, "libre_translate: request failed (code 403)"} = - LibreTranslate.translate("ギュギュ握りつぶしちゃうぞ", "en") + LibreTranslate.translate("ギュギュ握りつぶしちゃうぞ", nil, "en") + end + + test "should set a source language if requested" do + Tesla.Mock.mock(fn + %{method: :post, url: "http://libre.translate/translate"} = env -> + assert {:ok, %{"api_key" => nil, "source" => "ja"}} = Jason.decode(env.body) + + %Tesla.Env{ + status: 200, + body: + Jason.encode!(%{ + translatedText: "I will crush you" + }) + } + end) + + assert {:ok, "ja", "I will crush you"} = + LibreTranslate.translate("ギュギュ握りつぶしちゃうぞ", "ja", "en") end test "should gracefully handle an unsupported language" do @@ -110,7 +130,7 @@ test "should gracefully handle an unsupported language" do end) assert {:error, "libre_translate: request failed (code 400)"} = - LibreTranslate.translate("ギュギュ握りつぶしちゃうぞ", "zoop") + LibreTranslate.translate("ギュギュ握りつぶしちゃうぞ", nil, "zoop") end end end