commit e8dad52c365df302d574acd8efd43e8017c8c787 Author: Danylo Negriienko Date: Fri Jun 19 08:45:38 2020 +0300 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..4c4868b --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# The dialyzer directory. +/.elixir_ls/ + +# VSCode Artifacts +/.vscode/ + + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where 3rd-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Ignore package tarball (built via "mix hex.build"). +microsoft_translator-*.tar + +/config/config.exs \ No newline at end of file diff --git a/.iex.exs b/.iex.exs new file mode 100644 index 0000000..789f27e --- /dev/null +++ b/.iex.exs @@ -0,0 +1 @@ +alias MicrosoftTranslator.Client diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100755 index 0000000..6747917 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +## [0.1.0] - 2019-06-17 +### Added +- initial version + +### Modified +- code refactoring +- tests diff --git a/README.md b/README.md new file mode 100755 index 0000000..18d4058 --- /dev/null +++ b/README.md @@ -0,0 +1,87 @@ + + +# Microsoft Translator + +A simple Elixir interface to Microsoft Translator translation API in Azure. The Translator is a cloud-based machine translation service. The core service is the Translator, which powers a number of Microsoft products and services, and is used by thousands of businesses worldwide in their applications and workflows, which allows their content to reach a global audience. + +## Installation + +If [available in Hex](https://hex.pm/docs/publish), the package can be installed +by adding `microsoft_translator` to your list of dependencies in `mix.exs`: + +```elixir +def deps do + [ + {:microsoft_translator, "~> 0.1.0"} + ] +end +``` + +## Get access to API + +### Creating a account in the Microsoft Azure + +To perform operations in Microsoft Azure via the API, you need to: +- [Sign in](https://ms.portal.azure.com/) to the Azure portal. +- Create a subscription for Translator. Create a resource in the **Search the Marketplace search box**, enter **Translator** and then select it. +- Select **Create** to define details for the subscription. +- From the **Pricing tier** list, select the pricing tier that best fits your needs. +- Select **Create** to finish creating the subscription. + +### Getting a Authentication key + +When you sign up for Translator, you get a personalized access key unique to your subscription. This key is required on each call to the Translator. + +1. Retrieve your authentication key by first selecting the appropriate subscription. +1. Select **Keys** in the **Resource Management** section of your subscription's details. +1. Copy either of the keys listed for your subscription. + +```elixir +config :microsoft_translator, + api_key: "KEY_1 or KEY_2" + endpoint: "ENDPOINT" +``` + +## Usage + +### Supported languages + +Request for getting list of supported languages is #languages. + +```elixir + MicrosoftTranslator.languages() +``` + +### Language detection + +Request for detecting language of text is #detect. + +```elixir + MicrosoftTranslator.detect("Hello") +``` + +### Translation + +Request for translating text is #translate. + +```elixir + MicrosoftTranslator.translate("Hello", "uk") +``` + +## Contributing + +Bug reports and pull requests are welcome on at https://gl.negrienko.com/negrienko/microsoft_translator. + +## License + +The hex is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). + +## Disclaimer + +Use this package at your own peril and risk. + +## Documentation + +Documentation 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/microsoft_translator](https://hexdocs.pm/microsoft_translator). diff --git a/assets/microsoft_translator.png b/assets/microsoft_translator.png new file mode 100644 index 0000000..d7800d6 Binary files /dev/null and b/assets/microsoft_translator.png differ diff --git a/assets/microsoft_translator.svg b/assets/microsoft_translator.svg new file mode 100644 index 0000000..e634626 --- /dev/null +++ b/assets/microsoft_translator.svg @@ -0,0 +1,15 @@ + + + + microsoft_translator + Created with Sketch. + + + + + + + + + + \ No newline at end of file diff --git a/assets/microsoft_translator@2x.png b/assets/microsoft_translator@2x.png new file mode 100644 index 0000000..cde64ff Binary files /dev/null and b/assets/microsoft_translator@2x.png differ diff --git a/config/config.exs.example b/config/config.exs.example new file mode 100755 index 0000000..0219574 --- /dev/null +++ b/config/config.exs.example @@ -0,0 +1,6 @@ +# use Mix.Config + +# config :microsoft_translator, +# api_key: "API_KEY" +# region: "REGION" +# endpoint: "ENDPOINT" diff --git a/lib/microsoft_translator.ex b/lib/microsoft_translator.ex new file mode 100755 index 0000000..b61eb5d --- /dev/null +++ b/lib/microsoft_translator.ex @@ -0,0 +1,66 @@ +defmodule MicrosoftTranslator do + @moduledoc """ + Basic functions for requests to Yandex Translate API on Yandex Cloud + """ + + alias MicrosoftTranslator.Client + + @doc """ + Retrieves the list of supported languages. + + Return a map with language code (use it for translations) and native language name + + ```elixir + %{ + languages: [ + %{code: "af", "name":"Afrikaans","nativeName":"Afrikaans"}, + %{code: "ar", "name":"Arabic","nativeName":"العربية"}, + %{code: "bg", "name":"Bulgarian","nativeName":"Български"}, + %{code: "bn", "name":"Bangla","nativeName":"বাংলা"}, + %{code: "bs", "name":"Bosnian","nativeName":"bosanski (latinica)"} + ... + ] + } + ``` + """ + def languages(), do: Client.call(:languages, %{}) + + @doc """ + Detect the language of the text + + Get text as a string param and return a map with language code + + ```elixir + MicrosoftTranslator.detect("Криївка") + # Response + %{languageCode: "uk"} + ``` + + Or get a map with :text and :languageCodeHints (for specify the most likely languages). + > In some languages, one and the same word has the same spelling. For example, the English word “hand” is also written as “hand” in German, Swedish, and Dutch. If the text you transmit contains words like this, Translate may detect the source language incorrectly. + + To avoid mistakes, you can use the languageCodeHints field to specify which languages should be given priority when determining the language of the text + + ```elixir + MicrosoftTranslator.detect(%{text: "Капелюх"}) + # Response + %{languageCode: "uk"} + ``` + """ + def detect(params) when is_map(params), do: Client.call(:detect, params) + def detect(text) when is_binary(text), do: Client.call(:detect, %{text: text}) + + def translate(params) when is_map(params), do: Client.call(:translate, params) + + def translate(text, to) when is_binary(text) and is_binary(to), + do: Client.call(:translate, %{text: text, to: to}) + + def translate(text, to, from) + when is_binary(text) and is_binary(to) and is_binary(from), + do: + Client.call(:translate, %{ + text: text, + to: to, + from: from + }) +end diff --git a/lib/microsoft_translator/auth/api_key.ex b/lib/microsoft_translator/auth/api_key.ex new file mode 100644 index 0000000..ef48680 --- /dev/null +++ b/lib/microsoft_translator/auth/api_key.ex @@ -0,0 +1,12 @@ +defmodule MicrosoftTranslator.Auth.ApiKey do + def get_auth_headers(%{api_key: api_key, region: region}) do + [ + {"Ocp-Apim-Subscription-Key", api_key}, + {"Ocp-Apim-Subscription-Region", region} + ] + end + + def get_auth_headers(%{api_key: api_key}) do + [{"Ocp-Apim-Subscription-Key", api_key}] + end +end diff --git a/lib/microsoft_translator/auth/auth.ex b/lib/microsoft_translator/auth/auth.ex new file mode 100644 index 0000000..9bdd8fc --- /dev/null +++ b/lib/microsoft_translator/auth/auth.ex @@ -0,0 +1,10 @@ +defmodule MicrosoftTranslator.Auth do + alias MicrosoftTranslator.Auth.{ApiKey} + + defp get_config(), do: Application.get_all_env(:microsoft_translator) |> Map.new() + + def authorization_headers(config \\ get_config()) + + def authorization_headers(%{api_key: _} = params), + do: ApiKey.get_auth_headers(params) +end diff --git a/lib/microsoft_translator/client.ex b/lib/microsoft_translator/client.ex new file mode 100644 index 0000000..06d6d19 --- /dev/null +++ b/lib/microsoft_translator/client.ex @@ -0,0 +1,150 @@ +defmodule MicrosoftTranslator.Client do + @empty_state %{data: [], done: false} + @base_host "api.cognitive.microsofttranslator.com" + @base_path "/" + @api_methods %{ + languages: %{ + method: "GET", + path: "languages" + }, + detect: %{ + method: "POST", + path: "detect" + }, + translate: %{ + method: "POST", + path: "translate" + } + } + @api_default_params %{ + "api-version" => "3.0" + } + @availaible_api_methods Map.keys(@api_methods) + @default_headers [{"Content-Type", "application/json"}] + + alias MicrosoftTranslator.Auth + + def call(api_method \\ :languages, args \\ %{}) + when api_method in @availaible_api_methods and is_map(args) do + body = generate_body(api_method, args) + headers = generate_headers() + params = generate_params(api_method, args) + + api_method + |> fetch(headers, body, params) + |> parse(api_method) + end + + defp generate_headers(headers \\ []) + + defp generate_headers(header) when is_tuple(header), + do: generate_headers([header]) + + defp generate_headers(headers) when is_list(headers), + do: @default_headers ++ Auth.authorization_headers() ++ headers + + defp generate_params(:translate, params) do + params + |> Map.take([:to, :from]) + |> params_to_string() + end + + defp generate_params(api_method, _params) when api_method in [:detect, :languages], + do: params_to_string(%{}) + + defp params_to_string(params_map) do + params_map + |> Map.merge(@api_default_params) + |> Enum.reduce("", fn {key, value}, acc -> acc <> "&" <> "#{key}" <> "=" <> "#{value}" end) + |> String.replace_leading("&", "?") + end + + defp generate_body(method, params), do: transform_body(method, params) |> Jason.encode!() + + defp transform_body(:languages, _), do: %{} + + defp transform_body(method, params) when method in [:detect, :translate], + do: [%{text: Map.fetch!(params, :text)}] + + defp transform_body(_, params), do: params + + defp parse({:ok, %{data: body}}, api_method) do + Jason.decode!(body, keys: :atoms) + |> transform_response(api_method) + end + + defp transform_response(response, :detect) do + result = + response + |> List.first() + |> Map.fetch(:language) + + case result do + {:ok, language} -> %{languageCode: language} + end + end + + defp transform_response(response, :translate) do + result = + response + |> List.first() + |> Map.take([:translations]) + end + + defp transform_response(response, :languages) do + %{dictionary: dictionary} = response + + dictionary + |> Enum.map(fn {key, %{name: name, nativeName: native, dir: dir}} -> + %{code: "#{key}", name: name, nativeName: native, dir: dir} + end) + end + + def fetch(api_method, headers, body, params) do + method = @api_methods[api_method].method + path = @base_path <> @api_methods[api_method].path <> params + + with {:ok, conn} <- Mint.HTTP.connect(:https, @base_host, 443), + {:ok, conn, _ref} <- Mint.HTTP.request(conn, method, path, headers, body) do + handle_response(conn, @empty_state) + end + end + + defp handle_response(conn, state) do + receive do + message -> + case Mint.HTTP.stream(conn, message) do + {:ok, conn, responses} -> + case Enum.reduce(responses, state, &handle_res/2) do + # Loop ends here + %{done: true} = state -> {:ok, state} + %{done: false} = state -> handle_response(conn, state) + end + + {:error, _, reason, _} -> + {:error, reason} + + :unknown -> + exit({:unexpected, message}) + end + end + end + + defp handle_res({:status, _, status}, state), + do: Map.put(state, :status, status) + + defp handle_res({:headers, _, headers}, state), + do: Map.put(state, :headers, headers) + + defp handle_res({:data, _, data}, state), + do: Map.update!(state, :data, fn acc -> [data | acc] end) + + defp handle_res({:done, _}, state) do + Map.update!(state, :data, fn acc -> + acc + |> Enum.reverse() + |> Enum.join("") + end) + |> Map.put(:done, true) + end +end diff --git a/mix.exs b/mix.exs new file mode 100755 index 0000000..7b8c6aa --- /dev/null +++ b/mix.exs @@ -0,0 +1,64 @@ +defmodule MicrosoftTranslator.MixProject do + use Mix.Project + + @name "MicrosoftTranslator" + @version "0.4.0" + # @repo_url "https://github.com/negrienko/microsoft_translator" + @repo_url "https://gl.negrienko.com/negrienko/microsoft_translator" + @homepage_url "https://negrienko.com/all/microsoft-translator/" + @author_url "https://negrienko.com/" + @description """ + Translate word and phrases using the Microsoft Translator API. See README.md for information. + """ + + def project do + [ + app: :microsoft_translator, + version: @version, + elixir: "~> 1.9", + build_embedded: Mix.env() == :prod, + start_permanent: Mix.env() == :prod, + package: package(), + deps: deps(), + # Docs + name: @name, + description: @description, + source_url: @repo_url, + homepage_url: @homepage_url, + docs: [ + logo: "assets/microsoft_translator.svg", + main: @name, + source_ref: @version, + source_url: @repo_url + ] + ] + end + + def application do + [extra_applications: applications(Mix.env())] + end + + defp applications(:dev), do: applications(:all) ++ [:remix] + defp applications(_all), do: [:logger, :jose] + + defp deps do + [ + {:joken, "~> 2.2.0"}, + {:jose, "~> 1.10.1"}, + {:jason, "~> 1.2.0"}, + {:mint, "~> 1.1.0"}, + {:castore, "~> 0.1.6"}, + {:ex_spec, "~> 2.0.1", only: :test}, + {:ex_doc, "~> 0.21.3", only: :dev}, + {:remix, "~> 0.0.2", only: :dev} + ] + end + + defp package do + [ + maintainers: ["Danylo Negriienko"], + licenses: ["MIT"], + links: %{"git" => @repo_url, "author" => @author_url, "homepage" => @homepage_url} + ] + end +end diff --git a/mix.lock b/mix.lock new file mode 100644 index 0000000..7423e65 --- /dev/null +++ b/mix.lock @@ -0,0 +1,28 @@ +%{ + "base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm"}, + "castore": {:hex, :castore, "0.1.6", "2da0dccb3eacb67841d11790598ff03cd5caee861e01fad61dce1376b5da28e6", [:mix], [], "hexpm", "f874c510b720d31dd6334e9ae5c859a06a3c9e67dfe1a195c512e57588556d3f"}, + "certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, + "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"}, + "ex_spec": {:hex, :ex_spec, "2.0.1", "8bdbd6fa85995fbf836ed799571d44be6f9ebbcace075209fd0ad06372c111cf", [:mix], [], "hexpm", "b44fe5054497411a58341ece5bf7756c219d9d6c1303b5ac467f557a0a4c31ac"}, + "hackney": {:hex, :hackney, "1.15.1", "9f8f471c844b8ce395f7b6d8398139e26ddca9ebc171a8b91342ee15a19963f4", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, + "httpoison": {:hex, :httpoison, "1.5.1", "0f55b5b673b03c5c327dac7015a67cb571b99b631acc0bc1b0b98dcd6b9f2104", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, + "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, + "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"}, + "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, + "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm"}, + "mint": {:hex, :mint, "1.1.0", "1fd0189edd9e3ffdbd7fcd8bc3835902b987a63ec6c4fd1aa8c2a56e2165f252", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "5bfd316c3789340b682d5679a8116bcf2112e332447bdc20c1d62909ee45f48d"}, + "mojito": {:hex, :mojito, "0.3.0", "806cd3c1832333a9ee784e7ea2799863fbe4de55ecb4623a8f4ef870c2844cc6", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: false]}, {:mint, "~> 0.2.1", [hex: :mint, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}], "hexpm"}, + "nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm", "589b5af56f4afca65217a1f3eb3fee7e79b09c40c742fddc1c312b3ac0b3399f"}, + "ojson": {:hex, :ojson, "1.0.0", "fd28614eadaec00a15cdb2f53f29d8717a812a508ddb80d202f2f2e2aaeabbcc", [:mix, :rebar3], [], "hexpm"}, + "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"}, + "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, + "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm"}, + "remix": {:hex, :remix, "0.0.2", "f06115659d8ede8d725fae1708920ef73353a1b39efe6a232d2a38b1f2902109", [:mix], [], "hexpm", "5f5555646ed4fca83fab8620735150aa0bc408c5a17a70d28cfa7086bc6f497c"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"}, +} diff --git a/test/test_helper.exs b/test/test_helper.exs new file mode 100755 index 0000000..869559e --- /dev/null +++ b/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start() diff --git a/test/yandex_translate_test.exs b/test/yandex_translate_test.exs new file mode 100755 index 0000000..a657416 --- /dev/null +++ b/test/yandex_translate_test.exs @@ -0,0 +1,3 @@ +defmodule YandexTranslatorTest do + use ExUnit.Case +end