diff --git a/config/description.exs b/config/description.exs
index 6c13bde31..0efea0882 100644
--- a/config/description.exs
+++ b/config/description.exs
@@ -3523,5 +3523,18 @@ config :pleroma, :config_description, [
suggestion: [100_000]
}
]
+ },
+ %{
+ group: :pleroma,
+ key: Pleroma.Translation,
+ type: :group,
+ description: "Translation providers",
+ children: [
+ %{
+ key: Pleroma.Translation,
+ type: :service,
+ suggestions: [Pleroma.Translation.DeepL, Pleroma.Translation.LibreTranslate]
+ }
+ ]
}
]
diff --git a/lib/pleroma/translation.ex b/lib/pleroma/translation.ex
new file mode 100644
index 000000000..112f12cab
--- /dev/null
+++ b/lib/pleroma/translation.ex
@@ -0,0 +1,54 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Translation do
+ @cache_ttl 86_400_000
+ @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
+
+ def configured? do
+ service = get_service()
+
+ !!service and service.configured?
+ end
+
+ def translate(text, source_language, target_language) do
+ cache_key = get_cache_key(text, source_language, target_language)
+
+ case @cachex.get(:translations_cache, cache_key) do
+ {:ok, nil} ->
+ service = get_service()
+
+ result =
+ if !service or !service.configured? do
+ {:error, :not_found}
+ else
+ service.translate(text, source_language, target_language)
+ end
+
+ store_result(result, cache_key)
+
+ result
+
+ {:ok, result} ->
+ {:ok, result}
+
+ {:error, error} ->
+ {:error, error}
+ end
+ end
+
+ defp get_service, do: Pleroma.Config.get([__MODULE__, :service])
+
+ defp get_cache_key(text, source_language, target_language) do
+ "#{source_language}/#{target_language}/#{content_hash(text)}"
+ end
+
+ defp store_result({:ok, result}, cache_key) do
+ @cachex.put(:translations_cache, cache_key, result, ttl: @cache_ttl)
+ end
+
+ defp store_result(_, _), do: nil
+
+ defp content_hash(text), do: :crypto.hash(:sha256, text) |> Base.encode64()
+end
diff --git a/lib/pleroma/translation/deepl.ex b/lib/pleroma/translation/deepl.ex
new file mode 100644
index 000000000..76fff4693
--- /dev/null
+++ b/lib/pleroma/translation/deepl.ex
@@ -0,0 +1,75 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Translation.DeepL do
+ import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1]
+
+ alias Pleroma.Translation.Service
+
+ @behaviour Service
+
+ @impl Service
+ def configured? do
+ not_empty_string(get_plan()) and not_empty_string(get_api_key())
+ end
+
+ @impl Service
+ def translate(content, source_language, target_language) do
+ endpoint = endpoint_url()
+
+ case Pleroma.HTTP.post(
+ endpoint <>
+ "?" <>
+ URI.encode_query(%{
+ text: content,
+ source_lang: source_language |> String.upcase(),
+ target_lang: target_language,
+ tag_handling: "html"
+ }),
+ "",
+ [
+ {"Content-Type", "application/x-www-form-urlencoded"},
+ {"Authorization", "DeepL-Auth-Key #{get_api_key()}"}
+ ]
+ ) do
+ {:ok, %{status: 429}} ->
+ {:error, :too_many_requests}
+
+ {:ok, %{status: 456}} ->
+ {:error, :quota_exceeded}
+
+ {:ok, %{status: 200} = res} ->
+ %{
+ "translations" => [
+ %{"text" => content, "detected_source_language" => detected_source_language}
+ ]
+ } = Jason.decode!(res.body)
+
+ {:ok,
+ %{
+ content: content,
+ detected_source_language: detected_source_language,
+ provider: "DeepL"
+ }}
+
+ _ ->
+ {:error, :internal_server_error}
+ end
+ end
+
+ defp endpoint_url do
+ case get_plan() do
+ "free" -> "https://api-free.deepl.com/v2/translate"
+ _ -> "https://api.deepl.com/v2/translate"
+ end
+ end
+
+ defp get_plan do
+ Pleroma.Config.get([__MODULE__, :plan])
+ end
+
+ defp get_api_key do
+ Pleroma.Config.get([__MODULE__, :api_key])
+ end
+end
diff --git a/lib/pleroma/translation/libretranslate.ex b/lib/pleroma/translation/libretranslate.ex
new file mode 100644
index 000000000..049053d43
--- /dev/null
+++ b/lib/pleroma/translation/libretranslate.ex
@@ -0,0 +1,66 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Translation.LibreTranslate do
+ import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1]
+
+ alias Pleroma.Translation.Service
+
+ @behaviour Service
+
+ @impl Service
+ def configured?, do: not_empty_string(get_base_url())
+
+ @impl Service
+ def translate(content, source_language, target_language) do
+ endpoint = endpoint_url()
+
+ case Pleroma.HTTP.post(
+ endpoint,
+ Jason.encode!(%{
+ q: content,
+ source: source_language |> String.upcase(),
+ target: target_language,
+ format: "html",
+ api_key: get_api_key()
+ }),
+ [
+ {"Content-Type", "application/json"}
+ ]
+ ) do
+ {:ok, %{status: 429}} ->
+ {:error, :too_many_requests}
+
+ {:ok, %{status: 403}} ->
+ {:error, :quota_exceeded}
+
+ {:ok, %{status: 200} = res} ->
+ %{
+ "translatedText" => content
+ } = Jason.decode!(res.body)
+
+ {:ok,
+ %{
+ content: content,
+ detected_source_language: source_language,
+ provider: "LibreTranslate"
+ }}
+
+ _ ->
+ {:error, :internal_server_error}
+ end
+ end
+
+ defp endpoint_url do
+ get_base_url() <> "/translate"
+ end
+
+ defp get_base_url do
+ Pleroma.Config.get([__MODULE__, :base_url])
+ end
+
+ defp get_api_key do
+ Pleroma.Config.get([__MODULE__, :api_key], "")
+ end
+end
diff --git a/lib/pleroma/translation/service.ex b/lib/pleroma/translation/service.ex
new file mode 100644
index 000000000..55e995e92
--- /dev/null
+++ b/lib/pleroma/translation/service.ex
@@ -0,0 +1,20 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Translation.Service do
+ @callback configured?() :: boolean()
+
+ @callback translate(
+ content :: String.t(),
+ source_language :: String.t(),
+ target_language :: String.t()
+ ) ::
+ {:ok,
+ %{
+ content: String.t(),
+ detected_source_language: String.t(),
+ provider: String.t()
+ }}
+ | {:error, atom()}
+end
diff --git a/lib/pleroma/web/api_spec/operations/status_operation.ex b/lib/pleroma/web/api_spec/operations/status_operation.ex
index 961a2f402..00529bc47 100644
--- a/lib/pleroma/web/api_spec/operations/status_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/status_operation.ex
@@ -433,9 +433,9 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
required: false
),
responses: %{
- 200 => Operation.response("Translation", "application/json", translation())
- 400 => Operation.response("Error", "application/json", ApiError)
- 404 => Operation.response("Error", "application/json", ApiError)
+ 200 => Operation.response("Translation", "application/json", translation()),
+ 400 => Operation.response("Error", "application/json", ApiError),
+ 404 => Operation.response("Error", "application/json", ApiError),
503 => Operation.response("Error", "application/json", ApiError)
}
}
diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex
index e4c6c81e1..f48e80fd6 100644
--- a/lib/pleroma/web/mastodon_api/views/instance_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex
@@ -202,7 +202,8 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
},
vapid: %{
public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
- }
+ },
+ translation: %{enabled: Pleroma.Translation.configured?}
})
end