commit: 0dfcc24d30b3b33980ae7780b5c08bd28317399c
parent ba8b5682ccd77a819f644e714deab217195b4879
Author: nicole mikołajczyk <me@mkljczk.pl>
Date: Sat, 29 Nov 2025 16:50:42 +0100
Merge branch 'translation-provider-translatelocally' into 'develop'
Support translateLocally translation provider
See merge request pleroma/pleroma!4377
Diffstat:
4 files changed, 199 insertions(+), 1 deletion(-)
diff --git a/changelog.d/translation-provider-translatelocally.add b/changelog.d/translation-provider-translatelocally.add
@@ -0,0 +1 @@
+Support translateLocally translation provider
diff --git a/config/description.exs b/config/description.exs
@@ -3559,7 +3559,8 @@ config :pleroma, :config_description, [
suggestions: [
Pleroma.Language.Translation.Deepl,
Pleroma.Language.Translation.Libretranslate,
- Pleroma.Language.Translation.Mozhi
+ Pleroma.Language.Translation.Mozhi,
+ Pleroma.Language.Translation.TranslateLocally
]
},
%{
@@ -3591,6 +3592,14 @@ config :pleroma, :config_description, [
suggestions: ["YOUR_API_KEY"]
},
%{
+ group: {:subgroup, Pleroma.Language.Translation.TranslateLocally},
+ key: :intermediary_language,
+ label:
+ "translateLocally intermediary language (used when direct source->target model is not available)",
+ type: :string,
+ suggestions: ["en"]
+ },
+ %{
group: {:subgroup, Pleroma.Language.Translation.Mozhi},
key: :base_url,
label: "Mozhi instance URL",
diff --git a/lib/pleroma/language/translation/translate_locally.ex b/lib/pleroma/language/translation/translate_locally.ex
@@ -0,0 +1,129 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Language.Translation.TranslateLocally do
+ alias Pleroma.Language.Translation.Provider
+
+ use Provider
+
+ @behaviour Provider
+
+ @name "translateLocally"
+
+ @impl Provider
+ def missing_dependencies do
+ if Pleroma.Utils.command_available?("translateLocally") do
+ []
+ else
+ ["translateLocally"]
+ end
+ end
+
+ @impl Provider
+ def configured?, do: is_map(models())
+
+ @impl Provider
+ def translate(content, source_language, target_language) do
+ model =
+ models()
+ |> Map.get(source_language, %{})
+ |> Map.get(target_language)
+
+ models =
+ if model do
+ [model]
+ else
+ [
+ models()
+ |> Map.get(source_language, %{})
+ |> Map.get(intermediary_language()),
+ models()
+ |> Map.get(intermediary_language(), %{})
+ |> Map.get(target_language)
+ ]
+ end
+
+ translated_content =
+ Enum.reduce(models, content, fn model, content ->
+ text_path = Path.join(System.tmp_dir!(), "translateLocally-#{Ecto.UUID.generate()}")
+
+ File.write(text_path, content)
+
+ translated_content =
+ case System.cmd("translateLocally", ["-m", model, "-i", text_path, "--html"]) do
+ {content, _} -> content
+ _ -> nil
+ end
+
+ File.rm(text_path)
+
+ translated_content
+ end)
+
+ {:ok,
+ %{
+ content: translated_content,
+ detected_source_language: source_language,
+ provider: @name
+ }}
+ end
+
+ @impl Provider
+ def supported_languages(:source) do
+ languages =
+ languages_matrix()
+ |> elem(1)
+ |> Map.keys()
+
+ {:ok, languages}
+ end
+
+ @impl Provider
+ def supported_languages(:target) do
+ languages =
+ languages_matrix()
+ |> elem(1)
+ |> Map.values()
+ |> List.flatten()
+ |> Enum.uniq()
+
+ {:ok, languages}
+ end
+
+ @impl Provider
+ def languages_matrix do
+ languages =
+ models()
+ |> Map.to_list()
+ |> Enum.map(fn {key, value} -> {key, Map.keys(value)} end)
+ |> Enum.into(%{})
+
+ matrix =
+ if intermediary_language() do
+ languages
+ |> Map.to_list()
+ |> Enum.map(fn {key, value} ->
+ with_intermediary =
+ (((value ++ languages[intermediary_language()])
+ |> Enum.uniq()) --
+ [key])
+ |> Enum.sort()
+
+ {key, with_intermediary}
+ end)
+ |> Enum.into(%{})
+ else
+ languages
+ end
+
+ {:ok, matrix}
+ end
+
+ @impl Provider
+ def name, do: @name
+
+ defp models, do: Pleroma.Config.get([__MODULE__, :models])
+
+ defp intermediary_language, do: Pleroma.Config.get([__MODULE__, :intermediary_language])
+end
diff --git a/test/pleroma/language/translation/translate_locally_test.exs b/test/pleroma/language/translation/translate_locally_test.exs
@@ -0,0 +1,59 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Language.Translation.TranslateLocallyTest do
+ use Pleroma.DataCase
+
+ alias Pleroma.Language.Translation.TranslateLocally
+
+ @example_models %{
+ "de" => %{
+ "en" => "de-en-base"
+ },
+ "en" => %{
+ "de" => "en-de-base",
+ "pl" => "en-pl-tiny"
+ },
+ "cs" => %{
+ "en" => "cs-en-base"
+ },
+ "pl" => %{
+ "en" => "pl-en-tiny"
+ }
+ }
+
+ test "it returns languages list" do
+ clear_config([Pleroma.Language.Translation.TranslateLocally, :models], @example_models)
+
+ assert {:ok, languages} = TranslateLocally.supported_languages(:source)
+ assert ["cs", "de", "en", "pl"] = languages |> Enum.sort()
+ end
+
+ describe "it returns languages matrix" do
+ test "without intermediary language" do
+ clear_config([Pleroma.Language.Translation.TranslateLocally, :models], @example_models)
+
+ assert {:ok,
+ %{
+ "cs" => ["en"],
+ "de" => ["en"],
+ "en" => ["de", "pl"],
+ "pl" => ["en"]
+ }} = TranslateLocally.languages_matrix()
+ end
+
+ test "with intermediary language" do
+ clear_config([Pleroma.Language.Translation.TranslateLocally, :models], @example_models)
+ clear_config([Pleroma.Language.Translation.TranslateLocally, :intermediary_language], "en")
+
+ assert {:ok,
+ %{
+ "cs" => ["de", "en", "pl"],
+ "de" => ["en", "pl"],
+ "en" => ["de", "pl"],
+ "pl" => ["de", "en"]
+ }} = TranslateLocally.languages_matrix()
+ end
+ end
+end