├── .gitignore ├── LICENSE ├── Makefile ├── README ├── doc ├── .gitignore └── overview.edoc ├── ebin └── .gitignore ├── include └── gettext.hrl ├── priv └── .gitignore ├── rebar.config ├── src ├── gettext.app.src ├── gettext.erl ├── gettext_app.erl ├── gettext_checker.erl ├── gettext_compile.erl ├── gettext_format.erl ├── gettext_internal.hrl ├── gettext_iso639.erl ├── gettext_server.erl ├── gettext_sup.erl ├── gettext_validate.erl ├── gettext_validate_bad_case.erl ├── gettext_validate_bad_html.erl ├── gettext_validate_bad_punct.erl ├── gettext_validate_bad_stxt.erl ├── gettext_validate_bad_ws.erl ├── gettext_validate_no_trans.erl └── gettext_yaws_html.erl └── test ├── .gitignore ├── Makefile ├── gettext_demo.erl └── lang ├── .gitignore ├── custom ├── es │ └── gettext.po └── sv │ └── gettext.po └── default └── en ├── .gitignore └── gettext.po /.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etnt/gettext/9ab211734cf59415617b47c06f77504dd442b40f/LICENSE -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | REBAR_EXE = rebar 2 | REBAR=if ! command -v $(REBAR_EXE) >/dev/null; then echo '$(REBAR_EXE) not found - please install first'; exit 1; fi ; $(REBAR_EXE) 3 | 4 | .PHONY: all compile clean eunit xref dialyzer 5 | 6 | all: compile 7 | 8 | compile: 9 | @$(REBAR) compile 10 | 11 | clean: 12 | @$(REBAR) clean 13 | 14 | eunit: all 15 | @$(REBAR) eunit 16 | 17 | ct: all 18 | @$(REBAR) ct 19 | 20 | xref: all 21 | @$(REBAR) xref 22 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Erlang Gettext tools for multi-lingual capabilities. 2 | 3 | The gettext application makes it possible to internationalize your Erlang 4 | application. The name gettext comes from the GNU package with the same name. 5 | However, the only thing they have in common is the format of the PO-files, 6 | i.e, the files containing the text that can be translated. 7 | 8 | Using gettext you can create an initial PO-file containing all the strings 9 | of your application that should be possible to translate. By translating the 10 | strings into some other language and loading the new PO-file into the 11 | gettext database you can adapt your application for different languages. 12 | -------------------------------------------------------------------------------- /doc/.gitignore: -------------------------------------------------------------------------------- 1 | edoc-info 2 | erlang.png 3 | stylesheet.css 4 | *.html 5 | -------------------------------------------------------------------------------- /doc/overview.edoc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etnt/gettext/9ab211734cf59415617b47c06f77504dd442b40f/doc/overview.edoc -------------------------------------------------------------------------------- /ebin/.gitignore: -------------------------------------------------------------------------------- 1 | gettext.app 2 | *.beam 3 | -------------------------------------------------------------------------------- /include/gettext.hrl: -------------------------------------------------------------------------------- 1 | %% Client header file for Erlang gettext 2 | -ifndef(_GETTEXT_HRL). 3 | -define(_GETTEXT_HRL, true). 4 | 5 | -compile({parse_transform,gettext_compile}). 6 | 7 | %% Note: Every macro expansion must contain an explicit call to 8 | %% gettext:key2str(String), so that the parse transform can detect it. 9 | 10 | -define(TXT(S), gettext:key2str(S)). 11 | 12 | -define(TXT2(S, Lang), gettext:key2str(S, Lang)). 13 | 14 | %%% 15 | %%% This macro allows S (format string) to use tagged 16 | %%% args (A) in any order, any number of times or not at all. 17 | %%% This is needed for translators of po files to be able to write 18 | %%% translations with a natural sentence structure. 19 | %%% 20 | -define(STXT(S, A), gettext_format:stxt(?TXT(S),A)). 21 | 22 | -define(STXT2(S, A, Lang), gettext_format:stxt(?TXT2(S, Lang),A)). 23 | 24 | 25 | -endif. 26 | -------------------------------------------------------------------------------- /priv/.gitignore: -------------------------------------------------------------------------------- 1 | gettext_server_db.dets 2 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, [debug_info]}. 2 | -------------------------------------------------------------------------------- /src/gettext.app.src: -------------------------------------------------------------------------------- 1 | {application, gettext, 2 | [{description,"gettext handling"}, 3 | {vsn,"2.1.0"}, 4 | {modules,[]}, 5 | {registered,[]}, 6 | {env, 7 | [%% During frequent translation work in a large organization 8 | %% with numerous topic branches, all introducing changes to 9 | %% the PO-files, it quickly becomes very annoying getting header 10 | %% diffs in the PO-files. With the introduction of the POlish 11 | %% tool we therefore introduced a fixed PO-file header. 12 | %% The old behaviour is retained by setting the 'use_orig_header' 13 | %% to 'true'. 14 | {use_orig_header, false} 15 | , {charset, "iso-8859-1"} 16 | , {copyright, "YYYY Organization"} 17 | , {create_date, "2006-07-01 16:45+0200"} 18 | , {fixed_last_translator, "Gettext-POlish system"} 19 | , {fixed_revision_date, "2006-07-01 16:45+0200"} 20 | , {org_name, "Organization"} 21 | , {team, "Team "} 22 | ]}, 23 | {mod, {gettext_app, []}}, 24 | {applications,[kernel,stdlib]} 25 | ]}. 26 | -------------------------------------------------------------------------------- /src/gettext.erl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etnt/gettext/9ab211734cf59415617b47c06f77504dd442b40f/src/gettext.erl -------------------------------------------------------------------------------- /src/gettext_app.erl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etnt/gettext/9ab211734cf59415617b47c06f77504dd442b40f/src/gettext_app.erl -------------------------------------------------------------------------------- /src/gettext_checker.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------------- 2 | %% Permission is hereby granted, free of charge, to any person obtaining a 3 | %% copy of this software and associated documentation files (the 4 | %% "Software"), to deal in the Software without restriction, including 5 | %% without limitation the rights to use, copy, modify, merge, publish, 6 | %% distribute, sublicense, and/or sell copies of the Software, and to permit 7 | %% persons to whom the Software is furnished to do so, subject to the 8 | %% following conditions: 9 | %% 10 | %% The above copyright notice and this permission notice shall be included 11 | %% in all copies or substantial portions of the Software. 12 | %% 13 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | %% OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 15 | %% MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 16 | %% NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | %% DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 18 | %% OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 19 | %% USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | %% 21 | %% Created: 19 Jul 2007 by 22 | %% @author Jakub Nowak 23 | %% @doc Check and validate if any translated strings are missing. 24 | 25 | -module(gettext_checker). 26 | 27 | -export([dialog/0, 28 | dialog/1, 29 | help/0, 30 | get_first_key/0, 31 | next_non_translated/2, 32 | report_bug/3, 33 | check_bugs/0, 34 | display_errors/0 35 | ]). 36 | 37 | -include("gettext_internal.hrl"). 38 | 39 | 40 | -define(TABLE_NAME, gettext_server_db). 41 | -define(ROOT_DIR, filename:join([gettext_server:gettext_dir(), ?LANG_DIR])). 42 | -define(ENDCOL, 72). 43 | -define(PIVOT, 4). 44 | -define(SEP, $\s). 45 | -define(BUG_FILE, "bugs.dat"). 46 | -define(PROMPT, '> '). 47 | 48 | help() -> 49 | io:format( 50 | "Module for finding and reparing dependency errors and warnings in~n" 51 | "the gettext_server_db.~n" 52 | "It recognizes the following situations:~n" 53 | " * no key found in one of the languages~n" 54 | " * key has the same value in both languages~n" 55 | " * there is a space mismatch~n" 56 | "Use the dialog/0 or dialog/1 functions, e.g. " 57 | "gettext_checker:dialog(\"en\"),~nto start the checker~n~n" 58 | "The error finder is based on the two functions:~n" 59 | " * next_non_translated(Key, Language) - to get the next error " 60 | "or warning~n" 61 | " Returns [NextKey,Args,Status], where:~n" 62 | " Status is one of the atoms:~n" 63 | " - no_key~n" 64 | " - no_key_default~n" 65 | " - same_value~n" 66 | " - spaces_warning~n" 67 | " - the_end~n" 68 | " NextKey is a key from gettext_server_db in which the error " 69 | "occurred~n" 70 | " (without the language information).~n" 71 | " Args are additional arguments. Usually the empty list - " 72 | "only if~n" 73 | " there is a space error it will contain:~n" 74 | " - 'a', char_at_beginning, char_at_the_end~n" 75 | " - 'b', char_at_beginning~n" 76 | " - 'e', char_at_end~n" 77 | " * get_first_key() - this function is used in order to" 78 | " return the first key~n" 79 | " from the dets table.~n", 80 | []). 81 | 82 | 83 | %% To be called from e.g a Makefile 84 | display_errors() -> 85 | Lang = gettext:all_lcs(), 86 | F = fun(LC) -> 87 | FirstKey = get_first_key(), 88 | non_translated_stats(FirstKey, LC, 0, 0, 0) 89 | end, 90 | lists:foreach(F, Lang). 91 | 92 | 93 | 94 | %% function implementing dialog with the user by giving a user possibility 95 | %% to pick up numbers with actions associated to them 96 | 97 | dialog() -> 98 | LC = gettext:default_lang(), 99 | io:format("Language set to: ~s~n", [gettext_iso639:lc2lang(LC)]), 100 | dialog(LC). 101 | 102 | dialog(QueryLang) -> 103 | 104 | case check_lang(QueryLang) of 105 | error -> 106 | io:format("Wrong language! Specify a good one or exit(x)~n", []), 107 | io:format("Possible languages: ~p~n", [all_langs()]), 108 | 109 | case get_string(?PROMPT) of 110 | "x" -> 111 | true; 112 | NewLang -> 113 | dialog(NewLang) 114 | end; 115 | _Other -> 116 | io:format("~n" 117 | "1. check and change language~n" 118 | "2. count errors and warnings~n" 119 | "3. choose language~n" 120 | "4. change space-errors automatically~n" 121 | "5. check reported bugs~n" 122 | "6. exit~n", 123 | []), 124 | case get_int(?PROMPT) of 125 | {error, _Value} -> 126 | io:format("wrong option ~n"), 127 | dialog(QueryLang); 128 | 1 -> 129 | FirstKey = get_first_key(), 130 | find_non_translated(FirstKey, QueryLang), 131 | dialog(QueryLang); 132 | 2 -> 133 | FirstKey = get_first_key(), 134 | non_translated_stats(FirstKey, QueryLang,0,0,0), 135 | dialog(QueryLang); 136 | 3 -> 137 | io:format("Possible languages: ~p~n", [all_langs()]), 138 | NewLang = get_string(?PROMPT), 139 | dialog(NewLang); 140 | 4 -> 141 | FirstKey = get_first_key(), 142 | change_spaces(FirstKey, QueryLang), 143 | dialog(QueryLang); 144 | 5 -> 145 | check_bugs(), 146 | dialog(QueryLang), 147 | io:format("check finished~n", []); 148 | 6-> 149 | true; 150 | _Num -> 151 | io:format("wrong option ~n"), 152 | dialog(QueryLang) 153 | end 154 | end. 155 | 156 | %%this function checks if the language we choose exists in the system, 157 | %%uses gettext_iso639 module 158 | check_lang("a") -> 159 | ok; 160 | 161 | check_lang(Lang) -> 162 | case gettext_iso639:lc2lang(Lang) of 163 | "" -> 164 | error; 165 | _Other -> 166 | Fname = filename:join([?ROOT_DIR, ?CUSTOM_DIR, Lang, ?POFILE]), 167 | case filelib:is_regular(Fname) of 168 | true -> ok; 169 | false -> error 170 | end 171 | end. 172 | 173 | %% function thats is responsible for automatic space changing for all 174 | %% of the keys 175 | change_spaces('$end_of_table',QueryLang) -> 176 | dets:sync(?TABLE_NAME), 177 | save_to_po_file(QueryLang,custom), 178 | io:format("spaces checking is finished~n",[]); 179 | %% 180 | change_spaces(El,QueryLang) -> 181 | case next_non_translated(El,QueryLang) of 182 | [NextKey,Key,Args,spaces_warning] -> 183 | case Args of 184 | error -> 185 | true; 186 | _Other -> 187 | NewValue = space_auto_change(Args,Key,QueryLang), 188 | NewObj = {{Key,QueryLang},NewValue}, 189 | dets:insert(?TABLE_NAME,NewObj) 190 | end, 191 | change_spaces(NextKey,QueryLang); 192 | [NextKey,_,_,_Result ]-> 193 | change_spaces(NextKey,QueryLang) 194 | end. 195 | 196 | %% function returning statistics about the number of errors and warnings 197 | %% (simple counting) 198 | non_translated_stats('$end_of_table', QueryLang, E, W, S) -> 199 | io:format("~n------~n" 200 | "Results (~p):~n" 201 | "Num.of key errors: ~p ~n" 202 | "Same key warnings: ~p ~n" 203 | "Spaces warnings: ~p~n", 204 | [QueryLang, E, W, S]); 205 | %% 206 | non_translated_stats(El,QueryLang,E,W,S) -> 207 | case next_non_translated(El,QueryLang) of 208 | [NextKey,_Key,_,no_key]-> 209 | non_translated_stats(NextKey,QueryLang,E+1,W,S); 210 | [NextKey,_Key,_,same_value] -> 211 | non_translated_stats(NextKey,QueryLang,E,W+1,S); 212 | [NextKey,_Key,_,spaces_warning] -> 213 | non_translated_stats(NextKey,QueryLang,E,W,S+1); 214 | [NextKey,_Key,_,no_key_default] -> 215 | non_translated_stats(NextKey,QueryLang,E+1,W,S); 216 | [NextKey,_,_,_Result ]-> 217 | non_translated_stats(NextKey,QueryLang,E,W,S) 218 | end. 219 | 220 | %% function responsible for getting all errors and warning and running 221 | %% appropriate action for them 222 | find_non_translated('$end_of_table',QueryLang) -> 223 | io:format("checking finished:)~n",[]), 224 | dets:sync(?TABLE_NAME), 225 | save_to_po_file(QueryLang,custom), 226 | save_to_po_file(?DEFAULT_LANG,default); 227 | %% 228 | find_non_translated(El,QueryLang) -> 229 | case next_non_translated(El,QueryLang) of 230 | [NextKey,Key,[],no_key]-> 231 | io:format("error: there is non translated string with key ~p " 232 | "in ~p lang:~n", 233 | [show_string(Key),QueryLang]), 234 | Res = error_action(Key,QueryLang); 235 | [NextKey,Key,[],same_value] -> 236 | io:format("warning: key ~p in lang ~p have the" 237 | " same value:~n",[show_string(Key),QueryLang]), 238 | Res = warning_action(Key,QueryLang); 239 | [NextKey,Key,Args,spaces_warning] -> 240 | io:format("warning: key ~p in lang ~p have space " 241 | "mismatch~n",[show_string(Key),QueryLang]), 242 | Res = spaces_action(Key,QueryLang, Args); 243 | [NextKey,Key,[],no_key_default] -> 244 | io:format("error: there is non translated string with " 245 | "key ~p in deafault lang:~n", 246 | [show_string(Key)]), 247 | Res = error_action_default(Key,QueryLang); 248 | [NextKey,_,_,the_end ]-> 249 | Res = ok 250 | end, 251 | if 252 | Res == ret -> 253 | find_non_translated('$end_of_table',QueryLang); 254 | true-> 255 | find_non_translated(NextKey,QueryLang) 256 | end. 257 | 258 | %% function returning the first key in the whole table 259 | %% 260 | get_first_key() -> 261 | Key = dets:first(?TABLE_NAME), 262 | Key. 263 | 264 | %% functionality for getting the new error/warning information from 265 | %% gettext_server_db it returns a List - [NextKey,Key,Args,Status] - run 266 | %% gettext_checker:help() for more informations 267 | next_non_translated('$end_of_table',_QueryLang) -> 268 | ['$end_of_table',[],[],the_end]; 269 | %% 270 | next_non_translated({Key, ?DEFAULT_LANG = Lang}, QueryLang) when is_atom(Key) -> 271 | NextKey = dets:next(?TABLE_NAME,{Key,Lang}), 272 | next_non_translated(NextKey, QueryLang); 273 | %% 274 | next_non_translated({Key, ?DEFAULT_LANG = Lang}, QueryLang) -> 275 | NextKey = dets:next(?TABLE_NAME, {Key, Lang}), 276 | Value = lookup(Key, Lang), 277 | case lookup(Key, QueryLang) of 278 | empty -> 279 | [NextKey, Key, [], no_key]; 280 | Value -> 281 | [NextKey, Key, [], same_value]; 282 | RetValue -> 283 | do_check_spaces(Key, NextKey, QueryLang, 284 | RetValue, Value) 285 | end; 286 | %% 287 | next_non_translated({Key, QueryLang}, QueryLang) -> 288 | NextKey = dets:next(?TABLE_NAME, {Key, QueryLang}), 289 | case lookup(Key, ?DEFAULT_LANG) of 290 | empty -> 291 | [NextKey, Key, [], no_key_default]; 292 | _RetValue -> 293 | next_non_translated(NextKey, QueryLang) 294 | end; 295 | %% 296 | next_non_translated({Key, Lang}, QueryLang) -> 297 | NextKey = dets:next(?TABLE_NAME,{Key,Lang}), 298 | next_non_translated(NextKey, QueryLang). 299 | 300 | 301 | do_check_spaces(Key, NextKey, QueryLang, RetValue, Value) -> 302 | case check_spaces(Value,RetValue) of 303 | ok -> 304 | next_non_translated(NextKey,QueryLang); 305 | Args->%Args have info about where spaces mismatch 306 | [NextKey,Key,Args, 307 | spaces_warning] 308 | end. 309 | 310 | %% Simple function that wraps the dets:lookup function 311 | %% 312 | lookup(Key,Lang) -> 313 | case dets:lookup(?TABLE_NAME, {Key, Lang}) of 314 | [] -> empty; 315 | [{_,Str}|_] -> Str 316 | end. 317 | %% function showing only first 40 chars of the string 318 | %% 319 | show_string(String) -> 320 | Len = string:len(String), 321 | if 322 | Len>40 -> 323 | string:left(String,40)++"..."; 324 | true -> 325 | String 326 | end. 327 | 328 | %% function that gets int from the user 329 | %% 330 | get_int(Prompt)-> 331 | [_NewLine | TmpValue] = lists:reverse(io:get_line(Prompt)), 332 | Value = lists:reverse(TmpValue), 333 | case string:to_integer(Value) of 334 | {error,_Reason}-> 335 | {error,Value}; 336 | {Num,_Rest}-> 337 | Num 338 | end. 339 | 340 | %% function that gets string from the user 341 | get_string(Prompt) -> 342 | [_NewLine | TmpValue] = lists:reverse(io:get_line(Prompt)), 343 | Value = lists:reverse(TmpValue), 344 | Value. 345 | 346 | %% function responsible for finding spacing errors 347 | %% Space error is only check in the beginning and the end of 348 | %% the string. it check if arguments are proper and if are 349 | %% runs the check_spaces_real function 350 | check_spaces(S1,S2) when is_atom(S1) or is_atom(S2) -> 351 | error; 352 | check_spaces(S1,S2) -> 353 | Len1 = string:len(S1), 354 | Len2 = string:len(S2), 355 | if 356 | (Len1<1) or (Len2<1) -> 357 | error; 358 | (S1==" ") or (S2==" ") -> 359 | error; 360 | true -> 361 | check_spaces_real(S1,S2) 362 | end. 363 | 364 | %% B-begining 365 | %% E-end 366 | %% S-string 367 | check_spaces_real(S1,S2) -> 368 | B1 = hd(S1), 369 | B2 = hd(S2), 370 | 371 | [E1 | _Rest1] = lists:reverse(S1), 372 | [E2 | _Rest2] = lists:reverse(S2), 373 | if 374 | (B1==$\s) or (B2==$\s) , (E1==$\s) or (E2==$\s) -> 375 | if 376 | not (B1==B2), not (E1==E2) -> 377 | [a,B1,E1]; 378 | not (B1==B2) -> 379 | [b,B1]; 380 | not (E1==E2) -> 381 | [e,E1]; 382 | true -> 383 | ok 384 | end; 385 | (B1==$\s) or (B2==$\s) , not (B1==B2) -> 386 | [b,B1]; 387 | (E1==$\s) or (E2==$\s) ,not (E1==E2) -> 388 | [e,E1]; 389 | true -> 390 | ok 391 | end. 392 | 393 | %%!!!!!!!!!!!!!!!ACTIONS!!!!!!!!!!!!!! 394 | 395 | get_action() -> 396 | case string:tokens(io:get_line(?PROMPT),"\s\n\t") of 397 | [Action|_] -> Action; 398 | _ -> "" 399 | end. 400 | 401 | 402 | spaces_action(Key, QueryLang, error)-> 403 | io:format("Skip(s)~n" 404 | "Change manually(c)~n" 405 | "Look at file references(l)~n" 406 | "Return(r)~n" 407 | ,[]), 408 | Action = get_action(), 409 | action(Action, Key, QueryLang, spaces, error); 410 | %% 411 | spaces_action(Key, QueryLang, Args)-> 412 | io:format("Skip(s)~n" 413 | "ChangeAutomaticly(a)~n" 414 | "Change manually(c)~n" 415 | "Look at file references(l)~n" 416 | "Return(r)~n",[] 417 | ), 418 | Action = get_action(), 419 | action(Action, Key, QueryLang, spaces, Args). 420 | 421 | %% automaticly change the string so there is no space error 422 | %% 423 | space_auto_change([a | Arg], Key, QueryLang) -> 424 | Value = lookup(Key,QueryLang), 425 | [B1,E1] = Arg, 426 | if 427 | (B1==$\s) , (E1==$\s) -> 428 | NewValue=" " ++Value++" "; 429 | B1==$\s -> 430 | [_End | TmpValue] = lists:reverse(Value), 431 | NewValue = " " ++lists:reverse(TmpValue); 432 | E1==$\s -> 433 | [_End | TmpValue] = Value, 434 | NewValue = TmpValue ++ " "; 435 | true -> 436 | [_Begin | TmpValue1] = Value, 437 | [_End | TmpValue2] = lists:reverse(TmpValue1), 438 | NewValue = lists:reverse(TmpValue2) 439 | end, 440 | NewValue; 441 | space_auto_change([b ,B1], Key, QueryLang) -> 442 | Value = lookup(Key,QueryLang), 443 | if 444 | B1==$\s -> 445 | NewValue=" " ++ Value; 446 | true -> 447 | 448 | [_End| NewValue] = Value 449 | end, 450 | NewValue; 451 | space_auto_change([e ,E1], Key, QueryLang) -> 452 | Value = lookup(Key,QueryLang), 453 | if 454 | E1==$\s -> 455 | NewValue = Value++ " "; 456 | true -> 457 | [_End| TmpValue] = lists:reverse(Value), 458 | NewValue = lists:reverse(TmpValue) 459 | end, 460 | NewValue. 461 | 462 | %% it is used when the two values for different languages are the same 463 | %% 464 | warning_action (Key,QueryLang) -> 465 | io:format("Skip(s)~n" 466 | "ChangeValue(c)~n" 467 | "Look at file references(l)~n" 468 | "Return(r)~n",[] 469 | ), 470 | Action = get_action(), 471 | action(Action, Key, QueryLang, warning, undefined). 472 | 473 | %% error action for default language - it is used when there is 474 | %% no key in default language 475 | %% 476 | error_action_default(Key,QueryLang) -> 477 | io:format("Skip(s)~n" 478 | "AddValue(a)~n" 479 | "Delete(d)~n" 480 | "Look at file references(l)~n" 481 | "Return(r)~n",[]), 482 | Action = get_action(), 483 | action(Action, Key, QueryLang, error, default). 484 | 485 | %% error action used when there is no key in specified language 486 | %% (QueryLang) 487 | error_action(Key,QueryLang) -> 488 | io:format("Skip(s)~n" 489 | "AddValue(a)~n" 490 | "Look at file references(l)~n" 491 | "Return(r)~n",[]), 492 | Action = get_action(), 493 | action(Action, Key, QueryLang, error, no_key). 494 | 495 | 496 | %% 497 | %% Action matrix. 498 | %% 499 | action("s", _, _, _, _) -> true; 500 | action("r", _, _, _, _) -> ret; 501 | action("l", Key, _, _, _) -> print_comments(get_comments(Key)); 502 | %% 503 | action("a", Key, QLang, error, no_key) -> 504 | Similar = find_similar(Key, QLang), 505 | print_possibilities(Similar, no_key), 506 | case get_int('New Value> ') of 507 | {error,NewValue}-> 508 | NewObj = {{Key, QLang}, NewValue}, 509 | dets:insert(?TABLE_NAME, NewObj); 510 | Num-> 511 | if Num =< length(Similar) -> 512 | {ChosenKey, Val} = lists:nth(Num, Similar), 513 | dets:delete(?TABLE_NAME, {ChosenKey, QLang}), 514 | NewObj = {{Key, QLang}, Val}, 515 | dets:insert(?TABLE_NAME, NewObj); 516 | true -> 517 | io:format("wrong value~n"), 518 | error_action(Key, QLang) 519 | end 520 | end; 521 | action(_, Key, QLang, error, no_key) -> 522 | error_action(Key, QLang); 523 | %% 524 | action("a", Key, _, error, default) -> 525 | NewValue = get_string('New Value> '), 526 | NewObj = {{Key, ?DEFAULT_LANG}, NewValue}, 527 | dets:insert(?TABLE_NAME, NewObj); 528 | action("d", Key, QLang, error, default) -> 529 | dets:delete(?TABLE_NAME, {Key, QLang}); 530 | action(_, Key, QLang, error, default) -> 531 | error_action_default(Key,QLang); 532 | %% 533 | action("c", Key, QLang, warning, _) -> 534 | NewValue = get_string(?PROMPT), 535 | NewObj = {{Key, QLang}, NewValue}, 536 | dets:insert(?TABLE_NAME, NewObj); 537 | action(_, Key, QLang, warning, _) -> 538 | warning_action(Key, QLang); 539 | %% 540 | %% 541 | action("c", Key, QLang, spaces, _) -> 542 | NewValue = get_string(?PROMPT), 543 | NewObj = {{Key, QLang}, NewValue}, 544 | dets:insert(?TABLE_NAME, NewObj); 545 | action("a", Key, QLang, spaces, Args) -> 546 | NewValue = space_auto_change(Args, Key, QLang), 547 | NewObj = {{Key, QLang}, NewValue}, 548 | dets:insert(?TABLE_NAME, NewObj); 549 | action(_, Key, QLang, spaces, X) -> 550 | spaces_action(Key, QLang, X). 551 | 552 | 553 | print_comments([]) -> 554 | true; 555 | print_comments([ Comment | Rest]) -> 556 | io:format("~p~n",[Comment]), 557 | print_comments(Rest). 558 | 559 | %% print possible string that can match the one we need 560 | %% it uses find_similar function to look for keys that 561 | %% have similar look and print out their values 562 | %% (with the number we can choose) 563 | print_possibilities([],_Other) -> 564 | true; 565 | print_possibilities(List,no_key) -> 566 | io:format("possible values:~n",[]), 567 | F = fun(X,Acc) -> 568 | {_Key,Val} = X, 569 | Num = integer_to_list(Acc), 570 | String = Num++". "++show_string(Val)++"~n", 571 | io:format("~p~n",[String]), 572 | Acc+1 573 | end, 574 | lists:foldl(F,1,List); 575 | print_possibilities(List,_Other) -> 576 | io:format("possible values:~n",[]), 577 | F = fun(X,Acc) -> 578 | {Key,Val} = X, 579 | 580 | Num = integer_to_list(Acc), 581 | String = Num++". Key:"++show_string(Key)++" Val:"++ 582 | show_string(Val)++"~n", 583 | io:format("~p~n",[String]), 584 | Acc+1 585 | end, 586 | lists:foldl(F,1,List). 587 | 588 | 589 | %% function used to find similar strings it goes thought all 590 | %% dets table in check if the string is similar using the 591 | %% compare function 592 | %% 593 | find_similar(Key,Lang)-> 594 | Strings = dets:match(?TABLE_NAME, {{'$1',Lang},'$2'}), 595 | F = fun([SKey,SVal],Acc) -> 596 | case compare(SKey,Key) of 597 | true -> 598 | case lookup(SKey,?DEFAULT_LANG) of 599 | empty -> 600 | [{SKey,SVal} | Acc]; 601 | _Other -> 602 | Acc 603 | end; 604 | false -> 605 | Acc 606 | end 607 | end, 608 | 609 | lists:foldl(F,[],Strings). 610 | 611 | %% check if two string are similar - it is using the 612 | %% dist function which returns Levenshtein distance 613 | %% between two strings 614 | 615 | compare(S1,S2) when not is_list(S1) or not is_list(S2) -> 616 | false; 617 | 618 | compare(S1,S2) -> 619 | Len1 = string:len(S1), 620 | 621 | LevenDis = dist(S1,S2), 622 | if 623 | Len1>20 -> 624 | if 625 | LevenDis>4 -> 626 | false; 627 | true -> 628 | true 629 | end; 630 | Len1>7 -> 631 | if 632 | LevenDis>3 -> 633 | false; 634 | true -> 635 | true 636 | end; 637 | true -> 638 | if 639 | LevenDis>1 -> 640 | false; 641 | true -> 642 | true 643 | end 644 | end. 645 | 646 | %% ---------------------------------------------------------------------------- 647 | %% Calculates the Levenshtein distance between two strings 648 | dist(S1, S2) -> 649 | FirstRow = lists:seq(0, length(S2)), 650 | dist2(S1, S2, FirstRow, 1). 651 | 652 | dist2([H1|T1], S2, PrevRow, C) -> 653 | NextRow = next_row(H1, S2, PrevRow, [C]), 654 | dist2(T1, S2, NextRow, C+1); 655 | dist2([], _, LastRow, _) -> 656 | lists:last(LastRow). 657 | 658 | 659 | next_row(Ch, [H2|T2], [P0|TP], CurRow) -> 660 | V = min(P0+ch_cost(Ch, H2), hd(TP)+1, hd(CurRow)+1), 661 | next_row(Ch, T2, TP, [V|CurRow]); 662 | next_row(_Ch, [], _PrevRow, CurRow) -> 663 | lists:reverse(CurRow). 664 | 665 | ch_cost(C, C) -> 0; 666 | ch_cost(_, _) -> 1. 667 | 668 | min(A,B,C) -> 669 | if A < B, A < C -> A; 670 | B < C -> B; 671 | true -> C 672 | end. 673 | 674 | %% get comments for the specified key from the default language .po file 675 | %% 676 | get_comments(Key) -> 677 | [_Header |FileCommentsQuery] = 678 | parse_po_comment(filename:join([?ROOT_DIR, ?CUSTOM_DIR, 679 | gettext:default_lang(), ?POFILE])), 680 | case lists:keysearch(Key,1,FileCommentsQuery) of 681 | {value, {_,Res}} -> 682 | Res; 683 | _ -> [] 684 | end. 685 | 686 | 687 | %% function that saves changes made in dets to appropriate po file 688 | %% Kind argument can have two values: custom and default depending on 689 | %% what the language is in the system. Function parses the .po file 690 | %% and gets all commets for the keys - then they are merged with 691 | %% the keys and values from dets database and all together putted 692 | %% to the file 693 | %% 694 | save_to_po_file(QueryLang,Kind) -> 695 | FilePath = prepare_file(QueryLang,Kind), 696 | [Header |FileCommentsQuery] = parse_po_comment(FilePath), 697 | ValQuery = dets:match_object(?TABLE_NAME,{{'_',QueryLang},'_'}), 698 | SFileCommentsQuery = lists:keysort(1,FileCommentsQuery), 699 | SValQuery = lists:keysort(1,convert_val_query(ValQuery,[])), 700 | WholeQuery = two_to_one_merge(SValQuery,SFileCommentsQuery,[]), 701 | 702 | {ok,Fd} = file:open(FilePath,[write]), 703 | write_header(Fd,Header), 704 | write_entries(WholeQuery,Fd), 705 | file:close(Fd). 706 | 707 | %% gets full path to the gettext file 708 | %% 709 | prepare_file(QueryLang,default) -> 710 | Fname = filename:join([?ROOT_DIR,?DEFAULT_DIR,QueryLang,?POFILE]), 711 | filelib:ensure_dir(Fname), 712 | Fname; 713 | 714 | prepare_file(QueryLang,custom) -> 715 | Fname = filename:join([?ROOT_DIR,?CUSTOM_DIR,QueryLang,?POFILE]), 716 | filelib:ensure_dir(Fname), 717 | Fname. 718 | 719 | 720 | %% convert the resulst of dets:match function to more useful one 721 | %% it removes the Lang parametr 722 | %% 723 | convert_val_query([{{Key,_Lang},Val}| Rest],NewQuery) -> 724 | if is_list(Key) -> 725 | convert_val_query(Rest,[{Key,Val} | NewQuery]); 726 | true -> 727 | convert_val_query(Rest,NewQuery) 728 | end; 729 | 730 | convert_val_query([],NewQuery) -> NewQuery. 731 | 732 | %% merges two list basing on the fact that they both have the 733 | %% same key values 734 | %% 735 | two_to_one_merge([{Key1,Val1} | Rest1],L2,Result) -> 736 | 737 | 738 | case lists:keysearch(Key1,1,L2) of 739 | {value,{_Key,Val2}}-> 740 | NewTouple = {Key1,Val1,Val2}; 741 | false -> 742 | NewTouple = {Key1,Val1,[]} 743 | end, 744 | 745 | two_to_one_merge(Rest1,L2,[NewTouple |Result]); 746 | 747 | two_to_one_merge([],_L2,Result) -> 748 | lists:keysort(1,Result). 749 | 750 | 751 | %% writes header to the file 752 | write_header(Fd,{header_info,Header}) -> 753 | file:write(Fd, "msgid \"\"\n"), 754 | file:write(Fd, "msgstr "), 755 | file:write(Fd,"\""++Header++"\"\n"). 756 | 757 | %% write whole dets database to the file 758 | write_entries(L,Fd) -> 759 | F = fun({Key,Val1,Val2}) -> 760 | write_comments(Fd,Val2), 761 | file:write(Fd, "msgid "), 762 | write_pretty(Key,Fd), 763 | file:write(Fd, "msgstr "), 764 | write_pretty(Val1,Fd), 765 | io:format(Fd, "~n", []) 766 | end, 767 | lists:foreach(F, L). 768 | 769 | 770 | write_comments(Fd,[Val | Rest]) -> 771 | io:format(Fd,"#~s~n",[Val]), 772 | write_comments(Fd,Rest); 773 | write_comments(_Fd,[]) -> 774 | true. 775 | 776 | write_pretty(Str,_Fd) when is_atom(Str) -> 777 | true; 778 | write_pretty([], _) -> 779 | true; 780 | write_pretty(Str, Fd) when length(Str) =< ?ENDCOL -> 781 | write_string(Str, Fd); 782 | write_pretty(Str, Fd) -> 783 | {Line, Rest} = get_line(Str), 784 | write_string(Line, Fd), 785 | write_pretty(Rest, Fd). 786 | 787 | write_string(Str, Fd) -> 788 | %file:write(Fd, "\""), 789 | file:write(Fd, io_lib:print(Str)), 790 | file:write(Fd, "\n"). 791 | 792 | 793 | %%% Split the string into substrings, 794 | %%% aligned around a specific column. 795 | get_line(Str) -> 796 | get_line(Str, ?SEP, 1, ?ENDCOL, []). 797 | 798 | %%% End of string reached. 799 | get_line([], _Sep, _N, _End, Acc) -> 800 | {lists:reverse(Acc), []}; 801 | %%% Eat characters. 802 | get_line([H|T], Sep, N, End, Acc) when N < End -> 803 | get_line(T, Sep, N+1, End, [H|Acc]); 804 | %%% Ended with a Separator on the End boundary. 805 | get_line([Sep|T], Sep, End, End, Acc) -> 806 | {lists:reverse([Sep|Acc]), T}; 807 | %%% At the end, try to find end of token within 808 | %%% the given constraint, else backup one token. 809 | get_line([H|T] = In, Sep, End, End, Acc) -> 810 | case find_end(T, Sep) of 811 | {true, Racc, Rest} -> 812 | {lists:reverse(Racc ++ [H|Acc]), Rest}; 813 | false -> 814 | case reverse_tape(Acc, In) of 815 | {true, Bacc, Rest} -> 816 | {lists:reverse(Bacc), Rest}; 817 | {false,Str} -> 818 | %%% Ugh...the word is longer than ENDCOL... 819 | split_string(Str, ?ENDCOL) 820 | end 821 | end. 822 | 823 | find_end(Str, Sep) -> 824 | find_end(Str, Sep, 1, ?PIVOT, []). 825 | 826 | find_end([Sep|T], Sep, N, Pivot, Acc) when N =< Pivot -> 827 | {true, [Sep|Acc], T}; 828 | find_end(_Str, _Sep, N, Pivot, _Acc) when N > Pivot -> 829 | false; 830 | find_end([H|T], Sep, N, Pivot, Acc) -> 831 | find_end(T, Sep, N+1, Pivot, [H|Acc]); 832 | find_end([], _Sep, _N, _Pivot, Acc) -> 833 | {true, Acc, []}. 834 | 835 | reverse_tape(Acc, Str) -> 836 | reverse_tape(Acc, Str, ?SEP). 837 | 838 | reverse_tape([Sep|_T] = In, Str, Sep) -> 839 | {true, In, Str}; 840 | reverse_tape([H|T], Str, Sep) -> 841 | reverse_tape(T, [H|Str], Sep); 842 | reverse_tape([], Str, _Sep) -> 843 | {false, Str}. 844 | 845 | split_string(Str, End) -> 846 | split_string(Str, End, 1, []). 847 | 848 | split_string(Str, End, End, Acc) -> 849 | {lists:reverse(Acc), Str}; 850 | split_string([H|T], End, N, Acc) when N < End -> 851 | split_string(T, End, N+1, [H|Acc]); 852 | split_string([], _End, _N, Acc) -> 853 | {lists:reverse(Acc), []}. 854 | 855 | %% Parses PO file for finding all file information comments 856 | %% 857 | parse_po_comment(Fname) -> 858 | {ok,Bin} = file:read_file(Fname), 859 | parse_po_bin_comment(Bin). 860 | 861 | parse_po_bin_comment(Bin) -> 862 | parse_po_file_comment(binary_to_list(Bin),[]). 863 | 864 | parse_po_file_comment("msgid" ++ T,FileList) -> 865 | {Key, R0} = get_po_string(T), 866 | {Val, Rest} = get_msgstr(R0), 867 | if 868 | Key==header_info -> 869 | 870 | [{Key,Val} | parse_po_file_comment(Rest,[])]; 871 | true -> 872 | RevList = lists:reverse(FileList), 873 | [{Key,RevList} | parse_po_file_comment(Rest,[])] 874 | end; 875 | parse_po_file_comment("#"++ T,FileList) -> 876 | {Val,RO} = get_po_comment(T,[]), 877 | parse_po_file_comment(RO,[Val | FileList]); 878 | parse_po_file_comment([_ | T],FileList) -> 879 | parse_po_file_comment(T,FileList); 880 | parse_po_file_comment([],_FileList) -> 881 | []. 882 | 883 | get_msgstr("msgstr" ++ T) -> 884 | get_po_string(T); 885 | get_msgstr([_ | T]) -> 886 | get_msgstr(T). 887 | 888 | get_po_comment([$\n|T],Val) -> 889 | {lists:reverse(Val),T}; 890 | get_po_comment([Char|T],Val) -> 891 | get_po_comment(T,[Char | Val]). 892 | 893 | get_po_string([$\s|T]) -> get_po_string(T); 894 | get_po_string([$\r|T]) -> get_po_string(T); 895 | get_po_string([$\n|T]) -> get_po_string(T); 896 | get_po_string([$\t|T]) -> get_po_string(T); 897 | get_po_string([$"|T]) -> header_info(eat_string(T)). %" make emacs happy 898 | 899 | %%% only header-info has empty po-string ! 900 | header_info({"",R}) -> {header_info, R}; 901 | header_info(X) -> X. 902 | 903 | eat_string(S) -> 904 | eat_string(S,[]). 905 | 906 | eat_string([$\\,$"|T], Acc) -> eat_string(T, [$"|Acc]); % unescape ! 907 | eat_string([$\\,$\\ |T], Acc) -> eat_string(T, [$\\|Acc]); % unescape ! 908 | eat_string([$\\,$n |T], Acc) -> eat_string(T, [$\n|Acc]); % unescape ! 909 | eat_string([$"|T], Acc) -> eat_more(T,Acc); %" make emacs happy 910 | eat_string([H|T], Acc) -> eat_string(T, [H|Acc]). 911 | 912 | eat_more([$\s|T], Acc) -> eat_more(T, Acc); 913 | eat_more([$\n|T], Acc) -> eat_more(T, Acc); 914 | eat_more([$\r|T], Acc) -> eat_more(T, Acc); 915 | eat_more([$\t|T], Acc) -> eat_more(T, Acc); 916 | eat_more([$"|T], Acc) -> eat_string(T, Acc); %" make emacs happy 917 | eat_more(T, Acc) -> {lists:reverse(Acc), T}. 918 | 919 | 920 | %% this function saves new entry in dets 'bugs' table which is placed in 921 | %% ROOT_DIR 922 | %% 923 | report_bug(ActualValue,ProperValue,Lang) -> 924 | FileName = filename:join([?ROOT_DIR,?BUG_FILE]), 925 | case dets:open_file(bugs,[{file,FileName}]) of 926 | {ok,bugs} -> 927 | case dets:insert(bugs,{ActualValue,{ProperValue,Lang}}) of 928 | ok -> 929 | Res = {ok,"New bug reported"}; 930 | {error,_} -> 931 | Res = {error,"Problem with inserting message"} 932 | end; 933 | {error,_} -> 934 | 935 | Res = {error,"Problem with inserting message"} 936 | end, 937 | dets:sync(bugs), 938 | dets:close(bugs), 939 | Res. 940 | 941 | %% go thought all the reported bugs with the dets:traverse function and 942 | %% asks what to do witch them bu using the check_bug_action function 943 | check_bugs() -> 944 | FileName = filename:join([?ROOT_DIR,?BUG_FILE]), 945 | F = fun({ActualValue,{ProperValue,Lang}}) -> 946 | case check_bug_action(ActualValue,ProperValue,Lang) of 947 | ok -> 948 | continue; 949 | ret -> 950 | ret 951 | end 952 | end, 953 | case dets:open_file(bugs,[{file,FileName}]) of 954 | {ok,bugs} -> 955 | dets:traverse(bugs,F); 956 | {error,_} -> 957 | erlang:error({dets,open_file,FileName}) 958 | end, 959 | dets:sync(?TABLE_NAME), 960 | Save = fun(X)-> 961 | save_to_po_file(X,custom) 962 | end, 963 | Langs = all_langs(), 964 | lists:foreach(Save,Langs), 965 | dets:sync(bugs), 966 | dets:close(bugs), 967 | true. 968 | 969 | %% find similar entries in the gettext database it is looking 970 | %% by checking the similarity of the dets values (not keys) 971 | find_similar_by_value(Val,Lang)-> 972 | Strings = dets:match(?TABLE_NAME, {{'$1',Lang},'$2'}), 973 | F = fun([SKey,SVal],Acc) -> 974 | case compare(Val,SVal) of 975 | true -> 976 | [{SKey,SVal} | Acc]; 977 | 978 | false -> 979 | Acc 980 | end 981 | end, 982 | 983 | lists:foldl(F,[],Strings). 984 | 985 | %% bug action -it receive an input from the user - and do 986 | %% appropriate actions 987 | check_bug_action(ActualValue,ProperValue,Lang) -> 988 | 989 | io:format("Lang:~p~n" 990 | "ActualValue:~p~n" 991 | "ProposedValue:~p~n" 992 | ,[Lang,ActualValue,ProperValue]), 993 | 994 | io:format("Skip(s)~n" 995 | "AddValue(a)~n" 996 | "Ignore(i)~n" 997 | "Return(r)~n",[]), 998 | Action = io:get_line(?PROMPT), 999 | case Action of 1000 | "s\n" -> 1001 | ok; 1002 | "r\n" -> 1003 | ret; 1004 | "a\n" -> 1005 | Similar = find_similar_by_value(ActualValue,Lang), 1006 | print_possibilities(Similar,with_key), 1007 | 1008 | NewKey = get_new_key(Similar), 1009 | NewValue = get_new_value(Similar,ProperValue), 1010 | dets:insert(?TABLE_NAME,{{NewKey,Lang},NewValue}), 1011 | dets:delete(bugs,ActualValue), 1012 | ok; 1013 | "i\n" -> 1014 | dets:delete(bugs,ActualValue), 1015 | ok; 1016 | _Other -> 1017 | check_bug_action(ActualValue,ProperValue,Lang) 1018 | end. 1019 | 1020 | %% get new Key. if the number is specyfied it gets the value 1021 | %% from function argument which should be a list of {NewKey,NewVal} entries 1022 | %% returns new key 1023 | 1024 | get_new_key(Similar) -> 1025 | 1026 | case get_int('Key>') of 1027 | {error,Input}-> 1028 | Input; 1029 | Num -> 1030 | if 1031 | Num = 1032 | {NewKey,_Val} = lists:nth(Num,Similar), 1033 | NewKey; 1034 | true -> 1035 | io:format("wrong value~n"), 1036 | get_new_key(Similar) 1037 | end 1038 | end. 1039 | 1040 | %% get new Key. if the number is specyfied it gets the value 1041 | %% from function argument which should be a list of {NewKey,NewVal} entries 1042 | %% returns new value 1043 | get_new_value(Similar,ProperVal) -> 1044 | 1045 | case get_int('Value>') of 1046 | {error,""} -> 1047 | ProperVal; 1048 | {error,Input}-> 1049 | Input; 1050 | Num -> 1051 | if 1052 | Num = 1053 | {_NewKey,NewVal} = lists:nth(Num,Similar), 1054 | NewVal; 1055 | true -> 1056 | io:format("wrong value~n"), 1057 | get_new_value(Similar,ProperVal) 1058 | end 1059 | end. 1060 | 1061 | 1062 | %% This function returns all languages that are in the system, based on the 1063 | %% content of the ../gettext/priv/lang directory 1064 | all_langs() -> 1065 | gettext:all_lang() -- [gettext:default_lang()]. 1066 | 1067 | %% CustomPath = filename:join(?ROOT_DIR,?CUSTOM_DIR), 1068 | %% {ok,AllCustom} = file:list_dir(CustomPath), 1069 | %% DirList = [ X || X<-AllCustom, 1070 | %% filelib:is_dir(filename:join(CustomPath,X))==true], 1071 | %% LangList = [ X || X<-DirList, 1072 | %% filelib:is_regular(filename:join(CustomPath,X)++ 1073 | %% "/gettext.po")==ntrue], 1074 | %% LangList. 1075 | 1076 | 1077 | 1078 | -------------------------------------------------------------------------------- /src/gettext_compile.erl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etnt/gettext/9ab211734cf59415617b47c06f77504dd442b40f/src/gettext_compile.erl -------------------------------------------------------------------------------- /src/gettext_format.erl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etnt/gettext/9ab211734cf59415617b47c06f77504dd442b40f/src/gettext_format.erl -------------------------------------------------------------------------------- /src/gettext_internal.hrl: -------------------------------------------------------------------------------- 1 | %% Permission is hereby granted, free of charge, to any person obtaining a 2 | %% copy of this software and associated documentation files (the 3 | %% "Software"), to deal in the Software without restriction, including 4 | %% without limitation the rights to use, copy, modify, merge, publish, 5 | %% distribute, sublicense, and/or sell copies of the Software, and to permit 6 | %% persons to whom the Software is furnished to do so, subject to the 7 | %% following conditions: 8 | %% 9 | %% The above copyright notice and this permission notice shall be included 10 | %% in all copies or substantial portions of the Software. 11 | %% 12 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 13 | %% OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 14 | %% MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 15 | %% NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 16 | %% DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 17 | %% OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 18 | %% USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | -define(GETTEXT_HEADER_INFO, header_info). 21 | 22 | -define(DEFAULT_LANG, "en"). 23 | -define(DEFAULT_CHARSET, "iso-8859-1"). 24 | 25 | -define(EPOT_TABLE, epot). 26 | 27 | -define(LANG_DIR, "lang"). 28 | -define(DEFAULT_DIR, "default"). 29 | -define(CUSTOM_DIR, "custom"). 30 | -define(POFILE, "gettext.po"). 31 | 32 | -define(ENV_CBMOD, "GETTEXT_CBMOD"). 33 | -------------------------------------------------------------------------------- /src/gettext_iso639.erl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etnt/gettext/9ab211734cf59415617b47c06f77504dd442b40f/src/gettext_iso639.erl -------------------------------------------------------------------------------- /src/gettext_server.erl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etnt/gettext/9ab211734cf59415617b47c06f77504dd442b40f/src/gettext_server.erl -------------------------------------------------------------------------------- /src/gettext_sup.erl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etnt/gettext/9ab211734cf59415617b47c06f77504dd442b40f/src/gettext_sup.erl -------------------------------------------------------------------------------- /src/gettext_validate.erl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etnt/gettext/9ab211734cf59415617b47c06f77504dd442b40f/src/gettext_validate.erl -------------------------------------------------------------------------------- /src/gettext_validate_bad_case.erl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etnt/gettext/9ab211734cf59415617b47c06f77504dd442b40f/src/gettext_validate_bad_case.erl -------------------------------------------------------------------------------- /src/gettext_validate_bad_html.erl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etnt/gettext/9ab211734cf59415617b47c06f77504dd442b40f/src/gettext_validate_bad_html.erl -------------------------------------------------------------------------------- /src/gettext_validate_bad_punct.erl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etnt/gettext/9ab211734cf59415617b47c06f77504dd442b40f/src/gettext_validate_bad_punct.erl -------------------------------------------------------------------------------- /src/gettext_validate_bad_stxt.erl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etnt/gettext/9ab211734cf59415617b47c06f77504dd442b40f/src/gettext_validate_bad_stxt.erl -------------------------------------------------------------------------------- /src/gettext_validate_bad_ws.erl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etnt/gettext/9ab211734cf59415617b47c06f77504dd442b40f/src/gettext_validate_bad_ws.erl -------------------------------------------------------------------------------- /src/gettext_validate_no_trans.erl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etnt/gettext/9ab211734cf59415617b47c06f77504dd442b40f/src/gettext_validate_no_trans.erl -------------------------------------------------------------------------------- /src/gettext_yaws_html.erl: -------------------------------------------------------------------------------- 1 | %% -*- coding: latin-1 -*- 2 | %% ------------------------------------------------------------------------- 3 | %% Copyright (c) 2003, Johan Bevemyr. All rights reserved. 4 | %% 5 | %% Redistribution and use in source and binary forms, with or without 6 | %% modification, are permitted provided that the following conditions are 7 | %% met: 8 | %% * Redistributions of source code must retain the above copyright 9 | %% notice, this list of conditions and the following disclaimer. 10 | %% * Redistributions in binary form must reproduce the above copyright 11 | %% notice, this list of conditions and the following disclaimer in the 12 | %% documentation and/or other materials provided with the distribution. 13 | %% * Neither the name of "Yaws" nor the names of its contributors may be 14 | %% used to endorse or promote products derived from this software 15 | %% without specific prior written permission. 16 | %% 17 | %% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 18 | %% IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 19 | %% THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 20 | %% PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 21 | %% CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 22 | %% EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 23 | %% PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 24 | %% PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 25 | %% LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 26 | %% NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | %% SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | %% 29 | %% @copyright 2003 Johan Bevemyr. All rights reserved. 30 | %% @private 31 | %% @author Johan Bevemyr 32 | %% @doc HTML parser from Yaws, to allow gettext_validate.erl to check HTML 33 | %% in po file texts. Transforms HTML to an Erlang represention (ehtml). 34 | 35 | -module(gettext_yaws_html). 36 | 37 | -export([parse/1,parse/2,h2e/1]). 38 | 39 | parse(Name) -> 40 | {ok, B} = file:read_file(Name), 41 | h2e(binary_to_list(B)). 42 | 43 | parse(Name,Out) -> 44 | {ok, B} = file:read_file(Name), 45 | case h2e(binary_to_list(B)) of 46 | {ehtml, [], Ehtml} -> 47 | Cont = io_lib:format("~p", [{ehtml, Ehtml}]), 48 | file:write_file(Out, Cont); 49 | Error -> 50 | Error 51 | end. 52 | 53 | h2e(Input) -> 54 | Tokens = tokenize(Input, [], [], 1), 55 | parse(Tokens, {ehtml,[],0}, [], []). 56 | 57 | % parse(Tokens, Stack, Acc) 58 | 59 | parse([], {T,A,_L}, [], Acc) -> 60 | {T, A, lists:reverse(Acc)}; 61 | parse([], {T,A,_L}, [{CTag,CAcc}|Stack], Acc) -> 62 | %% gettext_validate_bad_html.erl doesn't care about this (broken html 63 | %% is expected), suppress io:format(...) as they should only see the 64 | %% validator output 65 | %% io:format("Unterminated tag '~p' at line ~p\n", [T,L]), 66 | parse([], CTag, Stack, [{T,A,lists:reverse(Acc)}|CAcc]); 67 | parse([{begin_tag,T,A,L}|Tokens], CTag, Stack, Acc) -> 68 | case tag_type(T) of 69 | leaf -> 70 | parse(Tokens, CTag, Stack, [{T,A}|Acc]); 71 | node -> 72 | parse(Tokens, {T,A,L}, [{CTag,Acc}|Stack],[]) 73 | end; 74 | 75 | parse([{end_tag,T,[],_L}|Tokens], {T,A,_}, [{CTag,CAcc}|Stack], Acc) -> 76 | E = case Acc of 77 | [Single] -> 78 | {T,A,Single}; 79 | _ -> 80 | {T,A,lists:reverse(Acc)} 81 | end, 82 | parse(Tokens, CTag, Stack, [E|CAcc]); 83 | 84 | parse([{end_tag,T1,[],L1}|Tokens], CTag = {T2,_A,L2}, Stack, Acc) -> 85 | case tag_type(T1) of 86 | leaf -> % ignore 87 | parse(Tokens, CTag, Stack, Acc); 88 | node -> 89 | Msg = lists:flatten(io_lib:format( 90 | "expected '' on line ~p, start " 91 | "tag at line: ~p", [T2,L1,L2])), 92 | {error, Msg} 93 | end; 94 | 95 | parse([{data, Data, _Line}|Tokens], CTag, Stack, Acc) -> 96 | case skip_space(Data, 0) of 97 | {[], _} -> 98 | parse(Tokens, CTag, Stack, Acc); 99 | _ -> 100 | parse(Tokens, CTag, Stack, [Data|Acc]) 101 | end. 102 | % 103 | 104 | tag_type(p) -> leaf; 105 | tag_type(hr) -> leaf; 106 | tag_type(input) -> leaf; 107 | tag_type(base) -> leaf; 108 | tag_type(img) -> leaf; 109 | tag_type('!doctype') -> leaf; 110 | tag_type(meta) -> leaf; 111 | tag_type(link) -> leaf; 112 | tag_type(br) -> leaf; 113 | tag_type(_) -> node. 114 | 115 | % tokenize(Input, DataAcc, TokenAcc, LineNr) 116 | 117 | tokenize([], [], Tokens, _Line) -> 118 | lists:reverse(Tokens); 119 | tokenize([], Acc, Tokens, Line) -> 120 | lists:reverse([{data, lists:reverse(Acc), Line}|Tokens]); 121 | tokenize([$<,$!,$-,$-|R0], Acc, Tokens, L0) -> 122 | {R1, L1} = skip_comment(R0,L0), 123 | tokenize(R1, Acc, Tokens, L1); 124 | tokenize([$<|R0], Acc, Tokens, L0) -> 125 | {Tag,R1,L1} = scan_tag(R0,L0), 126 | if 127 | Acc == [] -> 128 | next_token(Tag, R1, [Tag|Tokens], L1); 129 | true -> 130 | Data = {data,lists:reverse(Acc),L0}, 131 | next_token(Tag, R1, [Tag,Data|Tokens], L1) 132 | end; 133 | tokenize([C=$\n|R0], Acc, Tokens, L) -> 134 | tokenize(R0, [C|Acc], Tokens, L+1); 135 | tokenize([C=$\r|R0], Acc, Tokens, L) -> 136 | tokenize(R0, [C|Acc], Tokens, L+1); 137 | tokenize([C|R0], Acc, Tokens, L) -> 138 | tokenize(R0, [C|Acc], Tokens, L). 139 | 140 | % 141 | 142 | next_token({begin_tag, script, _, _}, R, Tokens, L) -> 143 | {Data, R1, L1} = scan_endtag(R, "script", L), 144 | tokenize(R1, [], [{data, Data, L}|Tokens], L1); 145 | next_token({begin_tag, style, _, _}, R, Tokens, L) -> 146 | {Data, R1, L1} = scan_endtag(R, "style", L), 147 | tokenize(R1, [], [{data, Data, L}|Tokens], L1); 148 | next_token(_Tag, R, Tokens, L) -> 149 | tokenize(R, [], Tokens, L). 150 | 151 | %% '<' + [*['=']]* ['/'] '>' 152 | 153 | scan_tag([$/|I], L) -> 154 | {_R0,L0} = skip_space(I, L), 155 | {Name,R1,L1} = scan_tag_name(I, L0), 156 | {R2,L2} = skip_space(R1, L1), 157 | {Args,R3,L3} = scan_tag_args(R2, L2), 158 | {{end_tag,list_to_atom(lowercase(Name)),Args,L0}, R3, L3}; 159 | scan_tag(I, L) -> 160 | {_R0,L0} = skip_space(I, L), 161 | {Name,R1,L1} = scan_tag_name(I, L0), 162 | {R2,L2} = skip_space(R1, L1), 163 | {Args,R3,L3} = scan_tag_args(R2, L2), 164 | {{begin_tag,list_to_atom(lowercase(Name)),Args,L0}, R3, L3}. 165 | 166 | % 167 | 168 | scan_tag_name(I, L) -> 169 | scan_token(I, [], L). 170 | 171 | % 172 | 173 | scan_tag_args(I, L) -> 174 | scan_tag_args(I, [], L). 175 | 176 | scan_tag_args([], Acc, L) -> 177 | {lists:reverse(Acc), [], L}; 178 | scan_tag_args([$>|R], Acc, L) -> 179 | {lists:reverse(Acc), R, L}; 180 | scan_tag_args(R=[$<|_], Acc, L) -> %% bad html 181 | {lists:reverse(Acc), R, L}; 182 | scan_tag_args(R0, Acc, L0) -> 183 | {Name,R1,L1} = scan_value(R0, L0), 184 | {R2, L2} = skip_space(R1, L1), 185 | case R2 of 186 | [$=|R3] -> 187 | {R4,L4} = skip_space(R3, L2), 188 | {Value,R5,L5} = scan_value(R4, L4), 189 | {R6,L6} = skip_space(R5, L5), 190 | OptName = list_to_atom(lowercase(Name)), 191 | scan_tag_args(R6, [{OptName,Value}|Acc], L6); 192 | _ -> 193 | scan_tag_args(R2, [Name|Acc], L2) 194 | end. 195 | 196 | % 197 | 198 | scan_value([$"|R], L) -> 199 | scan_quote(R, [], $", L); 200 | scan_value([$'|R], L) -> 201 | scan_quote(R, [], $', L); 202 | scan_value(R, L) -> 203 | scan_token(R, [], L). 204 | 205 | % 206 | 207 | scan_token([], Acc, L) -> 208 | {lists:reverse(Acc), [], L}; 209 | scan_token(R=[$>|_], Acc, L) -> 210 | {lists:reverse(Acc), R, L}; 211 | scan_token(R=[$<|_], Acc, L) -> %% bad html 212 | {lists:reverse(Acc), R, L}; 213 | scan_token(R=[$=|_], Acc, L) -> %% bad html 214 | {lists:reverse(Acc), R, L}; 215 | scan_token([C|R], Acc, L0) -> 216 | case char_class(C) of 217 | space -> 218 | {lists:reverse(Acc), R, L0}; 219 | nl -> 220 | {lists:reverse(Acc), R, L0+1}; 221 | _ -> 222 | scan_token(R, [C|Acc], L0) 223 | end. 224 | 225 | % 226 | 227 | scan_quote([], Acc, _Q, L) -> 228 | {lists:reverse(Acc), [], L}; 229 | scan_quote([Q|R], Acc, Q, L) -> 230 | {lists:reverse(Acc), R, L}; 231 | scan_quote([C=$\n|R], Acc, Q, L) -> 232 | scan_quote(R, [C|Acc], Q, L+1); 233 | scan_quote([C=$\r|R], Acc, Q, L) -> 234 | scan_quote(R, [C|Acc], Q, L+1); 235 | scan_quote([C|R], Acc, Q, L) -> 236 | scan_quote(R, [C|Acc], Q, L). 237 | 238 | % 239 | 240 | scan_endtag(R, Tag, L) -> 241 | scan_endtag(R, Tag, [], L). 242 | 243 | scan_endtag([], _Tag, Acc, L) -> 244 | {lists:reverse(Acc), [], L}; 245 | scan_endtag(R=[$<,$/|R0], Tag, Acc, L0) -> 246 | case casecmp(Tag, R0) of 247 | {true, R1} -> 248 | {R2,_} = skip_space(R1,L0), 249 | if hd(R2) == $> -> 250 | {lists:reverse(Acc), R, L0}; 251 | true -> 252 | scan_endtag(R0, Tag, Acc, L0) 253 | end; 254 | false -> 255 | scan_endtag(R0, Tag, Acc, L0) 256 | end; 257 | scan_endtag([C=$\n|R], Tag, Acc, L) -> 258 | scan_endtag(R, Tag, [C|Acc], L+1); 259 | scan_endtag([C=$\r|R], Tag, Acc, L) -> 260 | scan_endtag(R, Tag, [C|Acc], L+1); 261 | scan_endtag([C|R], Tag, Acc, L) -> 262 | scan_endtag(R, Tag, [C|Acc], L). 263 | 264 | % 265 | 266 | casecmp([], R) -> {true, R}; 267 | casecmp([C1|T1], [C2|T2]) -> 268 | C2low = lowercase_ch(C2), 269 | if C1 == C2low -> casecmp(T1,T2); 270 | true -> false 271 | end. 272 | 273 | % 274 | 275 | char_class($\n) -> nl; 276 | char_class($\r) -> nl; 277 | char_class($ ) -> space; 278 | char_class($\t) -> space; 279 | char_class(C) when C >= $a, C =< $z -> alpha; 280 | char_class(C) when C >= $A, C =< $Z -> alpha; 281 | char_class(C) when C >= $0, C =< $9 -> digit; 282 | char_class(_C) -> other. 283 | 284 | % 285 | 286 | skip_space([], L) -> 287 | {[], L}; 288 | skip_space(R = [C|R0], L) -> 289 | case char_class(C) of 290 | nl -> 291 | skip_space(R0, L+1); 292 | space -> 293 | skip_space(R0, L); 294 | _ -> 295 | {R, L} 296 | end. 297 | 298 | % 299 | 300 | skip_comment([], L) -> {[], L}; 301 | skip_comment([$-,$-,$>|R],L) -> {R,L}; 302 | skip_comment([$\n|R],L) -> skip_comment(R,L+1); 303 | skip_comment([$\r|R],L) -> skip_comment(R,L+1); 304 | skip_comment([_C|R],L) -> skip_comment(R,L). 305 | 306 | % 307 | 308 | lowercase(Str) -> 309 | [lowercase_ch(S) || S <- Str]. 310 | 311 | lowercase_ch(C) when C>=$A, C=<$Z -> C + 32; 312 | lowercase_ch(C) -> C. 313 | -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | gettext_server_db.dets -------------------------------------------------------------------------------- /test/Makefile: -------------------------------------------------------------------------------- 1 | MODULES=gettext_demo 2 | 3 | EBIN_FILES=$(MODULES:%=../ebin/%.beam) 4 | ERLC_FLAGS+=+debug_info 5 | 6 | tmp_name=tmp 7 | dir=. 8 | def_lang=en 9 | 10 | all: pot test 11 | 12 | po: $(dir)/lang/default/$(def_lang)/gettext.po 13 | 14 | %.po: $(dir)/lang/default/$(def_lang)/gettext.pot 15 | msginit -i $< -o $@ 16 | 17 | pot: $(dir)/lang/default/$(def_lang)/gettext.pot 18 | 19 | $(dir)/lang/default/$(def_lang)/gettext.pot: $(EBIN_FILES) 20 | erl -noshell -pa ../ebin -s gettext_compile epot2po 21 | install -D $(dir)/lang/$(tmp_name)/$(def_lang)/gettext.po \ 22 | $(dir)/lang/default/$(def_lang)/gettext.pot; \ 23 | 24 | clean: 25 | rm -rf lang/$(tmp_name) 26 | rm -f $(dir)/lang/default/$(def_lang)/gettext.pot 27 | rm -f gettext_server_db.dets 28 | rm -f $(EBIN_FILES) 29 | 30 | ../ebin/%.beam: %.erl $(INCLUDES) Makefile 31 | erlc -pa ../ebin -o ../ebin $(ERLC_FLAGS) +gettext $< 32 | 33 | test: $(EBIN_FILES) pot 34 | @erl -pa ../ebin -noshell \ 35 | -eval 'gettext_demo:hello_world()' \ 36 | -s init stop 37 | -------------------------------------------------------------------------------- /test/gettext_demo.erl: -------------------------------------------------------------------------------- 1 | -module(gettext_demo). 2 | 3 | -export([hello_world/0, hello_world/1]). 4 | 5 | %% configuration callbacks 6 | -export([gettext_dir/0, gettext_def_lang/0]). 7 | 8 | -include("../include/gettext.hrl"). 9 | 10 | -define(HELLO, "Hello World!"). 11 | 12 | hello_world() -> 13 | hello_world("en"). 14 | 15 | hello_world(LC) -> 16 | {ok, _} = gettext_server:start({?MODULE, []}), 17 | io:format("Using ?TXT/1 with default language: ~p~n", [?TXT(?HELLO)]), 18 | put(gettext_language, "sv"), 19 | io:format("Using ?TXT/1 with gettext_language set to \"sv\": ~p~n", 20 | [?TXT(?HELLO)]), 21 | io:format("Using ?TXT/2 with language=~p: ~p~n", 22 | [LC, ?TXT2(?HELLO, LC)]), 23 | io:format("Using ?TXT/2 with language=\"sv\": ~p~n", 24 | [?TXT2(?HELLO, "sv")]), 25 | put(gettext_language, "es"), 26 | io:format("Using ?TXT/1 with gettext_language set to \"es\": ~p~n", 27 | [?TXT(?HELLO)]), 28 | ok. 29 | 30 | 31 | %% configuration callbacks 32 | 33 | %% using code:priv_dir(gettext) might not work when building and testing 34 | %% this application, depending on the user's setup, to for this test we just 35 | %% use paths relative to the current directory 36 | 37 | gettext_dir() -> ".". % i.e., po files will be under ./lang/... 38 | 39 | gettext_def_lang() -> "en". 40 | -------------------------------------------------------------------------------- /test/lang/.gitignore: -------------------------------------------------------------------------------- 1 | tmp 2 | -------------------------------------------------------------------------------- /test/lang/custom/es/gettext.po: -------------------------------------------------------------------------------- 1 | # Team PO file for Spanish 2 | # Copyright (C) YYYY Organization 3 | # 4 | msgid "" 5 | msgstr "" 6 | "Project-Id-Version: PACKAGE VERSION\n" 7 | "POT-Creation-Date: 2006-07-01 16:45+0200\n" 8 | "PO-Revision-Date: 2012-05-21 13:37+0200\n" 9 | "Last-Translator: FULL NAME \n" 10 | "Language-Team: Spanish\n" 11 | "MIME-Version: 1.0\n" 12 | "Content-Type: text/plain; charset=Latin-1\n" 13 | "Content-Transfer-Encoding: 8bit\n" 14 | "Language: es_ES\n" 15 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 16 | 17 | msgid "Hello World!" 18 | msgstr "Hola Mundo!" 19 | -------------------------------------------------------------------------------- /test/lang/custom/sv/gettext.po: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etnt/gettext/9ab211734cf59415617b47c06f77504dd442b40f/test/lang/custom/sv/gettext.po -------------------------------------------------------------------------------- /test/lang/default/en/.gitignore: -------------------------------------------------------------------------------- 1 | gettext.pot -------------------------------------------------------------------------------- /test/lang/default/en/gettext.po: -------------------------------------------------------------------------------- 1 | # Team PO file for English 2 | # Copyright (C) YYYY Organization 3 | # 4 | msgid "" 5 | msgstr "" 6 | "Project-Id-Version: PACKAGE VERSION\n" 7 | "POT-Creation-Date: 2006-07-01 16:45+0200\n" 8 | "PO-Revision-Date: 2012-05-21 13:44+0200\n" 9 | "Last-Translator: FULL NAME \n" 10 | "Language-Team: English\n" 11 | "MIME-Version: 1.0\n" 12 | "Content-Type: text/plain; charset=Latin-1\n" 13 | "Content-Transfer-Encoding: 8bit\n" 14 | "Language: en_US\n" 15 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 16 | 17 | msgid "Hello World!" 18 | msgstr "Hello World!" 19 | --------------------------------------------------------------------------------