aboutsummaryrefslogtreecommitdiff
path: root/src/translate.erl
diff options
context:
space:
mode:
Diffstat (limited to 'src/translate.erl')
-rw-r--r--src/translate.erl322
1 files changed, 223 insertions, 99 deletions
diff --git a/src/translate.erl b/src/translate.erl
index e9f61ab8c..14f7b292f 100644
--- a/src/translate.erl
+++ b/src/translate.erl
@@ -5,7 +5,7 @@
%%% Created : 6 Jan 2003 by Alexey Shchepin <alexey@process-one.net>
%%%
%%%
-%%% ejabberd, Copyright (C) 2002-2016 ProcessOne
+%%% ejabberd, Copyright (C) 2002-2019 ProcessOne
%%%
%%% This program is free software; you can redistribute it and/or
%%% modify it under the terms of the GNU General Public License as
@@ -27,107 +27,147 @@
-author('alexey@process-one.net').
--export([start/0, load_dir/1, load_file/2,
- translate/2]).
+-behaviour(gen_server).
+
+-export([start_link/0, reload/0, translate/2]).
+%% gen_server callbacks
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ terminate/2, code_change/3]).
--include("ejabberd.hrl").
-include("logger.hrl").
+-include_lib("kernel/include/file.hrl").
+
+-define(ZERO_DATETIME, {{0,0,0}, {0,0,0}}).
+
+-type error_reason() :: file:posix() | {integer(), module(), term()} |
+ badarg | terminated | system_limit | bad_file |
+ bad_encoding.
+
+-record(state, {}).
+
+start_link() ->
+ gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
+
+init([]) ->
+ process_flag(trap_exit, true),
+ case load() of
+ ok ->
+ xmpp:set_tr_callback({?MODULE, translate}),
+ {ok, #state{}};
+ {error, Reason} ->
+ {stop, Reason}
+ end.
+
+handle_call(Request, From, State) ->
+ ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
+ {noreply, State}.
+
+handle_cast(Msg, State) ->
+ ?WARNING_MSG("Unexpected cast: ~p", [Msg]),
+ {noreply, State}.
+
+handle_info(Info, State) ->
+ ?WARNING_MSG("Unexpected info: ~p", [Info]),
+ {noreply, State}.
+
+terminate(_Reason, _State) ->
+ xmpp:set_tr_callback(undefined).
-start() ->
- ets:new(translations, [named_table, public]),
- Dir = case os:getenv("EJABBERD_MSGS_PATH") of
- false ->
- case code:priv_dir(ejabberd) of
- {error, _} -> ?MSGS_DIR;
- Path -> filename:join([Path, "msgs"])
- end;
- Path -> Path
- end,
- load_dir(iolist_to_binary(Dir)),
- ok.
-
--spec load_dir(binary()) -> ok.
-
-load_dir(Dir) ->
- case file:list_dir(Dir) of
- {ok, Files} ->
- MsgFiles = lists:filter(fun (FN) ->
- case length(FN) > 4 of
- true ->
- string:substr(FN, length(FN) - 3)
- == ".msg";
- _ -> false
- end
- end,
- Files),
- lists:foreach(fun (FNS) ->
- FN = list_to_binary(FNS),
- LP = ascii_tolower(str:substr(FN, 1,
- byte_size(FN) - 4)),
- L = case str:tokens(LP, <<".">>) of
- [Language] -> Language;
- [Language, _Project] -> Language
- end,
- load_file(L, <<Dir/binary, "/", FN/binary>>)
- end,
- MsgFiles),
- ok;
- {error, Reason} -> ?ERROR_MSG("~p", [Reason])
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+-spec reload() -> ok | {error, error_reason()}.
+reload() ->
+ load(true).
+
+-spec load() -> ok | {error, error_reason()}.
+load() ->
+ load(false).
+
+-spec load(boolean()) -> ok | {error, error_reason()}.
+load(ForceCacheRebuild) ->
+ {MsgsDirMTime, MsgsDir} = get_msg_dir(),
+ {CacheMTime, CacheFile} = get_cache_file(),
+ {FilesMTime, MsgFiles} = get_msg_files(MsgsDir),
+ LastModified = lists:max([MsgsDirMTime, FilesMTime]),
+ if ForceCacheRebuild orelse CacheMTime < LastModified ->
+ case load(MsgFiles, MsgsDir) of
+ ok -> dump_to_file(CacheFile);
+ Err -> Err
+ end;
+ true ->
+ case ets:file2tab(CacheFile) of
+ {ok, _} ->
+ ok;
+ {error, {read_error, {file_error, _, enoent}}} ->
+ load(MsgFiles, MsgsDir);
+ {error, {read_error, {file_error, _, Reason}}} ->
+ ?WARNING_MSG("Failed to read translation cache from ~ts: ~ts",
+ [CacheFile, format_error(Reason)]),
+ load(MsgFiles, MsgsDir);
+ {error, Reason} ->
+ ?WARNING_MSG("Failed to read translation cache from ~ts: ~p",
+ [CacheFile, Reason]),
+ load(MsgFiles, MsgsDir)
+ end
end.
-load_file(Lang, File) ->
- case file:open(File, [read]) of
- {ok, Fd} ->
- io:setopts(Fd, [{encoding,latin1}]),
- load_file_loop(Fd, 1, File, Lang),
- file:close(Fd);
- {error, Error} ->
- ExitText = iolist_to_binary([File, ": ",
- file:format_error(Error)]),
- ?ERROR_MSG("Problem loading translation file ~n~s",
- [ExitText]),
- exit(ExitText)
+-spec load([file:filename()], file:filename()) -> ok | {error, error_reason()}.
+load(Files, Dir) ->
+ try ets:new(translations, [named_table, public]) of
+ _ -> ok
+ catch _:badarg -> ok
+ end,
+ case Files of
+ [] ->
+ ?WARNING_MSG("No translation files found in ~ts, "
+ "check directory access",
+ [Dir]);
+ _ ->
+ ?INFO_MSG("Building language translation cache", []),
+ Objs = lists:flatten(misc:pmap(fun load_file/1, Files)),
+ case lists:keyfind(error, 1, Objs) of
+ false ->
+ ets:delete_all_objects(translations),
+ ets:insert(translations, Objs),
+ ?DEBUG("Language translation cache built successfully", []);
+ {error, File, Reason} ->
+ ?ERROR_MSG("Failed to read translation file ~ts: ~ts",
+ [File, format_error(Reason)]),
+ {error, Reason}
+ end
end.
-load_file_loop(Fd, Line, File, Lang) ->
- case io:read(Fd, '', Line) of
- {ok,{Orig, Trans}, NextLine} ->
- Trans1 = case Trans of
- <<"">> -> Orig;
- _ -> Trans
- end,
- ets:insert(translations,
- {{Lang, iolist_to_binary(Orig)},
- iolist_to_binary(Trans1)}),
-
- load_file_loop(Fd, NextLine, File, Lang);
- {ok,_, _NextLine} ->
- ExitText = iolist_to_binary([File,
- " approximately in the line ",
- Line]),
- ?ERROR_MSG("Problem loading translation file ~n~s",
- [ExitText]),
- exit(ExitText);
- {error,
- {_LineNumber, erl_parse, _ParseMessage} = Reason} ->
- ExitText = iolist_to_binary([File,
- " approximately in the line ",
- file:format_error(Reason)]),
- ?ERROR_MSG("Problem loading translation file ~n~s",
- [ExitText]),
- exit(ExitText);
- {error, Reason} ->
- ExitText = iolist_to_binary([File, ": ",
- file:format_error(Reason)]),
- ?ERROR_MSG("Problem loading translation file ~n~s",
- [ExitText]),
- exit(ExitText);
- {eof,_Line} ->
- ok
+-spec load_file(file:filename()) -> [{{binary(), binary()}, binary()} |
+ {error, file:filename(), error_reason()}].
+load_file(File) ->
+ Lang = lang_of_file(File),
+ try file:consult(File) of
+ {ok, Lines} ->
+ lists:map(
+ fun({In, Out}) ->
+ try {unicode:characters_to_binary(In),
+ unicode:characters_to_binary(Out)} of
+ {InB, OutB} when is_binary(InB), is_binary(OutB) ->
+ {{Lang, InB}, OutB};
+ _ ->
+ {error, File, bad_encoding}
+ catch _:badarg ->
+ {error, File, bad_encoding}
+ end;
+ (_) ->
+ {error, File, bad_file}
+ end, Lines);
+ {error, Reason} ->
+ [{error, File, Reason}]
+ catch _:{case_clause, {error, _}} ->
+ %% At the moment of the writing there was a bug in
+ %% file:consult_stream/3 - it doesn't process {error, term()}
+ %% result from io:read/3
+ [{error, File, bad_file}]
end.
-spec translate(binary(), binary()) -> binary().
-
translate(Lang, Msg) ->
LLang = ascii_tolower(Lang),
case ets:lookup(translations, {LLang, Msg}) of
@@ -148,8 +188,9 @@ translate(Lang, Msg) ->
end
end.
+-spec translate(binary()) -> binary().
translate(Msg) ->
- case ?MYLANG of
+ case ejabberd_option:language() of
<<"en">> -> Msg;
Lang ->
LLang = ascii_tolower(Lang),
@@ -172,10 +213,93 @@ translate(Msg) ->
end
end.
-ascii_tolower(B) ->
- iolist_to_binary(ascii_tolower_s(binary_to_list(B))).
+-spec ascii_tolower(list() | binary()) -> binary().
+ascii_tolower(B) when is_binary(B) ->
+ << <<(if X >= $A, X =< $Z ->
+ X + 32;
+ true ->
+ X
+ end)>> || <<X>> <= B >>;
+ascii_tolower(S) ->
+ ascii_tolower(unicode:characters_to_binary(S)).
+
+-spec get_msg_dir() -> {calendar:datetime(), file:filename()}.
+get_msg_dir() ->
+ Dir = misc:msgs_dir(),
+ case file:read_file_info(Dir) of
+ {ok, #file_info{mtime = MTime}} ->
+ {MTime, Dir};
+ {error, Reason} ->
+ ?ERROR_MSG("Failed to read directory ~ts: ~ts",
+ [Dir, format_error(Reason)]),
+ {?ZERO_DATETIME, Dir}
+ end.
+
+-spec get_msg_files(file:filename()) -> {calendar:datetime(), [file:filename()]}.
+get_msg_files(MsgsDir) ->
+ Res = filelib:fold_files(
+ MsgsDir, ".+\\.msg", false,
+ fun(File, {MTime, Files} = Acc) ->
+ case xmpp_lang:is_valid(lang_of_file(File)) of
+ true ->
+ case file:read_file_info(File) of
+ {ok, #file_info{mtime = Time}} ->
+ {lists:max([MTime, Time]), [File|Files]};
+ {error, Reason} ->
+ ?ERROR_MSG("Failed to read translation file ~ts: ~ts",
+ [File, format_error(Reason)]),
+ Acc
+ end;
+ false ->
+ ?WARNING_MSG("Ignoring translation file ~ts: file name "
+ "must be a valid language tag",
+ [File]),
+ Acc
+ end
+ end, {?ZERO_DATETIME, []}),
+ case Res of
+ {_, []} ->
+ case file:list_dir(MsgsDir) of
+ {ok, _} -> ok;
+ {error, Reason} ->
+ ?ERROR_MSG("Failed to read directory ~ts: ~ts",
+ [MsgsDir, format_error(Reason)])
+ end;
+ _ ->
+ ok
+ end,
+ Res.
+
+-spec get_cache_file() -> {calendar:datetime(), file:filename()}.
+get_cache_file() ->
+ MnesiaDir = mnesia:system_info(directory),
+ CacheFile = filename:join(MnesiaDir, "translations.cache"),
+ CacheMTime = case file:read_file_info(CacheFile) of
+ {ok, #file_info{mtime = Time}} -> Time;
+ {error, _} -> ?ZERO_DATETIME
+ end,
+ {CacheMTime, CacheFile}.
+
+-spec dump_to_file(file:filename()) -> ok.
+dump_to_file(CacheFile) ->
+ case ets:tab2file(translations, CacheFile) of
+ ok -> ok;
+ {error, Reason} ->
+ ?WARNING_MSG("Failed to create translation cache in ~ts: ~p",
+ [CacheFile, Reason])
+ end.
+
+-spec lang_of_file(file:filename()) -> binary().
+lang_of_file(FileName) ->
+ BaseName = filename:basename(FileName),
+ ascii_tolower(filename:rootname(BaseName)).
-ascii_tolower_s([C | Cs]) when C >= $A, C =< $Z ->
- [C + ($a - $A) | ascii_tolower_s(Cs)];
-ascii_tolower_s([C | Cs]) -> [C | ascii_tolower_s(Cs)];
-ascii_tolower_s([]) -> [].
+-spec format_error(error_reason()) -> string().
+format_error(bad_file) ->
+ "corrupted or invalid translation file";
+format_error(bad_encoding) ->
+ "cannot translate from UTF-8";
+format_error({_, _, _} = Reason) ->
+ "at line " ++ file:format_error(Reason);
+format_error(Reason) ->
+ file:format_error(Reason).