diff --git a/lib/pleroma/language/translation.ex b/lib/pleroma/language/translation.ex index c9cd9d2dd..05ab898f3 100644 --- a/lib/pleroma/language/translation.ex +++ b/lib/pleroma/language/translation.ex @@ -6,9 +6,9 @@ defmodule Pleroma.Language.Translation do @cachex Pleroma.Config.get([:cachex, :provider], Cachex) def configured? do - service = get_service() + provider = get_provider() - !!service and service.configured? + !!provider and provider.configured? end def translate(text, source_language, target_language) do @@ -16,13 +16,13 @@ defmodule Pleroma.Language.Translation do case @cachex.get(:translations_cache, cache_key) do {:ok, nil} -> - service = get_service() + provider = get_provider() result = - if !service or !service.configured? do + if !configured?() do {:error, :not_found} else - service.translate(text, source_language, target_language) + provider.translate(text, source_language, target_language) end store_result(result, cache_key) @@ -37,7 +37,33 @@ defmodule Pleroma.Language.Translation do end end - defp get_service, do: Pleroma.Config.get([__MODULE__, :provider]) + def supported_languages(type) when type in [:source, :target] do + provider = get_provider() + + cache_key = "#{type}_languages/#{provider.name()}" + + case @cachex.get(:translations_cache, cache_key) do + {:ok, nil} -> + result = + if !configured?() do + {:error, :not_found} + else + provider.supported_languages(type) + end + + store_result(result, cache_key) + + result + + {:ok, result} -> + {:ok, result} + + {:error, error} -> + {:error, error} + end + end + + defp get_provider, do: Pleroma.Config.get([__MODULE__, :provider]) defp get_cache_key(text, source_language, target_language) do "#{source_language}/#{target_language}/#{content_hash(text)}" diff --git a/lib/pleroma/language/translation/deepl.ex b/lib/pleroma/language/translation/deepl.ex index 5a3474090..8ce1209cc 100644 --- a/lib/pleroma/language/translation/deepl.ex +++ b/lib/pleroma/language/translation/deepl.ex @@ -9,14 +9,17 @@ defmodule Pleroma.Language.Translation.Deepl do @behaviour Provider + @name "DeepL" + @impl Provider - def configured? do - is_atom(get_base_url()) and not_empty_string(get_api_key()) - end + def configured?, do: not_empty_string(base_url()) and not_empty_string(api_key()) @impl Provider def translate(content, source_language, target_language) do - endpoint = get_base_url() + endpoint = + base_url() + |> URI.merge("/v2/translate") + |> URI.to_string() case Pleroma.HTTP.post( endpoint <> @@ -30,7 +33,7 @@ defmodule Pleroma.Language.Translation.Deepl do "", [ {"Content-Type", "application/x-www-form-urlencoded"}, - {"Authorization", "DeepL-Auth-Key #{get_api_key()}"} + {"Authorization", "DeepL-Auth-Key #{api_key()}"} ] ) do {:ok, %{status: 429}} -> @@ -50,7 +53,7 @@ defmodule Pleroma.Language.Translation.Deepl do %{ content: content, detected_source_language: detected_source_language, - provider: "DeepL" + provider: @name }} _ -> @@ -58,11 +61,41 @@ defmodule Pleroma.Language.Translation.Deepl do end end - defp get_base_url do + @impl Provider + def supported_languages(type) when type in [:source, :target] do + endpoint = + base_url() + |> URI.merge("/v2/languages") + |> URI.to_string() + + case Pleroma.HTTP.post( + endpoint <> "?" <> URI.encode_query(%{type: type}), + "", + [ + {"Content-Type", "application/x-www-form-urlencoded"}, + {"Authorization", "DeepL-Auth-Key #{api_key()}"} + ] + ) do + {:ok, %{status: 200} = res} -> + languages = + Jason.decode!(res.body) + |> Enum.map(fn %{"language" => language} -> language |> String.downcase() end) + + {:ok, languages} + + _ -> + {:error, :internal_server_error} + end + end + + @impl Provider + def name, do: @name + + defp base_url do Pleroma.Config.get([__MODULE__, :base_url]) end - defp get_api_key do + defp api_key do Pleroma.Config.get([__MODULE__, :api_key]) end end diff --git a/lib/pleroma/language/translation/libretranslate.ex b/lib/pleroma/language/translation/libretranslate.ex index 0c1fe17a0..92bde8772 100644 --- a/lib/pleroma/language/translation/libretranslate.ex +++ b/lib/pleroma/language/translation/libretranslate.ex @@ -9,21 +9,21 @@ defmodule Pleroma.Language.Translation.Libretranslate do @behaviour Provider + @name "LibreTranslate" + @impl Provider - def configured?, do: not_empty_string(get_base_url()) + def configured?, do: not_empty_string(base_url()) and not_empty_string(api_key()) @impl Provider def translate(content, source_language, target_language) do - endpoint = endpoint_url() - case Pleroma.HTTP.post( - endpoint, + base_url() <> "/translate", Jason.encode!(%{ q: content, source: source_language |> String.upcase(), target: target_language, format: "html", - api_key: get_api_key() + api_key: api_key() }), [ {"Content-Type", "application/json"} @@ -52,15 +52,29 @@ defmodule Pleroma.Language.Translation.Libretranslate do end end - defp endpoint_url do - get_base_url() <> "/translate" + @impl Provider + def supported_languages(_) do + case Pleroma.HTTP.get(base_url() <> "/languages") do + {:ok, %{status: 200} = res} -> + languages = + Jason.decode!(res.body) + |> Enum.map(fn %{"code" => code} -> code end) + + {:ok, languages} + + _ -> + {:error, :internal_server_error} + end end - defp get_base_url do + @impl Provider + def name, do: @name + + defp base_url do Pleroma.Config.get([__MODULE__, :base_url]) end - defp get_api_key do + defp api_key do Pleroma.Config.get([__MODULE__, :api_key], "") end end diff --git a/lib/pleroma/language/translation/provider.ex b/lib/pleroma/language/translation/provider.ex index a88461a47..a8b151fd7 100644 --- a/lib/pleroma/language/translation/provider.ex +++ b/lib/pleroma/language/translation/provider.ex @@ -17,4 +17,9 @@ defmodule Pleroma.Language.Translation.Provider do provider: String.t() }} | {:error, atom()} + + @callback supported_languages(type :: :string | :target) :: + {:ok, [String.t()]} | {:error, atom()} + + @callback name() :: String.t() end diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex index f98cf801f..8c2462c80 100644 --- a/lib/pleroma/web/mastodon_api/views/instance_view.ex +++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex @@ -206,10 +206,32 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do vapid: %{ public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key) }, - translation: %{enabled: Pleroma.Language.Translation.configured?()} + translation: translation_config() }) end + defp translation_config do + enabled = Pleroma.Language.Translation.configured?() + + source_languages = + case Pleroma.Language.Translation.supported_languages(:source) do + {:ok, languages} -> languages + _ -> nil + end + + target_languages = + case Pleroma.Language.Translation.supported_languages(:target) do + {:ok, languages} -> languages + _ -> nil + end + + %{ + enabled: enabled, + source_languages: source_languages, + target_languages: target_languages + } + end + defp pleroma_configuration(instance) do %{ metadata: %{ diff --git a/test/fixtures/tesla_mock/deepl-languages-list.json b/test/fixtures/tesla_mock/deepl-languages-list.json new file mode 100644 index 000000000..03d47d2ec --- /dev/null +++ b/test/fixtures/tesla_mock/deepl-languages-list.json @@ -0,0 +1 @@ +[{"language":"BG","name":"Bulgarian","supports_formality":false},{"language":"CS","name":"Czech","supports_formality":false},{"language":"DA","name":"Danish","supports_formality":false},{"language":"DE","name":"German","supports_formality":true},{"language":"EL","name":"Greek","supports_formality":false},{"language":"EN-GB","name":"English (British)","supports_formality":false},{"language":"EN-US","name":"English (American)","supports_formality":false},{"language":"ES","name":"Spanish","supports_formality":true},{"language":"ET","name":"Estonian","supports_formality":false},{"language":"FI","name":"Finnish","supports_formality":false},{"language":"FR","name":"French","supports_formality":true},{"language":"HU","name":"Hungarian","supports_formality":false},{"language":"ID","name":"Indonesian","supports_formality":false},{"language":"IT","name":"Italian","supports_formality":true},{"language":"JA","name":"Japanese","supports_formality":false},{"language":"LT","name":"Lithuanian","supports_formality":false},{"language":"LV","name":"Latvian","supports_formality":false},{"language":"NL","name":"Dutch","supports_formality":true},{"language":"PL","name":"Polish","supports_formality":true},{"language":"PT-BR","name":"Portuguese (Brazilian)","supports_formality":true},{"language":"PT-PT","name":"Portuguese (European)","supports_formality":true},{"language":"RO","name":"Romanian","supports_formality":false},{"language":"RU","name":"Russian","supports_formality":true},{"language":"SK","name":"Slovak","supports_formality":false},{"language":"SL","name":"Slovenian","supports_formality":false},{"language":"SV","name":"Swedish","supports_formality":false},{"language":"TR","name":"Turkish","supports_formality":false},{"language":"UK","name":"Ukrainian","supports_formality":false},{"language":"ZH","name":"Chinese (simplified)","supports_formality":false}] \ No newline at end of file diff --git a/test/pleroma/language/translation/deepl_test.ex b/test/pleroma/language/translation/deepl_test.ex index 0c29b84a4..3a7265622 100644 --- a/test/pleroma/language/translation/deepl_test.ex +++ b/test/pleroma/language/translation/deepl_test.ex @@ -24,4 +24,14 @@ defmodule Pleroma.Language.Translation.DeeplTest do provider: "DeepL" } = res end + + test "it returns languages list" do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + clear_config([Pleroma.Language.Translation.Deepl, :base_url], "https://api-free.deepl.com") + clear_config([Pleroma.Language.Translation.Deepl, :api_key], "API_KEY") + + assert {:ok, [language | _languages]} = Deepl.supported_languages(:target) + + assert is_binary(language) + end end diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index c5f29e0b8..771336b6f 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -1578,6 +1578,15 @@ defmodule HttpRequestMock do }} end + def post("https://api-free.deepl.com/v2/languages" <> _, _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/deepl-languages-list.json"), + headers: [{"content-type", "application/json"}] + }} + end + def post(url, query, body, headers) do {:error, "Mock response not implemented for POST #{inspect(url)}, #{query}, #{inspect(body)}, #{inspect(headers)}"} diff --git a/test/support/translation_mock.ex b/test/support/translation_mock.ex index 7e618c263..2047d6426 100644 --- a/test/support/translation_mock.ex +++ b/test/support/translation_mock.ex @@ -7,6 +7,8 @@ defmodule TranslationMock do @behaviour Provider + @name "TranslationMock" + @impl Provider def configured?, do: true @@ -16,7 +18,15 @@ defmodule TranslationMock do %{ content: content |> String.reverse(), detected_source_language: source_language, - provider: "TranslationMock" + provider: @name }} end + + @impl Provider + def supported_languages(_) do + ["en", "pl"] + end + + @impl Provider + def name, do: @name end