From f027db9654f90df92361ccd1215161862c9ff006 Mon Sep 17 00:00:00 2001 From: ilja Date: Thu, 8 Dec 2022 12:09:06 +0100 Subject: [PATCH] Add translation module for Argos Translate Argos Translate is a Python module for translation and can be used as a command line tool. This is also the engine for LibreTranslate, for which we already have a module. Here we can use the engine irectly from our server without doing requests to a third party or having to install our own LibreTranslate webservice. One thing that's currently still missing from ArgosTranslate is auto-detection of languages. --- config/config.exs | 5 ++ docs/docs/configuration/cheatsheet.md | 6 ++ .../akkoma/translators/argos_translate.ex | 83 +++++++++++++++++++ .../translators/argos_translate_test.exs | 66 +++++++++++++++ 4 files changed, 160 insertions(+) create mode 100644 lib/pleroma/akkoma/translators/argos_translate.ex create mode 100644 test/pleroma/akkoma/translators/argos_translate_test.exs diff --git a/config/config.exs b/config/config.exs index 48290fb05..6112cee09 100644 --- a/config/config.exs +++ b/config/config.exs @@ -882,6 +882,11 @@ url: "http://127.0.0.1:5000", api_key: nil +config :pleroma, :argos_translate, + command_argos_translate: "argos-translate", + command_argospm: "argospm", + default_language: "en" + # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. import_config "#{Mix.env()}.exs" diff --git a/docs/docs/configuration/cheatsheet.md b/docs/docs/configuration/cheatsheet.md index 22fc4ecbe..e369545ff 100644 --- a/docs/docs/configuration/cheatsheet.md +++ b/docs/docs/configuration/cheatsheet.md @@ -1140,3 +1140,9 @@ Translations are available at `/api/v1/statuses/:id/translations/:language`, whe - `:url` - URL of LibreTranslate instance - `:api_key` - API key for LibreTranslate + +### `:argos_translate` + +- `:command_argos_translate` - command for `argos-translate`. Can be the command if it's in your PATH, or the full path to the file (default: `argos-translate`). +- `:command_argospm` - command for `argospm`. Can be the command if it's in your PATH, or the full path to the file (default: `argospm`). +- `:default_language` - When no language is provided to translate from, this language will be used. Must be a two letter langage code from a language you have installed (default: `en`). diff --git a/lib/pleroma/akkoma/translators/argos_translate.ex b/lib/pleroma/akkoma/translators/argos_translate.ex new file mode 100644 index 000000000..0e64c4743 --- /dev/null +++ b/lib/pleroma/akkoma/translators/argos_translate.ex @@ -0,0 +1,83 @@ +defmodule Pleroma.Akkoma.Translators.ArgosTranslate do + @behaviour Pleroma.Akkoma.Translator + + alias Pleroma.Config + + defp argos_translate do + Config.get([:argos_translate, :command_argos_translate]) + end + + defp argospm do + Config.get([:argos_translate, :command_argospm]) + end + + defp default_language do + Config.get([:argos_translate, :default_language]) + end + + defp safe_languages() do + try do + System.cmd(argospm(), ["list"], stderr_to_stdout: true, parallelism: true) + rescue + _ -> {"Command #{argospm()} not found", 1} + end + end + + @impl Pleroma.Akkoma.Translator + def languages do + with {response, 0} <- safe_languages() do + langs = + response + |> String.split("\n", trim: true) + |> Enum.map(fn + "translate-" <> l -> String.split(l, "_") + _ -> "" + end) + + source_langs = + langs + |> Enum.map(fn [l, _] -> %{code: l, name: l} end) + |> Enum.uniq() + + dest_langs = + langs + |> Enum.map(fn [_, l] -> %{code: l, name: l} end) + |> Enum.uniq() + + {:ok, source_langs, dest_langs} + else + {response, _} -> {:error, "ArgosTranslate failed to fetch languages (#{response})"} + end + end + + defp safe_translate(string, from_language, to_language) do + try do + System.cmd( + argos_translate(), + ["--from-lang", from_language, "--to-lang", to_language, string], + stderr_to_stdout: true, + parallelism: true + ) + rescue + _ -> {"Command #{argos_translate()} not found", 1} + end + end + + @impl Pleroma.Akkoma.Translator + def translate(string, from_language, to_language) do + # Akkoma's Pleroma-fe expects us to detect the source language automatically. + # Argos-translate doesn't have that option (yet?) + # see + # For now we choose a default source language from settings. + # Afterwards people get the option to overwrite the source language from a dropdown. + from_language = from_language || default_language() + to_language = to_language || default_language() + + with {translated, 0} <- + safe_translate(string, from_language, to_language) do + {:ok, from_language, translated} + else + {response, _} -> {:error, "ArgosTranslate failed to translate (#{response})"} + end + end +end diff --git a/test/pleroma/akkoma/translators/argos_translate_test.exs b/test/pleroma/akkoma/translators/argos_translate_test.exs new file mode 100644 index 000000000..4c7c67d5a --- /dev/null +++ b/test/pleroma/akkoma/translators/argos_translate_test.exs @@ -0,0 +1,66 @@ +defmodule Pleroma.Akkoma.Translators.ArgosTranslateTest do + alias Pleroma.Akkoma.Translators.ArgosTranslate + + import Mock + + use Pleroma.DataCase, async: true + + setup do + clear_config([:argos_translate, :command_argos_translate], "argos-translate_test") + clear_config([:argos_translate, :command_argospm], "argospm_test") + end + + test "it lists available languages" do + languages = + with_mock System, [:passthrough], + cmd: fn "argospm_test", ["list"], _ -> + {"translate-nl_en\ntranslate-en_nl\ntranslate-ja_en\n", 0} + end do + ArgosTranslate.languages() + end + + assert {:ok, source_langs, dest_langs} = languages + + assert [%{code: "en", name: "en"}, %{code: "ja", name: "ja"}, %{code: "nl", name: "nl"}] = + source_langs |> Enum.sort() + + assert [%{code: "en", name: "en"}, %{code: "nl", name: "nl"}] = dest_langs |> Enum.sort() + end + + test "it translates from default language when no language is set" do + translation_response = + with_mock System, [:passthrough], + cmd: fn "argos-translate_test", ["--from-lang", "en", "--to-lang", "fr", "blabla"], _ -> + {"yadayada", 0} + end do + ArgosTranslate.translate("blabla", nil, "fr") + end + + assert {:ok, "en", "yadayada"} = translation_response + end + + test "it translates from the provided language" do + translation_response = + with_mock System, [:passthrough], + cmd: fn "argos-translate_test", ["--from-lang", "nl", "--to-lang", "en", "blabla"], _ -> + {"yadayada", 0} + end do + ArgosTranslate.translate("blabla", "nl", "en") + end + + assert {:ok, "nl", "yadayada"} = translation_response + end + + test "it returns a proper error when the executable can't be found" do + non_existing_command = "sfqsfgqsefd" + clear_config([:argos_translate, :command_argos_translate], non_existing_command) + clear_config([:argos_translate, :command_argospm], non_existing_command) + + assert nil == System.find_executable(non_existing_command) + + assert {:error, "ArgosTranslate failed to fetch languages" <> _} = ArgosTranslate.languages() + + assert {:error, "ArgosTranslate failed to translate" <> _} = + ArgosTranslate.translate("blabla", "nl", "en") + end +end