First commit

This commit is contained in:
Danil Negrienko 2020-04-20 18:18:51 +03:00
parent a14860401e
commit b45a818fa4
21 changed files with 363 additions and 0 deletions

4
.formatter.exs Normal file
View File

@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

7
.gitignore vendored
View File

@ -1,5 +1,12 @@
# The directory Mix will write compiled artifacts to. # The directory Mix will write compiled artifacts to.
/_build/ /_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. # If you run "mix test --cover", coverage assets end up here.
/cover/ /cover/

4
.iex.exs Normal file
View File

@ -0,0 +1,4 @@
alias Translator.Languages
alias Translator.Batcher
alias Translator.Parser
alias Translator.Parser.{Base, JSON}

21
README.md Normal file
View File

@ -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).

12
config/config.exs Normal file
View File

@ -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"

3
config/dev.exs Normal file
View File

@ -0,0 +1,3 @@
import Config
import_config "dev.secret.exs"

1
config/prod.exs Normal file
View File

@ -0,0 +1 @@
import Config

1
config/test.exs Normal file
View File

@ -0,0 +1 @@
import Config

View File

@ -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

28
lib/translator/batcher.ex Normal file
View File

@ -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

View File

@ -0,0 +1,4 @@
defmodule Translator.Languages do
def list(), do: Application.get_env(:translator, :availiable_languages)
def pattern(), do: "{" <> Enum.join(list(), ",") <> "}"
end

72
lib/translator/parser.ex Normal file
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,4 @@
defmodule Translator do
defdelegate translate(text, to, from), to: YandexTranslate
defdelegate translate(text, to), to: YandexTranslate
end

25
lib/translator/watcher.ex Normal file
View File

@ -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

44
mix.exs Normal file
View File

@ -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

19
mix.lock Normal file
View File

@ -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"},
}

1
test/test_helper.exs Normal file
View File

@ -0,0 +1 @@
ExUnit.start()

8
test/translator_test.exs Normal file
View File

@ -0,0 +1,8 @@
defmodule TranslatorTest do
use ExUnit.Case
doctest Translator
test "greets the world" do
assert Translator.hello() == :world
end
end