Packages

Inofficial Erlang/OTP client application for the official DeepL API Version 2

Current section

Files

Jump to
deeperl src deeperl.erl
Raw

src/deeperl.erl

-module(deeperl).
-behaviour(gen_server).
%% API
-export([start_link/0, usage/0, languages/0, translate/2, translate/3]).
-export_type([translation_options/0]).
-type language() :: de | en | es | fr | it | nl | pl | pt | ru.
-type tag_list() :: [nonempty_string()].
-type translation_options() :: #{
source_lang => language(),
split_sentences => boolean() | nonewlines,
preserve_formatting => boolean(),
tag_handling => xml,
non_splitting_tags => tag_list(),
outline_detection => boolean(),
splitting_tags => tag_list(),
ignore_tags => tag_list()
}.
%% gen_server callbacks
-export([init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2,
code_change/3]).
-define(SERVER, ?MODULE).
-record(state, {
conn :: pid(),
conn_mref :: erlang:monitor_process_identifier()
}).
%%%===================================================================
%%% API
%%%===================================================================
%% @private
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
%% @private
init(_) ->
GunOpts = case application:get_env(gun_opts) of
undefined -> #{};
{ok, Opts} -> Opts
end,
{ok, ConnPid} = gun:open("api.deepl.com", 443, GunOpts),
{ok, #state{
conn = ConnPid,
conn_mref = monitor(process, ConnPid)
}}.
%% @private
handle_call(usage, _From, State) ->
{reply, call(deeperl_usage, [], State), State};
handle_call(languages, _From, State) ->
{reply, call(deeperl_languages, [], State), State};
handle_call({translate, TargetLang, Texts, Options}, _From, State) ->
{reply, call(deeperl_translate, [TargetLang, Texts, Options], State), State};
handle_call(_Request, _From, State) ->
{reply, ok, State}.
%% @private
handle_cast(_Request, State) ->
{noreply, State}.
%% @private
handle_info({'DOWN', Mref, process, _ConnPid, Reason}, #state{conn_mref = Mref}) ->
exit(Reason);
handle_info(_Info, State) ->
{noreply, State}.
%% @private
terminate(_Reason, _State) ->
ok.
%% @private
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%% ---------------------------------------------------------------------------------------------------------------------
%% @doc Get the DeepL usage statistics
%% @end
%% ---------------------------------------------------------------------------------------------------------------------
-spec usage() -> {CharacterCount :: non_neg_integer(), CharacterLimit :: non_neg_integer()} | {error, Reason :: term()}.
usage() ->
gen_server:call(?SERVER, usage).
%% ---------------------------------------------------------------------------------------------------------------------
%% @doc Get the languages currently supported by DeepL
%% @end
%% ---------------------------------------------------------------------------------------------------------------------
-spec languages() -> [{Language :: language(), Text :: nonempty_string()}] | {error, Reason :: term()}.
languages() ->
gen_server:call(?SERVER, languages).
%% ---------------------------------------------------------------------------------------------------------------------
%% @doc Translate Text
%% @end
%% ---------------------------------------------------------------------------------------------------------------------
-spec translate(
TargetLang :: language(),
Text :: nonempty_string() | [nonempty_string()]
) -> [{DetectedSourceLanguage :: language(), Text :: nonempty_string()}] | {error, Reason :: term()}.
translate(TargetLang, Texts) ->
translate(TargetLang, Texts, #{}).
%% ---------------------------------------------------------------------------------------------------------------------
%% @doc Translate Text with additional options
%% @end
%% ---------------------------------------------------------------------------------------------------------------------
-spec translate(
TargetLang :: language(),
Text :: nonempty_string() | [nonempty_string()],
Options :: translation_options()
) -> [{DetectedSourceLanguage :: language(), Text :: nonempty_string()}] | {error, Reason :: term()}.
translate(TargetLang, [T | _] = Texts, Options) when is_list(T) == false ->
translate(TargetLang, [Texts], Options);
translate(TargetLang, Texts, Options) ->
gen_server:call(?SERVER, {translate, TargetLang, Texts, Options}).
%%%===================================================================
%%% Internal functions
%%%===================================================================
call(Module, Args, State) ->
Request = case application:get_env(authkey) of
undefined -> {error, no_authkey};
{ok, AuthKey} -> gen_deeperl_procedure:request(Module, AuthKey, Args)
end,
Result = case Request of
{error, _Reason} = RequestError -> RequestError;
{Uri, Header, Body} -> request(Uri, Header, Body, State)
end,
case Result of
{error, Reason} -> {error, Reason};
{ok, R} -> gen_deeperl_procedure:response(Module, R)
end.
request(Uri, Header, Body, #state{conn = ConnPid, conn_mref = MRef} = State) ->
StreamRef = gun:post(State#state.conn, Uri, Header, Body),
case gun:await(State#state.conn, StreamRef, 3000, State#state.conn_mref) of
{response, nofin, 200, _} -> gun:await_body(ConnPid, StreamRef, 1000, MRef);
{response, fin, 200, _} -> {error, emptyresponse};
{response, IsFin, 400, _} -> error_message(IsFin, badrequest, ConnPid, StreamRef, MRef);
{response, IsFin, 403, _} -> error_message(IsFin, badauth, ConnPid, StreamRef, MRef);
{response, IsFin, 404, _} -> error_message(IsFin, notfound, ConnPid, StreamRef, MRef);
{response, IsFin, 413, _} -> error_message(IsFin, sizelimit, ConnPid, StreamRef, MRef);
{response, IsFin, 429, _} -> error_message(IsFin, 'too many requests', ConnPid, StreamRef, MRef);
{response, IsFin, 456, _} -> error_message(IsFin, 'quota exceeded', ConnPid, StreamRef, MRef);
{response, IsFin, 503, _} -> error_message(IsFin, 'resource temporarily unavailable', ConnPid, StreamRef, MRef);
{response, IsFin, _, _} -> error_message(IsFin, 'unknown error', ConnPid, StreamRef, MRef);
{error, _} = Error -> Error
end.
error_message(fin, Error, _ConnPid, _StreamRef, _MRef) ->
{error, Error};
error_message(nofin, Error, ConnPid, StreamRef, MRef) ->
{ok, Body} = gun:await_body(ConnPid, StreamRef, 1000, MRef),
{error, {Error, jiffy:decode(Body, [return_maps])}}.