• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

processone / ejabberd / 1296

19 Jan 2026 11:25AM UTC coverage: 33.562% (+0.09%) from 33.468%
1296

push

github

badlop
mod_conversejs: Cosmetic change: sort paths alphabetically

0 of 4 new or added lines in 1 file covered. (0.0%)

11245 existing lines in 174 files now uncovered.

15580 of 46421 relevant lines covered (33.56%)

1074.56 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

47.66
/src/translate.erl
1
%%%----------------------------------------------------------------------
2
%%% File    : translate.erl
3
%%% Author  : Alexey Shchepin <alexey@process-one.net>
4
%%% Purpose : Localization helper
5
%%% Created :  6 Jan 2003 by Alexey Shchepin <alexey@process-one.net>
6
%%%
7
%%%
8
%%% ejabberd, Copyright (C) 2002-2026   ProcessOne
9
%%%
10
%%% This program is free software; you can redistribute it and/or
11
%%% modify it under the terms of the GNU General Public License as
12
%%% published by the Free Software Foundation; either version 2 of the
13
%%% License, or (at your option) any later version.
14
%%%
15
%%% This program is distributed in the hope that it will be useful,
16
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
17
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
18
%%% General Public License for more details.
19
%%%
20
%%% You should have received a copy of the GNU General Public License along
21
%%% with this program; if not, write to the Free Software Foundation, Inc.,
22
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
23
%%%
24
%%%----------------------------------------------------------------------
25

26
-module(translate).
27

28
-author('alexey@process-one.net').
29

30
-behaviour(gen_server).
31

32
-export([start_link/0, reload/0, translate/2]).
33
%% gen_server callbacks
34
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
35
         terminate/2, code_change/3]).
36

37
-include("logger.hrl").
38
-include_lib("kernel/include/file.hrl").
39

40
-define(ZERO_DATETIME, {{0,0,0}, {0,0,0}}).
41

42
-type error_reason() :: file:posix() | {integer(), module(), term()} |
43
                        badarg | terminated | system_limit | bad_file |
44
                        bad_encoding.
45

46
-record(state, {}).
47

48
start_link() ->
49
    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
11✔
50

51
init([]) ->
52
    process_flag(trap_exit, true),
11✔
53
    case load() of
11✔
54
        ok ->
55
            xmpp:set_tr_callback({?MODULE, translate}),
