Automatic status translation (#187)
Fixes #115 Co-authored-by: FloatingGhost <hannah@coffee-and-dreams.uk> Reviewed-on: https://akkoma.dev/AkkomaGang/akkoma/pulls/187buildx
parent
722e56b308
commit
df39cab9c1
@ -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: status} = response} ->
|
||||||
|
Logger.warning("DeepL: Request rejected: #{inspect(response)}")
|
||||||
|
{:error, "DeepL request failed (code #{status})"}
|
||||||
|
|
||||||
|
{: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: status} = response} ->
|
||||||
|
Logger.warning("libre_translate: request failed, #{inspect(response)}")
|
||||||
|
{:error, "libre_translate: request failed (code #{status})"}
|
||||||
|
|
||||||
|
{: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
|
@ -0,0 +1,3 @@
|
|||||||
|
defmodule Pleroma.Akkoma.Translator do
|
||||||
|
@callback translate(String.t(), String.t()) :: {:ok, String.t(), String.t()} | {:error, any()}
|
||||||
|
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 (code 403)"} = DeepL.translate("ギュギュ握りつぶしちゃうぞ", "en")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,91 @@
|
|||||||
|
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 (code 403)"} =
|
||||||
|
LibreTranslate.translate("ギュギュ握りつぶしちゃうぞ", "en")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should gracefully handle an unsupported language" do
|
||||||
|
clear_config([:libre_translate, :api_key], "")
|
||||||
|
|
||||||
|
Tesla.Mock.mock(fn
|
||||||
|
%{method: :post, url: "http://libre.translate/translate"} ->
|
||||||
|
%Tesla.Env{
|
||||||
|
status: 400,
|
||||||
|
body:
|
||||||
|
Jason.encode!(%{
|
||||||
|
error: "zoop is not supported"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert {:error, "libre_translate: request failed (code 400)"} =
|
||||||
|
LibreTranslate.translate("ギュギュ握りつぶしちゃうぞ", "zoop")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in new issue