%% -*- coding: utf-8 -*- %% Copyright (c) 2022, Madalin Grigore-Enescu %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(t__). -author("Madalin Grigore-Enescu"). -include("../include/t__.hrl"). -export([ application/0, language/0, config_get/0, config_get/1, config_get_environment/1, config_set/1, config_set/2, config_set/3, config_set_cast/2, config_delete/0, config_delete/1, config_delete/2, config_delete_cast/1, translate/1, translate/2, translate/3, translate/7 ]). -export_type([repository/0, config/0, p/0]). -type repository() :: #t__repository{}. -type config() :: #t__config{}. -type p() :: #t__p{}. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% application/language %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -spec application() -> 't__' | Application when Application :: atom(). %% @doc Returns the name of the application to which the calling process belongs. %% If the specified process does not belong to any application the function returns 't__'. application() -> case application:get_application() of {ok, Application} -> Application; _ -> t__ end. -spec language() -> Language when Language :: string(). %% @doc Returns the current language for the calling process. %% %% When you properly setup the language for the process, the expected time complexity for the current implementation %% of this macro is O(1) and the worst case time complexity is O(N), where N is the number of items in the process dictionary. %% %% The function will perform the following steps in order to determine the default language: %% - call erlang:get(t__language) %% - call application:get_env(t__config) %% - call application:get_env(t__, t__config) %% - if no t__config environment key was set for both the current application or the t__ application %% we return t__ default language. language() -> case erlang:get(t__language) of undefined -> #t__config{ language = Language } = config_get(), Language; PDLanguage -> PDLanguage end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% config %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%------------------------------------------------------------- %% config_get %%------------------------------------------------------------- -spec config_get() -> Config when Config :: t__:config(). %% @doc Returns the current configuration for the calling process. %% %% Please notice that the language config record field may not be the same %% to the calling process application config because language can also be configured %% at the process level. (The process level language overwrites the application %% configured language). %% %% If you want to get the config holding application language, call t__:config/1 function. %% %% The function will perform the following steps: %% - call erlang:get(t__language) to get any calling process specific language that will overwrite %% the language returned from the next calls %% - call application:get_env(t__config) %% - call application:get_env(t__, t__config) %% - if no t__config environment key was set for both the current application or the t__ application %% we return t__ default config config_get() -> PDLanguage = erlang:get(t__language), Application = application(), Config = config_get(Application), %% Process dictionary (may) overwrite application language case erlang:is_list(PDLanguage) of true -> Config#t__config{language = PDLanguage}; _ -> Config end. -spec config_get(Application) -> Config when Application :: atom(), Config :: t__:config(). %% @doc Returns the current configuration for the specified application. %% %% The function will perform the following steps: %% - call application:get_env(t__config) %% - call application:get_env(t__, t__config) %% - if no t__config environment key was set for both the current application or the t__ application %% we return t__ default config config_get(Application) -> case config_get_environment(Application) of {ok, Config} -> Config; _ -> case config_get_environment(t__) of {ok, TConfig} -> TConfig; _ -> #t__config{} end end. %% @doc Returns the value of configuration t__config for Application config_get_environment(Application) -> case application:get_env(Application, t__) of {ok, Config = #t__config{}} -> {ok, Config}; {ok, _InvalidConfig} -> ?T__LOG(emergency, "The value of configuration t__config for application is invalid!", [{application, Application}]), erlang:error(invalid_configuration); _ -> undefined end. %%------------------------------------------------------------- %% config_set %%------------------------------------------------------------- -spec config_set(Config) -> ok | {error, Error} when Config :: t__:config(), Error :: term(). %% @doc Sets a new configuration for the calling process application with infinity timeout. %% @see t__:config_set/3 config_set(Config) -> config_set(application(), Config, infinity). -spec config_set(Application, Config) -> ok | {error, Error} when Application :: atom(), Config :: t__:config(), Error :: term(). %% @doc Sets a new configuration for the specified application with infinity timeout. %% @see t__:config_set/3 config_set(Application, Config) -> config_set(Application, Config, infinity). -spec config_set(Application, Config, Timeout) -> ok | {error, Error} when Application :: atom(), Config :: t__:config(), Timeout :: timeout(), Error :: term(). %% @doc Sets a new configuration for the specified application with the specified timeout. %% %% There is a blocking gen_server call behind this! %% That's why there is a config_set_cast version of this function for convenience. %% However I don't recommend using that because you will not know if the operation failed. %% %% Changing applications config is possible but should not be abused. %% The system was designed for you to be able to change it at the application start or %% from an administration panel, from time to time. config_set(Application, Config = #t__config{}, Timeout) -> t__srv:config_set_call(Application, Config, Timeout). -spec config_set_cast(Application, Config) -> ok | {error, Error} when Application :: atom(), Config :: t__:config(), Error :: term(). %% @doc Sets a new configuration for the specified application asynchronous %% Sends an asynchronous request to the process handling the implementation %% and returns ok immediately. %% %% @see t__:config_set/3 config_set_cast(Application, Config = #t__config{}) -> t__srv:config_set_cast(Application, Config). %%------------------------------------------------------------- %% config_delete %%------------------------------------------------------------- -spec config_delete() -> ok | {error, Error} when Error :: term(). %% @doc Deletes configuration for the calling process application with infinity timeout. %% @see t__:config_delete/2 config_delete() -> config_set(application(), infinity). -spec config_delete(Application) -> ok | {error, Error} when Application :: atom(), Error :: term(). %% @doc Deletes configuration for the specified application with infinity timeout. %% @see t__:config_delete/2 config_delete(Application) -> config_delete(Application, infinity). -spec config_delete(Application, Timeout) -> ok | {error, Error} when Application :: atom(), Timeout :: timeout(), Error :: term(). %% @doc Deletes configuration for the specified application with the specified timeout. %% %% There is a blocking gen_server call behind this! %% That's why there is a config_delete_cast version of this function for convenience. %% %% Deleting applications config is possible but should not be abused. %% The system was designed for you to be able to change it at the application start or %% from an administration panel, from time to time. config_delete(Application, Timeout) -> t__srv:config_delete_call(Application, Timeout). -spec config_delete_cast(Application) -> ok | {error, Error} when Application :: atom(), Error :: term(). %% @doc Deletes configuration for the specified application asynchronous %% Sends an asynchronous request to the process handling the implementation %% and returns ok immediately. %% %% @see t__:config_delete/2 config_delete_cast(Application) -> t__srv:config_delete_cast(Application). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% translate %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -spec translate(Param) -> Msg when Param :: term(), Msg :: string(). %% @doc Translate a singular or plural term, with or without repository, language and context %% This is the main translate function that does everything. % using #t__p translate(#t__p{ application = Application, repository = Repository, language = Language, context = Context, msg = Msg, data = Data, reference = Reference }) -> Application1 = case Application of undefined -> erlang:atom_to_list(application()); _ -> erlang:atom_to_list(Application) end, Repository1 = case Repository of undefined -> "default"; _ -> translate_param_cast_term(Repository) end, Language1 = case Language of undefined -> language(); _ -> translate_param_cast_term(Language) end, Context1 = case Context of undefined -> undefined; _ -> translate_param_cast_term(Context) end, Reference1 = case Reference of undefined -> undefined; _ -> translate_param_cast_term(Context) end, Msg1 = translate_param_cast_msg(Msg), true = ((Data =:= undefined) or erlang:is_list(Data)), Config = config_get(Application1), translate_private(Application1, Config, Repository1, Language1, Context1, Msg1, Data, Reference1); translate(P) -> translate(P, undefined, undefined). -spec translate(Param, Data) -> Msg when Param :: term(), Data :: undefined | list(), Msg :: string(). %% @doc Translate with data %% @see t__:translate/3 translate(P, Data) -> translate(P, Data, undefined). -spec translate(Param, Data, Reference) -> Msg when Param :: term(), Data :: undefined | list(), Reference :: undefined | string(), Msg :: string(). %% @doc Translate with data and reference as separate parameters % using #t__p translate(P = #t__p{}, Data, Reference) -> translate(P#t__p{data = Data, reference = Reference}); % With proplist translate(P = [{_K, _V} | _T], Data, Reference) -> translate(translate_param_proplists_to_p(P, Data, Reference)); % With repository and term translate({Repository, {Term}}, Data, Reference) -> translate(translate_param_term_to_p(Term, Repository, undefined, Data, Reference)); % With repository, language and term translate({Repository, Language, {Term}}, Data, Reference) -> translate(translate_param_term_to_p(Term, Repository, Language, Data, Reference)); % Without repository and language translate(Term, Data, Reference) -> translate(translate_param_term_to_p(Term, undefined, undefined, Data, Reference)). -spec translate(Application, Repository, Language, Context, Msg, Data, Reference) -> TranslatedMsg when Application :: undefined | atom(), Repository :: undefined | string() | atom() | binary(), Language :: undefined | string() | atom() | binary(), Context :: undefined | string() | atom() | binary(), Msg :: string() | atom() | binary(), Data :: undefined | list(), Reference :: undefined | string(), TranslatedMsg :: string(). %% @doc Translate with all separate parameters. translate(Application, Repository, Language, Context, Msg, Data, Reference) -> translate(#t__p{ application = Application, repository = Repository, language = Language, context = Context, msg = Msg, data = Data, reference = Reference }). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% translate_param_ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%------------------------------------------------------------- %% translate_param_proplists_to_p %%------------------------------------------------------------- %% @doc Convert proplists translate parameter to #t__p{} translate_param_proplists_to_p(P, Data, Reference) -> #t__p{ application = proplists:get_value(application, P, undefined), repository = proplists:get_value(repository, P, undefined), language = proplists:get_value(language, P, undefined), context = proplists:get_value(context, P, undefined), msg = proplists:get_value(msg, P, undefined), data = proplists:get_value(data, P, Data), reference = proplists:get_value(reference, P, Reference) }. %%------------------------------------------------------------- %% translate_param_term_to_p %%------------------------------------------------------------- %% @doc Convert term to #t__p{} %% Singular terms: %% ``` %% "I have a joke about Erlang, but it requires a prologue." %% <<"Erlang is user-friendly, it’s just picky about its friends!">> %% 'Why can't you trust atoms? Because they make up everything!' %% ''' %% Plural terms: %% ``` %% %% ["user", "users"] %% ''' %% Plural terms with interpolation: %% ``` %% ["~B user", "~B users"] %% ''' translate_param_term_to_p(Msg, Repository, Language, Data, Reference) when erlang:is_list(Msg); erlang:is_binary(Msg); erlang:is_atom(Msg) -> #t__p{ repository = Repository, language = Language, msg = Msg, data = Data, reference = Reference}; %% Singular with context: %% ```{"menu", "Save"}''' %% Plural terms with context: %% ```{"female", ["File belongs to her", "Files belong to her"]}''' %% ```{"male", ["File belongs to him", "Files belong to him"]}''' %% Plural terms with context and interpolation: %% ```{"female", ["~B file belongs to her", "~B files belong to her"]}''' %% ```{"male", ["~B file belongs to him", "~B files belong to him"]}''' translate_param_term_to_p({Context, Msg}, Repository, Language, Data, Reference) -> #t__p{ repository = Repository, language = Language, context = Context, msg = Msg, data = Data, reference = Reference}. %%------------------------------------------------------------- %% translate_param_cast_term %%------------------------------------------------------------- %% @doc Convert non-empty string, atom, binary to string translate_param_cast_term(Term) when is_list(Term), erlang:length(Term) > 0 -> true = io_lib:char_list(Term), Term; translate_param_cast_term(Term) when is_binary(Term) -> case unicode:characters_to_list(Term) of L when is_list(L) -> translate_param_cast_term(L); %% Unicode _ -> translate_param_cast_term(erlang:binary_to_list(Term)) %% bytewise encoded end; translate_param_cast_term(Term) when is_atom(Term) -> translate_param_cast_term(erlang:atom_to_list(Term)). %%------------------------------------------------------------- %% translate_param_cast_msg %%------------------------------------------------------------- %% @doc Convert list of strings, atoms, binaries to list of strings translate_param_cast_msg([Msg1]) when erlang:is_list(Msg1); erlang:is_binary(Msg1); erlang:is_atom(Msg1) -> [translate_param_cast_term(Msg1)]; translate_param_cast_msg([Msg1, Msg2]) when (erlang:is_list(Msg1) or erlang:is_binary(Msg1) or erlang:is_atom(Msg1)); (erlang:is_list(Msg2) or erlang:is_binary(Msg2) or erlang:is_atom(Msg2)) -> [translate_param_cast_term(Msg1), translate_param_cast_term(Msg2)]; translate_param_cast_msg(M) when erlang:is_list(M) -> [M]; translate_param_cast_msg(M) when erlang:is_binary(M) -> case unicode:characters_to_list(M) of L when is_list(L) -> [L]; %% Unicode _ -> [erlang:binary_to_list(M)] %% bytewise encoded end; translate_param_cast_msg(M) when erlang:is_atom(M) -> [erlang:atom_to_list(M)]. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% translate_private %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% @doc Private translate function translate_private(Application, #t__config{dev=Dev}, Repository, Language, Context, Msg, Data, Reference) -> EtsTableRepository = t__repository:ets_table(Application, Repository), try translate_private(Dev, EtsTableRepository, Language, Context, Msg, Data, Reference) catch Exception1:Reason1 -> ?T__LOG(error, "Translate exception!", [ {exception, {Exception1, Reason1}}, {application, Application}, {repository, Repository}, {language, Language}, {context, Context}, {msg, Msg}, {data, Data}, {reference, Reference} ]), translate_private_sp_missing(Msg, Data, Language) end. %% @doc Private translate function for production mode/developer mode % Production mode translate_private(false, EtsTable, Language, Context, Msg, Data, _Reference) -> Key = {po, Language, Context, Msg}, case ets:lookup(EtsTable, Key) of [{_Key, {_Comments, Value}}] -> translate_private_sp(Value, Data, Language); _ -> translate_private_sp_missing(Msg, Data, Language) end; % developer mode translate_private(true, EtsTable, Language, Context, Msg, Data, Reference) -> PotKey = {pot, Context, Msg}, case ets:lookup(EtsTable, PotKey) of [] -> ets:insert(EtsTable, {PotKey, [Reference]}); _ -> ok end, Key = {po, Language, Context, Msg}, case ets:lookup(EtsTable, Key) of [{_Key, {_Comments, Value}}] -> translate_private_sp(Value, Data, Language); _ -> translate_private_sp_missing(Msg, Data, Language) end. %% @doc Detect and translate single or plural terms when the translation is missing %% Single terms translate_private_sp_missing([Msg], Data, _Language) -> translate_private_format(Msg, Data); %% Plural terms translate_private_sp_missing([M1|_T], Data = [N | _], _Language) when N == 1 -> translate_private_format(M1, Data); translate_private_sp_missing([_M1,M2|_T], Data, _Language) -> translate_private_format(M2, Data). %% @doc Detect and translate single or plural terms %% Single terms translate_private_sp([Msg], Data, _Language) -> translate_private_format(Msg, Data); %% Plural terms translate_private_sp(Msg, Data = [N | _], Language) -> SelectedMsg = t__plural:select(Language, N, Msg), translate_private_format(SelectedMsg, Data). %% @doc Returns a character list that represents Data formatted in accordance with Format %% If any exception is raised, returns original Msg unaltered. translate_private_format(Msg, Data) when erlang:is_list(Data); erlang:length(Data) > 0 -> try lists:flatten(io_lib:format(Msg, Data)) catch Exception:Reason -> ?T__LOG(error, "Exception for io_lib:format(Msg, Data)", [ {exception, {Exception, Reason}}, {msg, Msg}, {data, Data} ]), Msg end; translate_private_format(Msg, _Data) when erlang:is_list(Msg) -> Msg.