11✔
56
            {ok, #state{}};
11✔
57
        {error, Reason} ->
58
            {stop, Reason}
×
59
    end.
60

61
handle_call(Request, From, State) ->
62
    ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
×
63
    {noreply, State}.
×
64

65
handle_cast(Msg, State) ->
66
    ?WARNING_MSG("Unexpected cast: ~p", [Msg]),
×
67
    {noreply, State}.
×
68

69
handle_info(Info, State) ->
70
    ?WARNING_MSG("Unexpected info: ~p", [Info]),
×
71
    {noreply, State}.
×
72

73
terminate(_Reason, _State) ->
74
    xmpp:set_tr_callback(undefined).
11✔
75

76
code_change(_OldVsn, State, _Extra) ->
77
    {ok, State}.
×
78

79
-spec reload() -> ok | {error, error_reason()}.
80
reload() ->
81
    load(true).
×
82

83
-spec load() -> ok | {error, error_reason()}.
84
load() ->
85
    load(false).
11✔
86

87
-spec load(boolean()) -> ok | {error, error_reason()}.
88
load(ForceCacheRebuild) ->
89
    {MsgsDirMTime, MsgsDir} = get_msg_dir(),
11✔
90
    {CacheMTime, CacheFile} = get_cache_file(),
11✔
91
    {FilesMTime, MsgFiles} = get_msg_files(MsgsDir),
11✔
92
    LastModified = lists:max([MsgsDirMTime, FilesMTime]),
11✔
93
    if ForceCacheRebuild orelse CacheMTime < LastModified ->
11✔
94
            case load(MsgFiles, MsgsDir) of
11✔
95
                ok -> dump_to_file(CacheFile);
11✔
96
                Err -> Err
×
97
            end;
98
       true ->
99
            case ets:file2tab(CacheFile) of
×
100
                {ok, _} ->
101
                    ok;
×
102
                {error, {read_error, {file_error, _, enoent}}} ->
103
                    load(MsgFiles, MsgsDir);
×
104
                {error, {read_error, {file_error, _, Reason}}} ->
105
                    ?WARNING_MSG("Failed to read translation cache from ~ts: ~ts",
×
106
                                 [CacheFile, format_error(Reason)]),
×
107
                    load(MsgFiles, MsgsDir);
×
108
                {error, Reason} ->
109
                    ?WARNING_MSG("Failed to read translation cache from ~ts: ~p",
×
110
                                 [CacheFile, Reason]),
×
111
                    load(MsgFiles, MsgsDir)
×
112
            end
113
    end.
114

115
-spec load([file:filename()], file:filename()) -> ok | {error, error_reason()}.
116
load(Files, Dir) ->
117
    try ets:new(translations, [named_table, public]) of
11✔
118
        _ -> ok
11✔
119
    catch _:badarg -> ok
×
120
    end,
121
    case Files of
11✔
122
        [] ->
123
            ?WARNING_MSG("No translation files found in ~ts, "
×
124
                         "check directory access",
125
                         [Dir]);
×
126
        _ ->
127
            ?INFO_MSG("Building language translation cache", []),
11✔
128
            Objs = lists:flatten(misc:pmap(fun load_file/1, Files)),
11✔
129
            case lists:keyfind(error, 1, Objs) of
11✔
130
                false ->
131
                    ets:delete_all_objects(translations),
11✔
132
                    ets:insert(translations, Objs),
11✔
133
                    ?DEBUG("Language translation cache built successfully", []);
11✔
134
                {error, File, Reason} ->
135
                    ?ERROR_MSG("Failed to read translation file ~ts: ~ts",
×
136
                               [File, format_error(Reason)]),
×
137
                    {error, Reason}
×
138
            end
139
    end.
140

141
-spec load_file(file:filename()) -> [{{binary(), binary()}, binary()} |
142
                                     {error, file:filename(), error_reason()}].
143
load_file(File) ->
144
    Lang = lang_of_file(File),
341✔
145
    try file:consult(File) of
341✔
146
        {ok, Lines} ->
147
            lists:map(
341✔
148
              fun({In, Out}) ->
149
                      try {unicode:characters_to_binary(In),
150,623✔
150
                           unicode:characters_to_binary(Out)} of
151
                          {InB, OutB} when is_binary(InB), is_binary(OutB) ->
152
                              {{Lang, InB}, OutB};
150,623✔
153
                          _ ->
154
                              {error, File, bad_encoding}
×
155
                      catch _:badarg ->
156
                              {error, File, bad_encoding}
×
157
                      end;
158
                 (_) ->
159
                      {error, File, bad_file}
×
160
              end, Lines);
161
        {error, Reason} ->
162
            [{error, File, Reason}]
×
163
    catch _:{case_clause, {error, _}} ->
164
            %% At the moment of the writing there was a bug in
165
            %% file:consult_stream/3 - it doesn't process {error, term()}
166
            %% result from io:read/3
167
            [{error, File, bad_file}]
×
168
    end.
169

170
-spec translate(binary(), binary()) -> binary().
171
translate(Lang, Msg) ->
172
    LLang = ascii_tolower(Lang),
48,904✔
173
    case ets:lookup(translations, {LLang, Msg}) of
48,904✔
174
      [{_, Trans}] -> Trans;
×
175
      _ ->
176
          ShortLang = case str:tokens(LLang, <<"-">>) of
48,904✔
UNCOV
177
                        [] -> LLang;
6,851✔
178
                        [SL | _] -> SL
42,053✔
179
                      end,
180
          case ShortLang of
48,904✔
181
            <<"en">> -> Msg;
42,052✔
UNCOV
182
            LLang -> translate(Msg);
6,852✔
183
            _ ->
184
                case ets:lookup(translations, {ShortLang, Msg}) of
×
185
                  [{_, Trans}] -> Trans;
×
186
                  _ -> translate(Msg)
×
187
                end
188
          end
189
    end.
190

191
-spec translate(binary()) -> binary().
192
translate(Msg) ->
UNCOV
193
    case ejabberd_option:language() of
6,852✔
UNCOV
194
      <<"en">> -> Msg;
6,852✔
195
      Lang ->
196
          LLang = ascii_tolower(Lang),
×
197
          case ets:lookup(translations, {LLang, Msg}) of
×
198
            [{_, Trans}] -> Trans;
×
199
            _ ->
200
                ShortLang = case str:tokens(LLang, <<"-">>) of
×
201
                              [] -> LLang;
×
202
                              [SL | _] -> SL
×
203
                            end,
204
                case ShortLang of
×
205
                  <<"en">> -> Msg;
×
206
                  Lang -> Msg;
×
207
                  _ ->
208
                      case ets:lookup(translations, {ShortLang, Msg}) of
×
209
                        [{_, Trans}] -> Trans;
×
210
                        _ -> Msg
×
211
                      end
212
                end
213
          end
214
    end.
215

216
-spec ascii_tolower(list() | binary()) -> binary().
217
ascii_tolower(B) when is_binary(B) ->
218
    << <<(if X >= $A, X =< $Z ->
49,586✔
219
                  X + 32;
×
220
             true ->
221
                  X
85,570✔
222
          end)>> || <<X>> <= B >>;
49,586✔
223
ascii_tolower(S) ->
224
    ascii_tolower(unicode:characters_to_binary(S)).
682✔
225

226
-spec get_msg_dir() -> {calendar:datetime(), file:filename()}.
227
get_msg_dir() ->
228
    Dir = misc:msgs_dir(),
11✔
229
    case file:read_file_info(Dir) of
11✔
230
        {ok, #file_info{mtime = MTime}} ->
231
            {MTime, Dir};
11✔
232
        {error, Reason} ->
233
            ?ERROR_MSG("Failed to read directory ~ts: ~ts",
×
234
                       [Dir, format_error(Reason)]),
×
235
            {?ZERO_DATETIME, Dir}
×
236
    end.
237

238
-spec get_msg_files(file:filename()) -> {calendar:datetime(), [file:filename()]}.
239
get_msg_files(MsgsDir) ->
240
    Res = filelib:fold_files(
11✔
241
            MsgsDir, ".+\\.msg", false,
242
            fun(File, {MTime, Files} = Acc) ->
243
                    case xmpp_lang:is_valid(lang_of_file(File)) of
341✔
244
                        true ->
245
                            case file:read_file_info(File) of
341✔
246
                                {ok, #file_info{mtime = Time}} ->
247
                                    {lists:max([MTime, Time]), [File|Files]};
341✔
248
                                {error, Reason} ->
249
                                    ?ERROR_MSG("Failed to read translation file ~ts: ~ts",
×
250
                                               [File, format_error(Reason)]),
×
251
                                    Acc
×
252
                            end;
253
                        false ->
254
                            ?WARNING_MSG("Ignoring translation file ~ts: file name "
×
255
                                         "must be a valid language tag",
256
                                         [File]),
×
257
                            Acc
×
258
                    end
259
            end, {?ZERO_DATETIME, []}),
260
    case Res of
11✔
261
        {_, []} ->
262
            case file:list_dir(MsgsDir) of
×
263
                {ok, _} -> ok;
×
264
                {error, Reason} ->
265
                    ?ERROR_MSG("Failed to read directory ~ts: ~ts",
×
266
                               [MsgsDir, format_error(Reason)])
×
267
            end;
268
        _ ->
269
            ok
11✔
270
    end,
271
    Res.
11✔
272

273
-spec get_cache_file() -> {calendar:datetime(), file:filename()}.
274
get_cache_file() ->
275
    MnesiaDir = mnesia:system_info(directory),
11✔
276
    CacheFile = filename:join(MnesiaDir, "translations.cache"),
11✔
277
    CacheMTime = case file:read_file_info(CacheFile) of
11✔
278
                     {ok, #file_info{mtime = Time}} -> Time;
×
279
                     {error, _} -> ?ZERO_DATETIME
11✔
280
                 end,
281
    {CacheMTime, CacheFile}.
11✔
282

283
-spec dump_to_file(file:filename()) -> ok.
284
dump_to_file(CacheFile) ->
285
    case ets:tab2file(translations, CacheFile) of
11✔
286
        ok -> ok;
11✔
287
        {error, Reason} ->
288
            ?WARNING_MSG("Failed to create translation cache in ~ts: ~p",
×
289
                         [CacheFile, Reason])
×
290
    end.
291

292
-spec lang_of_file(file:filename()) -> binary().
293
lang_of_file(FileName) ->
294
    BaseName = filename:basename(FileName),
682✔
295
    ascii_tolower(filename:rootname(BaseName)).
682✔
296

297
-spec format_error(error_reason()) -> string().
298
format_error(bad_file) ->
299
    "corrupted or invalid translation file";
×
300
format_error(bad_encoding) ->
301
    "cannot translate from UTF-8";
×
302
format_error({_, _, _} = Reason) ->
303
    "at line " ++ file:format_error(Reason);
×
304
format_error(Reason) ->
305
    file:format_error(Reason).
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc