diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/.gitignore b/.gitignore index 4d43bb0..13e49d3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,12 @@ # The directory Mix will write compiled artifacts to. /_build/ +/.elixir_ls/ +/priv/input/ +/priv/output/ + +/config/*.secret.exs +/config/*.pem +/config/*.key # If you run "mix test --cover", coverage assets end up here. /cover/ diff --git a/.iex.exs b/.iex.exs new file mode 100644 index 0000000..045bf55 --- /dev/null +++ b/.iex.exs @@ -0,0 +1,4 @@ +alias Translator.Languages +alias Translator.Batcher +alias Translator.Parser +alias Translator.Parser.{Base, JSON} diff --git a/README.md b/README.md new file mode 100644 index 0000000..3a680e9 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# Translator + +**TODO: Add description** + +## Installation + +If [available in Hex](https://hex.pm/docs/publish), the package can be installed +by adding `translator` to your list of dependencies in `mix.exs`: + +```elixir +def deps do + [ + {:translator, "~> 0.1.0"} + ] +end +``` + +Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) +and published on [HexDocs](https://hexdocs.pm). Once published, the docs can +be found at [https://hexdocs.pm/translator](https://hexdocs.pm/translator). + diff --git a/config/config.exs b/config/config.exs new file mode 100644 index 0000000..e1a4a52 --- /dev/null +++ b/config/config.exs @@ -0,0 +1,12 @@ +use Mix.Config + +# Configures Elixir's Logger +config :logger, :console, + format: "$time $metadata[$level] $message\n", + metadata: [:request_id] + +config :translator, + parsers: [Translator.Parser.YAML, Translator.Parser.JSON], + availiable_languages: ~w(en zh ar es hi de ru be uk kk bn pl pt it fr) + +import_config "#{Mix.env()}.exs" diff --git a/config/dev.exs b/config/dev.exs new file mode 100644 index 0000000..d3a1175 --- /dev/null +++ b/config/dev.exs @@ -0,0 +1,3 @@ +import Config + +import_config "dev.secret.exs" diff --git a/config/prod.exs b/config/prod.exs new file mode 100644 index 0000000..becde76 --- /dev/null +++ b/config/prod.exs @@ -0,0 +1 @@ +import Config diff --git a/config/test.exs b/config/test.exs new file mode 100644 index 0000000..becde76 --- /dev/null +++ b/config/test.exs @@ -0,0 +1 @@ +import Config diff --git a/lib/translator/application.ex b/lib/translator/application.ex new file mode 100644 index 0000000..7b2ccd0 --- /dev/null +++ b/lib/translator/application.ex @@ -0,0 +1,19 @@ +defmodule Translator.Application do + # See https://hexdocs.pm/elixir/Application.html + # for more information on OTP Applications + @moduledoc false + + use Application + + def start(_type, _args) do + children = [ + # Starts a worker by calling: Translator.Worker.start_link(arg) + # {Translator.Worker, arg} + ] + + # See https://hexdocs.pm/elixir/Supervisor.html + # for other strategies and supported options + opts = [strategy: :one_for_one, name: Translator.Supervisor] + Supervisor.start_link(children, opts) + end +end diff --git a/lib/translator/batcher.ex b/lib/translator/batcher.ex new file mode 100644 index 0000000..3fbc930 --- /dev/null +++ b/lib/translator/batcher.ex @@ -0,0 +1,28 @@ +defmodule Translator.Batcher do + alias Translator.Languages + + @spec parse(String.t()) :: {:ok, map} | {:error, String.t()} + def parse(path) do + batch = Path.basename(path) + + folder = + Path.join(path, Languages.pattern()) + |> Path.wildcard() + |> List.first() + + case folder do + nil -> + {:error, "Couldn't find input folder in batch '#{path}'"} + + _ -> + {:ok, + %{ + name: batch, + folder: path, + input: folder, + language: Path.basename(folder), + files: Path.wildcard(Path.join(folder, "**")) + }} + end + end +end diff --git a/lib/translator/languages.ex b/lib/translator/languages.ex new file mode 100644 index 0000000..27ed07a --- /dev/null +++ b/lib/translator/languages.ex @@ -0,0 +1,4 @@ +defmodule Translator.Languages do + def list(), do: Application.get_env(:translator, :availiable_languages) + def pattern(), do: "{" <> Enum.join(list(), ",") <> "}" +end diff --git a/lib/translator/parser.ex b/lib/translator/parser.ex new file mode 100644 index 0000000..5f4cde9 --- /dev/null +++ b/lib/translator/parser.ex @@ -0,0 +1,72 @@ +defmodule Translator.Parser do + @type filename :: String.t() + @type extension :: String.t() + @type data :: map + @type parser :: atom + @type availiable_parsers_map :: %{required(extension) => parser} + + @spec load(filename) :: {:error, any} | {:ok, any} + def load(filename) do + case select_parser(filename) do + {:error, message} -> {:error, message} + {:ok, parser} -> load(filename, parser) + end + end + + @spec load(filename, parser) :: {:error, any} | {:ok, any} + def load(filename, parser) do + with {:ok, contents} <- File.read(filename), + {:ok, result} <- parser.parse(contents) do + {:ok, result} + else + {:error, error} -> {:error, error} + end + end + + @spec save(filename, data) :: {:error, any} | {:ok, any} + def save(filename, data) do + case select_parser(filename) do + {:error, message} -> {:error, message} + {:ok, parser} -> save(filename, data, parser) + end + end + + @spec save(filename, data, parser) :: :ok | {:error, any} + def save(filename, data, parser) do + with {:ok, contents} <- parser.generate(data), + :ok <- File.write(filename, contents) do + :ok + else + {:error, error} -> {:error, error} + end + end + + @spec select_parser(filename) :: {:ok, atom} | {:error, any} + defp select_parser(filename) do + case select_parser!(filename) do + nil -> {:error, "Couldn't find availiable parser for this extension"} + value -> {:ok, value} + end + end + + @spec select_parser!(filename) :: atom + defp select_parser!(filename), + do: Map.get(availiable(), String.trim_leading(Path.extname(filename), ".")) + + @spec availiable() :: availiable_parsers_map + defp availiable() do + parsers = Application.get_env(:translator, :parsers) + + parsers + |> Enum.reduce(%{}, fn parser, avaliable_extensions_map -> + {:ok, extensions} = parser.extensions() + + parser_extensions_map = + extensions + |> Enum.map(&{&1, parser}) + |> Map.new() + + Map.merge(avaliable_extensions_map, parser_extensions_map) + end) + end +end diff --git a/lib/translator/parser/base.ex b/lib/translator/parser/base.ex new file mode 100644 index 0000000..55f54fd --- /dev/null +++ b/lib/translator/parser/base.ex @@ -0,0 +1,20 @@ +defmodule Translator.Parser.Base do + @typedoc """ + Text contents of the file for parse in Map + """ + @type contents :: String.t() + + @typedoc """ + Data in Map format for encoding to text contents of the file + """ + @type data :: map + + @typedoc """ + List of the file extensions for parser + """ + @type extensions :: [String.t()] + + @callback parse(contents) :: {:ok, data} | {:error, any} + @callback generate(data) :: {:ok, contents} | {:error, any} + @callback extensions() :: {:ok, extensions} +end diff --git a/lib/translator/parser/json.ex b/lib/translator/parser/json.ex new file mode 100644 index 0000000..7dde575 --- /dev/null +++ b/lib/translator/parser/json.ex @@ -0,0 +1,25 @@ +defmodule Translator.Parser.JSON do + @extensions ["json"] + + @type contents :: String.t() + @type data :: map + @type extensions :: [String.t()] + + @behaviour Translator.Parser.Base + + @impl Translator.Parser.Base + @spec parse(contents) :: {:ok, data} | {:error, atom | Jason.DecodeError.t()} + def parse(contents) do + Jason.decode(contents) + end + + @impl Translator.Parser.Base + @spec generate(data) :: {:ok, contents} | {:error, any} + def generate(data) do + Jason.encode(data) + end + + @impl Translator.Parser.Base + @spec extensions() :: {:ok, extensions} + def extensions(), do: {:ok, @extensions} +end diff --git a/lib/translator/parser/yaml.ex b/lib/translator/parser/yaml.ex new file mode 100644 index 0000000..074e127 --- /dev/null +++ b/lib/translator/parser/yaml.ex @@ -0,0 +1,41 @@ +defmodule Translator.Parser.YAML do + @extensions ["yml", "yaml"] + + @type contents :: String.t() + @type data :: map + @type extensions :: [String.t()] + + @behaviour Translator.Parser.Base + + @impl Translator.Parser.Base + @spec parse(contents) :: {:ok, data} | {:error, atom | Jason.DecodeError.t()} + def parse(contents) do + YamlElixir.read_from_string(contents) + end + + @impl Translator.Parser.Base + @spec generate(data) :: {:ok, contents} | {:error, any} + def generate(data) do + {:ok, to_yaml(data)} + end + + defp to_yaml(data, indentation \\ "") do + data + |> Map.keys() + |> Enum.map(fn key -> + Map.fetch!(data, key) + |> value_to_yaml(key, indentation) + end) + |> Enum.join("\n") + end + + defp value_to_yaml(value, key, indentation) when is_number(value) or is_bitstring(value), + do: "#{indentation}#{key}: #{value}" + + defp value_to_yaml(value, key, indentation) when is_map(value), + do: "#{indentation}#{key}:\n#{to_yaml(value, "#{indentation} ")}" + + @impl Translator.Parser.Base + @spec extensions() :: {:ok, extensions} + def extensions(), do: {:ok, @extensions} +end diff --git a/lib/translator/translator.ex b/lib/translator/translator.ex new file mode 100644 index 0000000..b05a0c6 --- /dev/null +++ b/lib/translator/translator.ex @@ -0,0 +1,4 @@ +defmodule Translator do + defdelegate translate(text, to, from), to: YandexTranslate + defdelegate translate(text, to), to: YandexTranslate +end diff --git a/lib/translator/watcher.ex b/lib/translator/watcher.ex new file mode 100644 index 0000000..82e2dd0 --- /dev/null +++ b/lib/translator/watcher.ex @@ -0,0 +1,25 @@ +defmodule Translator.Watcher do + use GenServer + + @spec start_link(any) :: :ignore | {:error, any} | {:ok, pid} + def start_link(args) do + GenServer.start_link(__MODULE__, args) + end + + @spec init(keyword) :: {:ok, %{watcher_pid: pid}} + def init(args) do + {:ok, watcher_pid} = FileSystem.start_link(args) + FileSystem.subscribe(watcher_pid) + {:ok, %{watcher_pid: watcher_pid}} + end + + def handle_info({:file_event, watcher_pid, {path, events}}, %{watcher_pid: watcher_pid} = state) do + # YOUR OWN LOGIC FOR PATH AND EVENTS + {:noreply, state} + end + + def handle_info({:file_event, watcher_pid, :stop}, %{watcher_pid: watcher_pid} = state) do + # YOUR OWN LOGIC WHEN MONITOR STOP + {:noreply, state} + end +end diff --git a/mix.exs b/mix.exs new file mode 100644 index 0000000..ead128e --- /dev/null +++ b/mix.exs @@ -0,0 +1,44 @@ +defmodule Translator.MixProject do + use Mix.Project + + def project do + [ + app: :translator, + version: "0.1.0", + elixir: "~> 1.10", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + mod: {Translator.Application, []}, + extra_applications: applications(Mix.env()) + ] + end + + defp applications(:dev), do: applications(:all) ++ [:remix] + defp applications(_all), do: [:logger] + + # Run "mix help deps" to learn about dependencies. + defp deps do + [ + # Translators + {:yandex_translate, "~> 0.4.0"}, + + # Parsers/generators + {:jason, "~> 1.2.0"}, + {:yaml_elixir, "~> 2.4.0"}, + + # File system monitoring + {:file_system, "~> 0.2.8"}, + + # Remix for autorestart + {:ex_doc, "~> 0.21.3", only: :dev, runtime: false}, + {:credo, "~> 1.4.0-rc.1", only: :dev, runtime: false}, + {:remix, "~> 0.0.2", only: :dev} + ] + end +end diff --git a/mix.lock b/mix.lock new file mode 100644 index 0000000..668f9eb --- /dev/null +++ b/mix.lock @@ -0,0 +1,19 @@ +%{ + "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, + "castore": {:hex, :castore, "0.1.5", "591c763a637af2cc468a72f006878584bc6c306f8d111ef8ba1d4c10e0684010", [:mix], [], "hexpm", "6db356b2bc6cc22561e051ff545c20ad064af57647e436650aa24d7d06cd941a"}, + "credo": {:hex, :credo, "1.4.0-rc.2", "5a618536557c6bacc1ad0b13eb93c271f29946394b23a23e5f5c5a327e10ae52", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "a40447f2fb853824e5acbd0f69100a0a2794c6b62d5774d6566e7601ad727cca"}, + "earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm", "8cf8a291ebf1c7b9539e3cddb19e9cef066c2441b1640f13c34c1d3cfc825fec"}, + "ex_doc": {:hex, :ex_doc, "0.21.3", "857ec876b35a587c5d9148a2512e952e24c24345552259464b98bfbb883c7b42", [:mix], [{:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "0db1ee8d1547ab4877c5b5dffc6604ef9454e189928d5ba8967d4a58a801f161"}, + "file_system": {:hex, :file_system, "0.2.8", "f632bd287927a1eed2b718f22af727c5aeaccc9a98d8c2bd7bff709e851dc986", [:mix], [], "hexpm", "97a3b6f8d63ef53bd0113070102db2ce05352ecf0d25390eb8d747c2bde98bca"}, + "jason": {:hex, :jason, "1.2.0", "10043418c42d2493d0ee212d3fddd25d7ffe484380afad769a0a38795938e448", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "116747dbe057794c3a3e4e143b7c8390b29f634e16c78a7f59ba75bfa6852e7f"}, + "joken": {:hex, :joken, "2.2.0", "2daa1b12be05184aff7b5ace1d43ca1f81345962285fff3f88db74927c954d3a", [:mix], [{:jose, "~> 1.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "b4f92e30388206f869dd25d1af628a1d99d7586e5cf0672f64d4df84c4d2f5e9"}, + "jose": {:hex, :jose, "1.10.1", "16d8e460dae7203c6d1efa3f277e25b5af8b659febfc2f2eb4bacf87f128b80a", [:mix, :rebar3], [], "hexpm", "3c7ddc8a9394b92891db7c2771da94bf819834a1a4c92e30857b7d582e2f8257"}, + "makeup": {:hex, :makeup, "1.0.1", "82f332e461dc6c79dbd82fbe2a9c10d48ed07146f0a478286e590c83c52010b5", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "49736fe5b66a08d8575bf5321d716bac5da20c8e6b97714fec2bcd6febcfa1f8"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "d4b316c7222a85bbaa2fd7c6e90e37e953257ad196dc229505137c5e505e9eff"}, + "mint": {:hex, :mint, "1.0.0", "ca5ab33497ba2bdcc42f6cdd3927420a6159116be87c8173658e93c8746703da", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "b8943ef1e630879538dd6620bfc189d4d75fab3ad39f3fe9c50539879f7efd84"}, + "nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm", "589b5af56f4afca65217a1f3eb3fee7e79b09c40c742fddc1c312b3ac0b3399f"}, + "remix": {:hex, :remix, "0.0.2", "f06115659d8ede8d725fae1708920ef73353a1b39efe6a232d2a38b1f2902109", [:mix], [], "hexpm", "5f5555646ed4fca83fab8620735150aa0bc408c5a17a70d28cfa7086bc6f497c"}, + "yamerl": {:hex, :yamerl, "0.7.0", "e51dba652dce74c20a88294130b48051ebbbb0be7d76f22de064f0f3ccf0aaf5", [:rebar3], [], "hexpm", "cb5a4481e2e2ad36db83bd9962153e1a9208e2b2484185e33fc2caac6a50b108"}, + "yaml_elixir": {:hex, :yaml_elixir, "2.4.0", "2f444abc3c994c902851fde56b6a9cb82895c291c05a0490a289035c2e62ae71", [:mix], [{:yamerl, "~> 0.7", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "4e25a6d5c873e393689c6f1062c5ec90f6cd1be2527b073178ae37eae4c78bee"}, + "yandex_translate": {:hex, :yandex_translate, "0.4.0", "a8851bdd0899d29334c9028aecc70c9921cae5dff86e512e30e72a0bceb6613a", [:mix], [{:castore, "~> 0.1.5", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.2.0", [hex: :jason, repo: "hexpm", optional: false]}, {:joken, "~> 2.2.0", [hex: :joken, repo: "hexpm", optional: false]}, {:mint, "~> 1.0.0", [hex: :mint, repo: "hexpm", optional: false]}], "hexpm", "d4cdb333d8e447d0de20e0314ffc5fdd0a38421800866801007baca9102936f1"}, +} diff --git a/test/test_helper.exs b/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start() diff --git a/test/translator_test.exs b/test/translator_test.exs new file mode 100644 index 0000000..1c2e8ea --- /dev/null +++ b/test/translator_test.exs @@ -0,0 +1,8 @@ +defmodule TranslatorTest do + use ExUnit.Case + doctest Translator + + test "greets the world" do + assert Translator.hello() == :world + end +end