mirror of
https://git.pleroma.social/pleroma/pleroma.git
synced 2025-04-04 17:49:36 +00:00
Merge branch 'translate-posts' into 'develop'
Support translation providers (DeepL, LibreTranslate) See merge request pleroma/pleroma!4102
This commit is contained in:
commit
81960dccf2
23 changed files with 815 additions and 6 deletions
1
changelog.d/translate-posts.add
Normal file
1
changelog.d/translate-posts.add
Normal file
|
@ -0,0 +1 @@
|
|||
Support translation providers (DeepL, LibreTranslate)
|
|
@ -3522,5 +3522,49 @@ config :pleroma, :config_description, [
|
|||
suggestions: ["/usr/share/fasttext/lid.176.bin"]
|
||||
}
|
||||
]
|
||||
},
|
||||
%{
|
||||
group: :pleroma,
|
||||
key: Pleroma.Language.Translation,
|
||||
type: :group,
|
||||
description: "Translation providers",
|
||||
children: [
|
||||
%{
|
||||
key: :provider,
|
||||
type: :module,
|
||||
suggestions: [
|
||||
Pleroma.Language.Translation.Deepl,
|
||||
Pleroma.Language.Translation.Libretranslate
|
||||
]
|
||||
},
|
||||
%{
|
||||
group: {:subgroup, Pleroma.Language.Translation.Deepl},
|
||||
key: :base_url,
|
||||
label: "DeepL base URL",
|
||||
type: :string,
|
||||
suggestions: ["https://api-free.deepl.com", "https://api.deepl.com"]
|
||||
},
|
||||
%{
|
||||
group: {:subgroup, Pleroma.Language.Translation.Deepl},
|
||||
key: :api_key,
|
||||
label: "DeepL API Key",
|
||||
type: :string,
|
||||
suggestions: ["YOUR_API_KEY"]
|
||||
},
|
||||
%{
|
||||
group: {:subgroup, Pleroma.Language.Translation.Libretranslate},
|
||||
key: :base_url,
|
||||
label: "LibreTranslate instance URL",
|
||||
type: :string,
|
||||
suggestions: ["https://libretranslate.com"]
|
||||
},
|
||||
%{
|
||||
group: {:subgroup, Pleroma.Language.Translation.Libretranslate},
|
||||
key: :api_key,
|
||||
label: "LibreTranslate API Key",
|
||||
type: :string,
|
||||
suggestions: ["YOUR_API_KEY"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -172,7 +172,8 @@ defmodule Pleroma.Application do
|
|||
limit: 500_000
|
||||
),
|
||||
build_cachex("rel_me", limit: 2500),
|
||||
build_cachex("host_meta", default_ttl: :timer.minutes(120), limit: 5000)
|
||||
build_cachex("host_meta", default_ttl: :timer.minutes(120), limit: 5_000),
|
||||
build_cachex("translations", default_ttl: :timer.hours(24), limit: 5_000)
|
||||
]
|
||||
end
|
||||
|
||||
|
|
|
@ -202,11 +202,24 @@ defmodule Pleroma.ApplicationRequirements do
|
|||
false
|
||||
end
|
||||
|
||||
translation_commands_status =
|
||||
if Pleroma.Language.Translation.missing_dependencies() == [] do
|
||||
true
|
||||
else
|
||||
Logger.error(
|
||||
"The following dependencies required by the currently enabled " <>
|
||||
"translation provider are not installed: " <>
|
||||
inspect(Pleroma.Language.Translation.missing_dependencies())
|
||||
)
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
if Enum.all?(
|
||||
[
|
||||
preview_proxy_commands_status,
|
||||
language_detector_commands_status
|
||||
| filter_commands_statuses
|
||||
language_detector_commands_status,
|
||||
translation_commands_status | filter_commands_statuses
|
||||
],
|
||||
& &1
|
||||
) do
|
||||
|
|
127
lib/pleroma/language/translation.ex
Normal file
127
lib/pleroma/language/translation.ex
Normal file
|
@ -0,0 +1,127 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Language.Translation do
|
||||
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
|
||||
|
||||
def configured? do
|
||||
provider = get_provider()
|
||||
|
||||
!!provider and provider.configured?
|
||||
end
|
||||
|
||||
def missing_dependencies do
|
||||
provider = get_provider()
|
||||
|
||||
if provider do
|
||||
provider.missing_dependencies()
|
||||
else
|
||||
[]
|
||||
end
|
||||
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} ->
|
||||
provider = get_provider()
|
||||
|
||||
result =
|
||||
if !configured?() do
|
||||
{:error, :not_found}
|
||||
else
|
||||
provider.translate(text, source_language, target_language)
|
||||
|> scrub_html()
|
||||
end
|
||||
|
||||
store_result(result, cache_key)
|
||||
|
||||
result
|
||||
|
||||
{:ok, result} ->
|
||||
{:ok, result}
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def languages_matrix do
|
||||
provider = get_provider()
|
||||
|
||||
cache_key = "languages_matrix/#{provider.name()}"
|
||||
|
||||
case @cachex.get(:translations_cache, cache_key) do
|
||||
{:ok, nil} ->
|
||||
result =
|
||||
if !configured?() do
|
||||
{:error, :not_found}
|
||||
else
|
||||
provider.languages_matrix()
|
||||
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)}"
|
||||
end
|
||||
|
||||
defp store_result({:ok, result}, cache_key) do
|
||||
@cachex.put(:translations_cache, cache_key, result)
|
||||
end
|
||||
|
||||
defp store_result(_, _), do: nil
|
||||
|
||||
defp content_hash(text), do: :crypto.hash(:sha256, text) |> Base.encode64()
|
||||
|
||||
defp scrub_html({:ok, %{content: content} = result}) when is_binary(content) do
|
||||
scrubbers = Pleroma.Config.get([:markup, :scrub_policy])
|
||||
|
||||
content
|
||||
|> Pleroma.HTML.filter_tags(scrubbers)
|
||||
|
||||
{:ok, %{result | content: content}}
|
||||
end
|
||||
|
||||
defp scrub_html(result), do: result
|
||||
end
|
123
lib/pleroma/language/translation/deepl.ex
Normal file
123
lib/pleroma/language/translation/deepl.ex
Normal file
|
@ -0,0 +1,123 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Language.Translation.Deepl do
|
||||
import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1]
|
||||
|
||||
alias Pleroma.Language.Translation.Provider
|
||||
|
||||
use Provider
|
||||
|
||||
@behaviour Provider
|
||||
|
||||
@name "DeepL"
|
||||
|
||||
@impl Provider
|
||||
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 =
|
||||
base_url()
|
||||
|> URI.merge("/v2/translate")
|
||||
|> URI.to_string()
|
||||
|
||||
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 #{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: @name
|
||||
}}
|
||||
|
||||
_ ->
|
||||
{:error, :internal_server_error}
|
||||
end
|
||||
end
|
||||
|
||||
@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)
|
||||
|> Enum.map(fn language ->
|
||||
if String.contains?(language, "-") do
|
||||
[language, language |> String.split("-") |> Enum.at(0)]
|
||||
else
|
||||
language
|
||||
end
|
||||
end)
|
||||
|> List.flatten()
|
||||
|> Enum.uniq()
|
||||
|
||||
{:ok, languages}
|
||||
|
||||
_ ->
|
||||
{:error, :internal_server_error}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Provider
|
||||
def languages_matrix do
|
||||
with {:ok, source_languages} <- supported_languages(:source),
|
||||
{:ok, target_languages} <- supported_languages(:target) do
|
||||
{:ok,
|
||||
Map.new(source_languages, fn language -> {language, target_languages -- [language]} end)}
|
||||
else
|
||||
{:error, error} -> {:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Provider
|
||||
def name, do: @name
|
||||
|
||||
defp base_url do
|
||||
Pleroma.Config.get([__MODULE__, :base_url])
|
||||
end
|
||||
|
||||
defp api_key do
|
||||
Pleroma.Config.get([__MODULE__, :api_key])
|
||||
end
|
||||
end
|
93
lib/pleroma/language/translation/libretranslate.ex
Normal file
93
lib/pleroma/language/translation/libretranslate.ex
Normal file
|
@ -0,0 +1,93 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Language.Translation.Libretranslate do
|
||||
import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1]
|
||||
|
||||
alias Pleroma.Language.Translation.Provider
|
||||
|
||||
use Provider
|
||||
|
||||
@behaviour Provider
|
||||
|
||||
@name "LibreTranslate"
|
||||
|
||||
@impl Provider
|
||||
def configured?, do: not_empty_string(base_url()) and not_empty_string(api_key())
|
||||
|
||||
@impl Provider
|
||||
def translate(content, source_language, target_language) do
|
||||
case Pleroma.HTTP.post(
|
||||
base_url() <> "/translate",
|
||||
Jason.encode!(%{
|
||||
q: content,
|
||||
source: source_language |> String.upcase(),
|
||||
target: target_language,
|
||||
format: "html",
|
||||
api_key: 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: @name
|
||||
}}
|
||||
|
||||
_ ->
|
||||
{:error, :internal_server_error}
|
||||
end
|
||||
end
|
||||
|
||||
@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
|
||||
|
||||
@impl Provider
|
||||
def languages_matrix do
|
||||
with {:ok, source_languages} <- supported_languages(:source),
|
||||
{:ok, target_languages} <- supported_languages(:target) do
|
||||
{:ok,
|
||||
Map.new(source_languages, fn language -> {language, target_languages -- [language]} end)}
|
||||
else
|
||||
{:error, error} -> {:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Provider
|
||||
def name, do: @name
|
||||
|
||||
defp base_url do
|
||||
Pleroma.Config.get([__MODULE__, :base_url])
|
||||
end
|
||||
|
||||
defp api_key do
|
||||
Pleroma.Config.get([__MODULE__, :api_key], "")
|
||||
end
|
||||
end
|
40
lib/pleroma/language/translation/provider.ex
Normal file
40
lib/pleroma/language/translation/provider.ex
Normal file
|
@ -0,0 +1,40 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Language.Translation.Provider do
|
||||
alias Pleroma.Language.Translation.Provider
|
||||
|
||||
@callback missing_dependencies() :: [String.t()]
|
||||
|
||||
@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()}
|
||||
|
||||
@callback supported_languages(type :: :string | :target) ::
|
||||
{:ok, [String.t()]} | {:error, atom()}
|
||||
|
||||
@callback languages_matrix() :: {:ok, Map.t()} | {:error, atom()}
|
||||
|
||||
@callback name() :: String.t()
|
||||
|
||||
defmacro __using__(_opts) do
|
||||
quote do
|
||||
@impl Provider
|
||||
def missing_dependencies, do: []
|
||||
|
||||
defoverridable missing_dependencies: 0
|
||||
end
|
||||
end
|
||||
end
|
|
@ -57,6 +57,29 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do
|
|||
}
|
||||
end
|
||||
|
||||
def translation_languages_operation do
|
||||
%Operation{
|
||||
tags: ["Instance misc"],
|
||||
summary: "Retrieve supported languages matrix",
|
||||
operationId: "InstanceController.translation_languages",
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response(
|
||||
"Translation languages matrix",
|
||||
"application/json",
|
||||
%Schema{
|
||||
type: :object,
|
||||
additionalProperties: %Schema{
|
||||
type: :array,
|
||||
items: %Schema{type: :string},
|
||||
description: "Supported target languages for a source language"
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp instance do
|
||||
%Schema{
|
||||
type: :object,
|
||||
|
|
|
@ -427,6 +427,38 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
|
|||
}
|
||||
end
|
||||
|
||||
def translate_operation do
|
||||
%Operation{
|
||||
tags: ["Retrieve status information"],
|
||||
summary: "Translate status",
|
||||
description: "Translate status with an external API",
|
||||
operationId: "StatusController.translate",
|
||||
security: [%{"oAuth" => ["read:statuses"]}],
|
||||
parameters: [id_param()],
|
||||
requestBody:
|
||||
request_body(
|
||||
"Parameters",
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
lang: %Schema{
|
||||
type: :string,
|
||||
nullable: true,
|
||||
description: "Translation target language."
|
||||
}
|
||||
}
|
||||
},
|
||||
required: false
|
||||
),
|
||||
responses: %{
|
||||
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)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def favourites_operation do
|
||||
%Operation{
|
||||
tags: ["Timelines"],
|
||||
|
@ -819,4 +851,32 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
|
|||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp translation do
|
||||
%Schema{
|
||||
title: "StatusTranslation",
|
||||
description: "Represents status translation with related information.",
|
||||
type: :object,
|
||||
required: [:content, :detected_source_language, :provider],
|
||||
properties: %{
|
||||
content: %Schema{
|
||||
type: :string,
|
||||
description: "Translated status content"
|
||||
},
|
||||
detected_source_language: %Schema{
|
||||
type: :string,
|
||||
description: "Detected source language"
|
||||
},
|
||||
provider: %Schema{
|
||||
type: :string,
|
||||
description: "Translation provider service name"
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
"content" => "Software für die nächste Generation der sozialen Medien.",
|
||||
"detected_source_language" => "en",
|
||||
"provider" => "Deepl"
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -30,4 +30,9 @@ defmodule Pleroma.Web.MastodonAPI.InstanceController do
|
|||
def rules(conn, _params) do
|
||||
render(conn, "rules.json")
|
||||
end
|
||||
|
||||
@doc "GET /api/v1/instance/translation_languages"
|
||||
def translation_languages(conn, _params) do
|
||||
render(conn, "translation_languages.json")
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13,6 +13,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
|
|||
alias Pleroma.Activity
|
||||
alias Pleroma.Bookmark
|
||||
alias Pleroma.BookmarkFolder
|
||||
alias Pleroma.Language.Translation
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.ScheduledActivity
|
||||
|
@ -44,6 +45,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
|
|||
]
|
||||
)
|
||||
|
||||
plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action == :translate)
|
||||
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["write:statuses"]}
|
||||
|
@ -85,7 +88,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
|
|||
%{scopes: ["write:bookmarks"]} when action in [:bookmark, :unbookmark]
|
||||
)
|
||||
|
||||
@rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete)a
|
||||
@rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete translate)a
|
||||
|
||||
plug(
|
||||
RateLimiter,
|
||||
|
@ -549,6 +552,41 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
|
|||
end
|
||||
end
|
||||
|
||||
@doc "POST /api/v1/statuses/:id/translate"
|
||||
def translate(
|
||||
%{
|
||||
assigns: %{user: user},
|
||||
private: %{open_api_spex: %{body_params: params, params: %{id: status_id}}}
|
||||
} = conn,
|
||||
_
|
||||
) do
|
||||
with %Activity{object: object} <- Activity.get_by_id_with_object(status_id),
|
||||
{:visibility, visibility} when visibility in ["public", "unlisted"] <-
|
||||
{:visibility, Visibility.get_visibility(object)},
|
||||
{:language, language} when is_binary(language) <-
|
||||
{:language, Map.get(params, :lang) || user.language},
|
||||
{:ok, result} <-
|
||||
Translation.translate(
|
||||
object.data["content"],
|
||||
object.data["language"],
|
||||
language
|
||||
) do
|
||||
render(conn, "translation.json", result)
|
||||
else
|
||||
{:language, nil} ->
|
||||
render_error(conn, :bad_request, "Language not specified")
|
||||
|
||||
{:visibility, _} ->
|
||||
render_error(conn, :not_found, "Record not found")
|
||||
|
||||
{:error, :not_found} ->
|
||||
render_error(conn, :not_found, "Translation service not configured")
|
||||
|
||||
{:error, error} when error in [:unexpected_response, :quota_exceeded, :too_many_requests] ->
|
||||
render_error(conn, :service_unavailable, "Translation service not available")
|
||||
end
|
||||
end
|
||||
|
||||
@doc "GET /api/v1/favourites"
|
||||
def favourites(
|
||||
%{assigns: %{user: %User{} = user}, private: %{open_api_spex: %{params: params}}} = conn,
|
||||
|
|
|
@ -90,6 +90,15 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
|
|||
}
|
||||
end
|
||||
|
||||
def render("translation_languages.json", _) do
|
||||
with true <- Pleroma.Language.Translation.configured?(),
|
||||
{:ok, languages} <- Pleroma.Language.Translation.languages_matrix() do
|
||||
languages
|
||||
else
|
||||
_ -> %{}
|
||||
end
|
||||
end
|
||||
|
||||
defp common_information(instance) do
|
||||
%{
|
||||
languages: Keyword.get(instance, :languages, ["en"]),
|
||||
|
@ -246,7 +255,8 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
|
|||
},
|
||||
vapid: %{
|
||||
public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
|
||||
}
|
||||
},
|
||||
translation: %{enabled: Pleroma.Language.Translation.configured?()}
|
||||
})
|
||||
end
|
||||
|
||||
|
@ -259,7 +269,8 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
|
|||
fields_limits: fields_limits(),
|
||||
post_formats: Config.get([:instance, :allowed_post_formats]),
|
||||
birthday_required: Config.get([:instance, :birthday_required]),
|
||||
birthday_min_age: Config.get([:instance, :birthday_min_age])
|
||||
birthday_min_age: Config.get([:instance, :birthday_min_age]),
|
||||
translation: supported_languages()
|
||||
},
|
||||
stats: %{mau: Pleroma.User.active_user_count()},
|
||||
vapid_public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
|
||||
|
@ -285,4 +296,29 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
|
|||
})
|
||||
})
|
||||
end
|
||||
|
||||
defp supported_languages do
|
||||
enabled = Pleroma.Language.Translation.configured?()
|
||||
|
||||
source_languages =
|
||||
with true <- enabled,
|
||||
{:ok, languages} <- Pleroma.Language.Translation.supported_languages(:source) do
|
||||
languages
|
||||
else
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
target_languages =
|
||||
with true <- enabled,
|
||||
{:ok, languages} <- Pleroma.Language.Translation.supported_languages(:target) do
|
||||
languages
|
||||
else
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
%{
|
||||
source_languages: source_languages,
|
||||
target_languages: target_languages
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -681,6 +681,14 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
}
|
||||
end
|
||||
|
||||
def render("translation.json", %{
|
||||
content: content,
|
||||
detected_source_language: detected_source_language,
|
||||
provider: provider
|
||||
}) do
|
||||
%{content: content, detected_source_language: detected_source_language, provider: provider}
|
||||
end
|
||||
|
||||
def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do
|
||||
object = Object.normalize(activity, fetch: false)
|
||||
|
||||
|
|
|
@ -740,6 +740,7 @@ defmodule Pleroma.Web.Router do
|
|||
post("/statuses/:id/unbookmark", StatusController, :unbookmark)
|
||||
post("/statuses/:id/mute", StatusController, :mute_conversation)
|
||||
post("/statuses/:id/unmute", StatusController, :unmute_conversation)
|
||||
post("/statuses/:id/translate", StatusController, :translate)
|
||||
|
||||
post("/push/subscription", SubscriptionController, :create)
|
||||
get("/push/subscription", SubscriptionController, :show)
|
||||
|
@ -787,6 +788,7 @@ defmodule Pleroma.Web.Router do
|
|||
get("/instance", InstanceController, :show)
|
||||
get("/instance/peers", InstanceController, :peers)
|
||||
get("/instance/rules", InstanceController, :rules)
|
||||
get("/instance/translation_languages", InstanceController, :translation_languages)
|
||||
|
||||
get("/statuses", StatusController, :index)
|
||||
get("/statuses/:id", StatusController, :show)
|
||||
|
|
1
test/fixtures/tesla_mock/deepl-languages-list.json
vendored
Normal file
1
test/fixtures/tesla_mock/deepl-languages-list.json
vendored
Normal file
|
@ -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}]
|
1
test/fixtures/tesla_mock/deepl-translation.json
vendored
Normal file
1
test/fixtures/tesla_mock/deepl-translation.json
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
{"translations":[{"detected_source_language":"PL","text":"REMOVE THE FOLLOWER!Paste this on your follower. If we get 70% of nk users...they will remove the follower!!!"}]}
|
37
test/pleroma/language/translation/deepl_test.exs
Normal file
37
test/pleroma/language/translation/deepl_test.exs
Normal file
|
@ -0,0 +1,37 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Language.Translation.DeeplTest do
|
||||
use Pleroma.Web.ConnCase
|
||||
|
||||
alias Pleroma.Language.Translation.Deepl
|
||||
|
||||
test "it translates text" 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")
|
||||
|
||||
{:ok, res} =
|
||||
Deepl.translate(
|
||||
"USUNĄĆ ŚLEDZIKA!Wklej to na swojego śledzika. Jeżeli uzbieramy 70% użytkowników nk...to usuną śledzika!!!",
|
||||
"pl",
|
||||
"en"
|
||||
)
|
||||
|
||||
assert %{
|
||||
detected_source_language: "PL",
|
||||
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
|
28
test/pleroma/language/translation_test.exs
Normal file
28
test/pleroma/language/translation_test.exs
Normal file
|
@ -0,0 +1,28 @@
|
|||
defmodule Pleroma.Language.TranslationTest do
|
||||
use Pleroma.Web.ConnCase
|
||||
|
||||
alias Pleroma.Language.Translation
|
||||
|
||||
setup do: clear_config([Pleroma.Language.Translation, :provider], TranslationMock)
|
||||
|
||||
test "it translates text" do
|
||||
assert {:ok,
|
||||
%{
|
||||
content: "txet emos",
|
||||
detected_source_language: _,
|
||||
provider: _
|
||||
}} = Translation.translate("some text", "en", "uk")
|
||||
end
|
||||
|
||||
test "it stores translation result in cache" do
|
||||
Translation.translate("some text", "en", "uk")
|
||||
|
||||
assert {:ok, result} =
|
||||
Cachex.get(
|
||||
:translations_cache,
|
||||
"en/uk/#{:crypto.hash(:sha256, "some text") |> Base.encode64()}"
|
||||
)
|
||||
|
||||
assert result.content == "txet emos"
|
||||
end
|
||||
end
|
|
@ -152,4 +152,13 @@ defmodule Pleroma.Web.MastodonAPI.InstanceControllerTest do
|
|||
}
|
||||
] = result["rules"]
|
||||
end
|
||||
|
||||
test "translation languages matrix", %{conn: conn} do
|
||||
clear_config([Pleroma.Language.Translation, :provider], TranslationMock)
|
||||
|
||||
assert %{"en" => ["pl"], "pl" => ["en"]} =
|
||||
conn
|
||||
|> get("/api/v1/instance/translation_languages")
|
||||
|> json_response_and_validate_schema(200)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2483,4 +2483,62 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
|
|||
|> json_response_and_validate_schema(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
describe "translating statuses" do
|
||||
setup do: clear_config([Pleroma.Language.Translation, :provider], TranslationMock)
|
||||
|
||||
test "it translates a status to user language" do
|
||||
user = insert(:user, language: "fr")
|
||||
%{conn: conn} = oauth_access(["read:statuses"], user: user)
|
||||
another_user = insert(:user)
|
||||
|
||||
{:ok, activity} =
|
||||
CommonAPI.post(another_user, %{
|
||||
status: "Cześć!",
|
||||
visibility: "public",
|
||||
language: "pl"
|
||||
})
|
||||
|
||||
response =
|
||||
conn
|
||||
|> post("/api/v1/statuses/#{activity.id}/translate")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert response == %{
|
||||
"content" => "!ćśezC",
|
||||
"detected_source_language" => "pl",
|
||||
"provider" => "TranslationMock"
|
||||
}
|
||||
end
|
||||
|
||||
test "it returns an error if no target language provided" do
|
||||
%{conn: conn} = oauth_access(["read:statuses"])
|
||||
another_user = insert(:user)
|
||||
|
||||
{:ok, activity} =
|
||||
CommonAPI.post(another_user, %{
|
||||
status: "Cześć!",
|
||||
language: "pl"
|
||||
})
|
||||
|
||||
assert conn
|
||||
|> post("/api/v1/statuses/#{activity.id}/translate")
|
||||
|> json_response_and_validate_schema(400)
|
||||
end
|
||||
|
||||
test "it doesn't translate non-public statuses" do
|
||||
%{conn: conn, user: user} = oauth_access(["read:statuses"])
|
||||
|
||||
{:ok, activity} =
|
||||
CommonAPI.post(user, %{
|
||||
status: "Cześć!",
|
||||
visibility: "private",
|
||||
language: "pl"
|
||||
})
|
||||
|
||||
assert conn
|
||||
|> post("/api/v1/statuses/#{activity.id}/translate")
|
||||
|> json_response_and_validate_schema(404)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1706,6 +1706,24 @@ defmodule HttpRequestMock do
|
|||
}}
|
||||
end
|
||||
|
||||
def post("https://api-free.deepl.com/v2/translate" <> _, _, _, _) do
|
||||
{:ok,
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
body: File.read!("test/fixtures/tesla_mock/deepl-translation.json"),
|
||||
headers: [{"content-type", "application/json"}]
|
||||
}}
|
||||
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)}"}
|
||||
|
|
43
test/support/translation_mock.ex
Normal file
43
test/support/translation_mock.ex
Normal file
|
@ -0,0 +1,43 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule TranslationMock do
|
||||
alias Pleroma.Language.Translation.Provider
|
||||
|
||||
use Provider
|
||||
|
||||
@behaviour Provider
|
||||
|
||||
@name "TranslationMock"
|
||||
|
||||
@impl Provider
|
||||
def configured?, do: true
|
||||
|
||||
@impl Provider
|
||||
def translate(content, source_language, _target_language) do
|
||||
{:ok,
|
||||
%{
|
||||
content: content |> String.reverse(),
|
||||
detected_source_language: source_language,
|
||||
provider: @name
|
||||
}}
|
||||
end
|
||||
|
||||
@impl Provider
|
||||
def supported_languages(_) do
|
||||
{:ok, ["en", "pl"]}
|
||||
end
|
||||
|
||||
@impl Provider
|
||||
def languages_matrix do
|
||||
{:ok,
|
||||
%{
|
||||
"en" => ["pl"],
|
||||
"pl" => ["en"]
|
||||
}}
|
||||
end
|
||||
|
||||
@impl Provider
|
||||
def name, do: @name
|
||||
end
|
Loading…
Reference in a new issue