src/i18n/z_trans.erl

%% @author Marc Worrell <marc@worrell.nl>
%% @copyright 2009-2020 Marc Worrell
%% @doc Translate english sentences into other languages, following
%% the GNU gettext principle.

%% Copyright 2009-2020 Marc Worrell
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%%     http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.

-module(z_trans).
-author("Marc Worrell <marc@worrell.nl>").

-export([
    translations/2,
    parse_translations/1,
    trans/2,
    trans/3,
    lookup/2,
    lookup/3,
    lookup_fallback/2,
    lookup_fallback/3,
    lookup_fallback_language/2,
    lookup_fallback_language/3
]).

-include_lib("../../include/zotonic.hrl").

%% @doc Fetch all translations for the given string.
-spec translations(z:trans() | binary() | string(), z:context()) -> z:trans() | binary().
translations(#trans{ tr = Tr0 } = Trans0, Context) ->
    {en, From} = proplists:lookup(en, Tr0),
    case translations(From, Context) of
        #trans{ tr = Tr1 } ->
            #trans{ tr = merge_trs(Tr0, lists:reverse(Tr1)) };
        _ -> Trans0
    end;
translations(From, Context) when is_binary(From) ->
    try
        case ets:lookup(z_trans_server:table(Context), From) of
            [] ->
    			From;
            [{_, Trans}] ->
    			#trans{ tr = Trans }
        end
    catch
        error:badarg ->
            From
    end;
translations(From, Context) when is_list(From) ->
    translations(unicode:characters_to_binary(From), Context);
translations(From, Context) ->
    translations(z_convert:to_binary(From), Context).

merge_trs([], Acc) ->
    lists:reverse(Acc);
merge_trs([{Lang,_} = LT|Rest], Acc) ->
    case proplists:is_defined(Lang, Acc) of
        true -> merge_trs(Rest, Acc);
        false -> merge_trs(Rest, [LT|Acc])
    end.

%% @doc Prepare a translations table based on all .po files in the active modules.
%%      Returns a map of english sentences with all their translations
-spec parse_translations(z:context()) -> map().
parse_translations(Context) ->
    Mods = z_module_indexer:translations(Context),
    ParsePOFiles = parse_mod_pofiles(Mods, []),
    build_index(ParsePOFiles, #{}).

%% @doc Parse all .po files. Results in a dict {label, [iso_code,trans]}
parse_mod_pofiles([], Acc) ->
    lists:reverse(Acc);
parse_mod_pofiles([{_Module, POFiles}|Rest], Acc) ->
    Acc1 = parse_pofiles(POFiles, Acc),
    parse_mod_pofiles(Rest, Acc1).

parse_pofiles([], Acc) ->
    Acc;
parse_pofiles([{Lang,File}|POFiles], Acc) ->
    parse_pofiles(POFiles, [{Lang, z_gettext:parse_po(File)}|Acc]).

build_index([], Acc) ->
    Acc;
build_index([{Lang, Labels}|Rest], Acc) ->
    build_index(Rest, add_labels(Lang, Labels, Acc)).

add_labels(_Lang, [], Acc) ->
    Acc;
add_labels(Lang, [{header,_}|Rest], Acc) ->
    add_labels(Lang, Rest, Acc);
add_labels(Lang, [{Label,Trans}|Rest], Acc) when is_binary(Trans), is_binary(Label) ->
    case maps:find(Label, Acc) of
        {ok, Ts} ->
            case proplists:is_defined(Lang, Ts) of
                false -> add_labels(Lang, Rest, Acc#{ Label => [{Lang,Trans}|Ts]});
                true -> add_labels(Lang, Rest, Acc)
            end;
        error ->
            add_labels(Lang, Rest, Acc#{ Label => [{Lang,Trans}] })
    end.

%% @doc Strict translation lookup of a language version
-spec lookup(z:trans()|binary()|string(), #context{}) -> binary() | string() | undefined.
lookup(Trans, Context) ->
    lookup(Trans, z_context:language(Context), Context).

-spec lookup(z:trans()|binary()|string(), atom(), #context{}) -> binary() | string() | undefined.
lookup(#trans{ tr = Tr }, Lang, _Context) ->
    proplists:get_value(Lang, Tr);
lookup(Text, Lang, Context) ->
    case z_context:language(Context) of
        Lang -> Text;
        _ -> undefined
    end.

%% @doc Non strict translation lookup of a language version.
%%      In order check: requested language, default configured language, english, any
-spec lookup_fallback(z:trans()|binary()|string()|undefined, z:context()|undefined) -> binary() | string() | undefined.
lookup_fallback(undefined, _Context) ->
    undefined;
lookup_fallback(Trans, undefined) ->
    lookup_fallback(Trans, en, undefined);
lookup_fallback(Trans, Context) ->
    lookup_fallback(Trans, z_context:language(Context), Context).

lookup_fallback(#trans{ tr = Tr }, Lang, Context) ->
    case proplists:get_value(Lang, Tr) of
        undefined ->
            FallbackLang = case Context of
                undefined -> en;
                _ -> z_context:fallback_language(Context)
            end,
            case proplists:get_value(FallbackLang, Tr) of
                undefined ->
                    case z_language:default_language(Context) of
                        undefined -> take_english_or_first(Tr);
                        CfgLang ->
                            case proplists:get_value(z_convert:to_atom(CfgLang), Tr) of
                                undefined -> take_english_or_first(Tr);
                                Text -> Text
                            end
                    end;
                Text ->
                    Text
            end;
        Text ->
            Text
    end;
lookup_fallback(Text, _Lang, _Context) ->
    Text.

take_english_or_first(Tr) ->
    case proplists:get_value(en, Tr) of
        undefined ->
            case Tr of
                [{_,Text}|_] -> Text;
                _ -> undefined
            end;
        EnglishText ->
            EnglishText
    end.

-spec lookup_fallback_language([atom()], z:context()) -> atom().
lookup_fallback_language(Langs, Context) ->
    lookup_fallback_language(Langs, z_context:language(Context), Context).

-spec lookup_fallback_language([atom()], atom(), z:context()) -> atom().
lookup_fallback_language([], Lang, _Context) ->
    Lang;
lookup_fallback_language(Langs, Lang, Context) ->
    case lists:member(Lang, Langs) of
        false ->
            case z_language:default_language(Context) of
                undefined ->
                    case lists:member(en, Langs) of
                        true ->
                            en;
                        false ->
                            case Langs of
                                [] -> Lang;
                                [L|_] -> L
                            end
                    end;
                CfgLang ->
                    CfgLangAtom = z_convert:to_atom(CfgLang),
                    case lists:member(CfgLangAtom, Langs) of
                        false ->
                            case lists:member(en, Langs) of
                                true ->
                                    en;
                                false ->
                                    case Langs of
                                        [] -> Lang;
                                        [L|_] -> L
                                    end
                            end;
                        true ->
                            CfgLangAtom
                    end
            end;
        true ->
            Lang
    end.


%% @doc translate a string or trans record into another language
-spec trans(z:trans() | binary() | string(), z:context() | atom()) -> binary() | undefined.
trans(#trans{ tr = Tr }, Lang) when is_atom(Lang) ->
    proplists:get_value(Lang, Tr);
trans(Text, Lang) when is_atom(Lang) ->
    z_convert:to_binary(Text);
trans(Text, Context) ->
    trans(Text, z_context:language(Context), Context).

trans(#trans{ tr = Tr0 }, Language, Context) ->
    case proplists:lookup(en, Tr0) of
        {en, Text} ->
            case translations(Text, Context) of
                #trans{ tr = Tr } ->
                    case proplists:get_value(Language, Tr) of
                        undefined -> proplists:get_value(Language, Tr0, Text);
                        Translated -> Translated
                    end;
                _ ->
                    proplists:get_value(Language, Tr0, Text)
            end;
        none ->
            proplists:get_value(Language, Tr0)
    end;
trans(Text, Language, Context) ->
    case translations(Text, Context) of
        #trans{ tr = Tr } ->
            proplists:get_value(Language, Tr, Text);
        _ ->
            z_convert:to_binary(Text)
    end.