Automatic status translation #187
|
@ -3352,9 +3352,9 @@ config :pleroma, :config_description, [
|
|||
},
|
||||
%{
|
||||
group: :pleroma,
|
||||
key: :deepl,
|
||||
key: :translator,
|
||||
type: :group,
|
||||
description: "DeepL settings.",
|
||||
description: "Translation Settings",
|
||||
children: [
|
||||
%{
|
||||
key: :enabled,
|
||||
|
@ -3362,16 +3362,51 @@ config :pleroma, :config_description, [
|
|||
description: "Is translation enabled?",
|
||||
suggestion: [true, false]
|
||||
},
|
||||
%{
|
||||
key: :module,
|
||||
type: :module,
|
||||
description: "Translation module.",
|
||||
suggestions: {:list_behaviour_implementations, Pleroma.Akkoma.Translator}
|
||||
}
|
||||
]
|
||||
},
|
||||
%{
|
||||
group: :pleroma,
|
||||
key: :deepl,
|
||||
label: "DeepL",
|
||||
type: :group,
|
||||
description: "DeepL Settings.",
|
||||
children: [
|
||||
%{
|
||||
key: :tier,
|
||||
type: :atom,
|
||||
type: {:dropdown, :atom},
|
||||
description: "API Tier",
|
||||
suggestion: [:free, :pro]
|
||||
suggestions: [:free, :pro]
|
||||
},
|
||||
%{
|
||||
key: :api_key,
|
||||
type: :string,
|
||||
description: "API key for DeepL",
|
||||
suggestions: [nil]
|
||||
}
|
||||
]
|
||||
},
|
||||
%{
|
||||
group: :pleroma,
|
||||
key: :libre_translate,
|
||||
type: :group,
|
||||
description: "LibreTranslate Settings.",
|
||||
children: [
|
||||
%{
|
||||
key: :url,
|
||||
type: :string,
|
||||
description: "URL for libretranslate",
|
||||
suggestion: [nil]
|
||||
},
|
||||
%{
|
||||
key: :api_key,
|
||||
type: :string,
|
||||
description: "API key for libretranslate",
|
||||
suggestion: [nil]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
defmodule Pleroma.Akkoma.Translators.DeepL do
|
||||
@behaviour Pleroma.Akkoma.Translator
|
||||
|
||||
alias Pleroma.HTTP
|
||||
alias Pleroma.Config
|
||||
require Logger
|
||||
|
||||
defp base_url(:free) do
|
||||
"https://api-free.deepl.com/v2/"
|
||||
end
|
||||
|
||||
defp base_url(:pro) do
|
||||
"https://api.deepl.com/v2/"
|
||||
end
|
||||
|
||||
defp api_key do
|
||||
Config.get([:deepl, :api_key])
|
||||
end
|
||||
|
||||
defp tier do
|
||||
Config.get([:deepl, :tier])
|
||||
end
|
||||
|
||||
@impl Pleroma.Akkoma.Translator
|
||||
def translate(string, to_language) do
|
||||
with {:ok, %{status: 200} = response} <- do_request(api_key(), tier(), string, to_language),
|
||||
{:ok, body} <- Jason.decode(response.body) do
|
||||
%{"translations" => [%{"text" => translated, "detected_source_language" => detected}]} =
|
||||
body
|
||||
|
||||
{:ok, detected, translated}
|
||||
else
|
||||
{:ok, %{status: 403} = response} ->
|
||||
Logger.warning("DeepL: Request rejected, please check your API key: #{inspect(response)}")
|
||||
{:error, "DeepL request failed"}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_request(api_key, tier, string, to_language) do
|
||||
HTTP.post(
|
||||
base_url(tier) <> "translate",
|
||||
URI.encode_query(
|
||||
%{
|
||||
text: string,
|
||||
target_lang: to_language
|
||||
},
|
||||
:rfc3986
|
||||
),
|
||||
[
|
||||
{"authorization", "DeepL-Auth-Key #{api_key}"},
|
||||
{"content-type", "application/x-www-form-urlencoded"}
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,51 @@
|
|||
defmodule Pleroma.Akkoma.Translators.LibreTranslate do
|
||||
@behaviour Pleroma.Akkoma.Translator
|
||||
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.HTTP
|
||||
require Logger
|
||||
|
||||
defp api_key do
|
||||
Config.get([:libre_translate, :api_key])
|
||||
end
|
||||
|
||||
defp url do
|
||||
Config.get([:libre_translate, :url])
|
||||
end
|
||||
|
||||
@impl Pleroma.Akkoma.Translator
|
||||
def translate(string, to_language) do
|
||||
with {:ok, %{status: 200} = response} <- do_request(string, to_language),
|
||||
{:ok, body} <- Jason.decode(response.body) do
|
||||
%{"translatedText" => translated, "detectedLanguage" => %{"language" => detected}} = body
|
||||
|
||||
{:ok, detected, translated}
|
||||
else
|
||||
{:ok, %{status: 403} = response} ->
|
||||
Logger.warning("DeepL: Request rejected, please check your API key: #{inspect(response)}")
|
||||
{:error, "libre_translate: request failed"}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_request(string, to_language) do
|
||||
url = URI.parse(url())
|
||||
url = %{url | path: "/translate"}
|
||||
|
||||
HTTP.post(
|
||||
to_string(url),
|
||||
Jason.encode!(%{
|
||||
q: string,
|
||||
source: "auto",
|
||||
target: to_language,
|
||||
format: "html",
|
||||
api_key: api_key()
|
||||
}),
|
||||
[
|
||||
{"content-type", "application/json"}
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
|
@ -1,3 +1,3 @@
|
|||
defmodule Akkoma.Translator do
|
||||
defmodule Pleroma.Akkoma.Translator do
|
||||
@callback translate(String.t(), String.t()) :: {:ok, String.t(), String.t()} | {:error, any()}
|
||||
end
|
|
@ -1,45 +0,0 @@
|
|||
defmodule Akkoma.Translators.DeepL do
|
||||
@behaviour Akkoma.Translator
|
||||
|
||||
use Tesla
|
||||
alias Pleroma.Config
|
||||
|
||||
plug(Tesla.Middleware.EncodeFormUrlencoded)
|
||||
plug(Tesla.Middleware.DecodeJson)
|
||||
|
||||
defp base_url(:free) do
|
||||
"https://api-free.deepl.com/v2/"
|
||||
end
|
||||
|
||||
defp base_url(:pro) do
|
||||
"https://api.deepl.com/v2/"
|
||||
end
|
||||
|
||||
defp api_key do
|
||||
Config.get([:deepl, :api_key])
|
||||
end
|
||||
|
||||
defp tier do
|
||||
Config.get([:deepl, :tier])
|
||||
end
|
||||
|
||||
@impl Akkoma.Translator
|
||||
def translate(string, to_language) do
|
||||
with {:ok, response} <- do_request(api_key(), tier(), string, to_language) do
|
||||
%{"translations" => [%{"text" => translated, "detected_source_language" => detected}]} =
|
||||
response.body
|
||||
|
||||
{:ok, detected, translated}
|
||||
else
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_request(api_key, tier, string, to_language) do
|
||||
post(base_url(tier) <> "translate", %{
|
||||
auth_key: api_key,
|
||||
text: string,
|
||||
target_lang: to_language
|
||||
})
|
||||
end
|
||||
end
|
|
@ -1,40 +0,0 @@
|
|||
defmodule Akkoma.Translators.LibreTranslate do
|
||||
@behaviour Akkoma.Translator
|
||||
|
||||
use Tesla
|
||||
alias Pleroma.Config
|
||||
|
||||
plug(Tesla.Middleware.JSON)
|
||||
|
||||
defp api_key do
|
||||
Config.get([:libre_translate, :api_key])
|
||||
end
|
||||
|
||||
defp url do
|
||||
Config.get([:libre_translate, :url])
|
||||
end
|
||||
|
||||
@impl Akkoma.Translator
|
||||
def translate(string, to_language) do
|
||||
with {:ok, response} <- do_request(string, to_language) do
|
||||
%{"translatedText" => translated, "detectedLanguage" => %{"language" => detected}} =
|
||||
response.body
|
||||
|
||||
{:ok, detected, translated}
|
||||
else
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_request(string, to_language) do
|
||||
url = URI.parse(url())
|
||||
url = %{url | path: "/translate"}
|
||||
|
||||
post(url, %{
|
||||
q: string,
|
||||
source: "auto",
|
||||
target: to_language,
|
||||
api_key: api_key()
|
||||
})
|
||||
end
|
||||
end
|
|
@ -428,18 +428,18 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
|
|||
translation_module <- Config.get([:translator, :module]),
|
||||
{:ok, detected, translation} <-
|
||||
translation_module.translate(activity.object.data["content"], language) do
|
||||
json(conn, %{detected_lanugage: detected, text: translation})
|
||||
json(conn, %{detected_language: detected, text: translation})
|
||||
else
|
||||
{:enabled, false} ->
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{"error" => "DeepL is not enabled"})
|
||||
|> json(%{"error" => "Translation is not enabled"})
|
||||
|
||||
{:visible, false} ->
|
||||
{:error, :not_found}
|
||||
|
||||
_e ->
|
||||
{:error, :internal_server_error}
|
||||
e ->
|
||||
e
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
defmodule Pleroma.Akkoma.Translators.DeepLTest do
|
||||
use Pleroma.DataCase, async: true
|
||||
|
||||
alias Pleroma.Akkoma.Translators.DeepL
|
||||
|
||||
describe "translating with deepl" do
|
||||
setup do
|
||||
clear_config([:deepl, :api_key], "deepl_api_key")
|
||||
end
|
||||
|
||||
test "should work with the free tier" do
|
||||
clear_config([:deepl, :tier], :free)
|
||||
|
||||
Tesla.Mock.mock(fn
|
||||
%{method: :post, url: "https://api-free.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
|
||||
|
||||
%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("ギュギュ握りつぶしちゃうぞ", "en")
|
||||
end
|
||||
|
||||
test "should work with the pro tier" 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
|
||||
|
||||
%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("ギュギュ握りつぶしちゃうぞ", "en")
|
||||
end
|
||||
|
||||
test "should gracefully fail if the API errors" do
|
||||
clear_config([:deepl, :tier], :free)
|
||||
|
||||
Tesla.Mock.mock(fn
|
||||
%{method: :post, url: "https://api-free.deepl.com/v2/translate"} ->
|
||||
%Tesla.Env{
|
||||
status: 403,
|
||||
body: ""
|
||||
}
|
||||
end)
|
||||
|
||||
assert {:error, "DeepL request failed"} = DeepL.translate("ギュギュ握りつぶしちゃうぞ", "en")
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,73 @@
|
|||
defmodule Pleroma.Akkoma.Translators.LibreTranslateTest do
|
||||
use Pleroma.DataCase, async: true
|
||||
|
||||
alias Pleroma.Akkoma.Translators.LibreTranslate
|
||||
|
||||
describe "translating with libre translate" do
|
||||
setup do
|
||||
clear_config([:libre_translate, :url], "http://libre.translate/translate")
|
||||
end
|
||||
|
||||
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)
|
||||
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
body:
|
||||
Jason.encode!(%{
|
||||
detectedLanguage: %{
|
||||
confidence: 83,
|
||||
language: "ja"
|
||||
},
|
||||
translatedText: "I will crush you"
|
||||
})
|
||||
}
|
||||
end)
|
||||
|
||||
assert {:ok, "ja", "I will crush you"} = LibreTranslate.translate("ギュギュ握りつぶしちゃうぞ", "en")
|
||||
end
|
||||
|
||||
test "should work with an API key" do
|
||||
clear_config([:libre_translate, :api_key], "libre_translate_api_key")
|
||||
|
||||
Tesla.Mock.mock(fn
|
||||
%{method: :post, url: "http://libre.translate/translate"} = env ->
|
||||
assert {:ok, %{"api_key" => "libre_translate_api_key"}} = Jason.decode(env.body)
|
||||
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
body:
|
||||
Jason.encode!(%{
|
||||
detectedLanguage: %{
|
||||
confidence: 83,
|
||||
language: "ja"
|
||||
},
|
||||
translatedText: "I will crush you"
|
||||
})
|
||||
}
|
||||
end)
|
||||
|
||||
assert {:ok, "ja", "I will crush you"} = LibreTranslate.translate("ギュギュ握りつぶしちゃうぞ", "en")
|
||||
end
|
||||
|
||||
test "should gracefully handle API key errors" do
|
||||
clear_config([:libre_translate, :api_key], "")
|
||||
|
||||
Tesla.Mock.mock(fn
|
||||
%{method: :post, url: "http://libre.translate/translate"} ->
|
||||
%Tesla.Env{
|
||||
status: 403,
|
||||
body:
|
||||
Jason.encode!(%{
|
||||
error: "Please contact the server operator to obtain an API key"
|
||||
})
|
||||
}
|
||||
end)
|
||||
|
||||
assert {:error, "libre_translate: request failed"} =
|
||||
LibreTranslate.translate("ギュギュ握りつぶしちゃうぞ", "en")
|
||||
end
|
||||
end
|
||||
end
|
|
@ -2073,30 +2073,74 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
|
|||
end
|
||||
|
||||
describe "translating statuses" do
|
||||
setup do: oauth_access(["read:statuses"])
|
||||
setup do
|
||||
clear_config([:translator, :enabled], true)
|
||||
clear_config([:translator, :module], Pleroma.Akkoma.Translators.DeepL)
|
||||
clear_config([:deepl, :api_key], "deepl_api_key")
|
||||
oauth_access(["read:statuses"])
|
||||
end
|
||||
|
||||
test "should return text and detected language", %{conn: conn} do
|
||||
clear_config([:deepl, :tier], :free)
|
||||
|
||||
test "translating a status with deepl", %{conn: conn} do
|
||||
Tesla.Mock.mock(fn
|
||||
%{method: :post, url: "http://api-free.deepl.com/translate"} ->
|
||||
{:ok,
|
||||
%{
|
||||
status: 200,
|
||||
body:
|
||||
~s({"data": {"translations": [{"translatedText": "Tell me, for whom do you fight?"}]}})
|
||||
}}
|
||||
%{method: :post, url: "https://api-free.deepl.com/v2/translate"} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
body:
|
||||
Jason.encode!(%{
|
||||
translations: [
|
||||
%{
|
||||
"text" => "Tell me, for whom do you fight?",
|
||||
"detected_source_language" => "ja"
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
end)
|
||||
|
||||
user = insert(:user)
|
||||
{:ok, quoted_status} = CommonAPI.post(user, %{status: "何のために闘う?"})
|
||||
{:ok, to_translate} = CommonAPI.post(user, %{status: "何のために闘う?"})
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> get("/api/v1/statuses/#{quoted_status.id}/translations/en")
|
||||
|> get("/api/v1/statuses/#{to_translate.id}/translations/en")
|
||||
|
||||
response = json_response_and_validate_schema(conn, 200)
|
||||
|
||||
assert response["text"] == "Tell me, for whom do you fight?"
|
||||
assert response["detected_language"] == "ja"
|
||||
end
|
||||
|
||||
test "should not allow translating of statuses you cannot see", %{conn: conn} do
|
||||
clear_config([:deepl, :tier], :free)
|
||||
|
||||
Tesla.Mock.mock(fn
|
||||
%{method: :post, url: "https://api-free.deepl.com/v2/translate"} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
body:
|
||||
Jason.encode!(%{
|
||||
translations: [
|
||||
%{
|
||||
"text" => "Tell me, for whom do you fight?",
|
||||
"detected_source_language" => "ja"
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
end)
|
||||
|
||||
user = insert(:user)
|
||||
{:ok, to_translate} = CommonAPI.post(user, %{status: "何のために闘う?", visibility: "private"})
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> get("/api/v1/statuses/#{to_translate.id}/translations/en")
|
||||
|
||||
json_response_and_validate_schema(conn, 404)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue