Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Preliminary blocktrans support

  • Loading branch information...
commit 694fc92e4623bae90c322cfa8dba6a409b12c40e 1 parent 1facccc
Evan Miller authored
View
24 README.markdown
@@ -3,7 +3,7 @@ ErlyDTL
ErlyDTL compiles Django Template Language to Erlang bytecode.
-*Supported tags*: autoescape, block, comment, cycle, extends, filter, firstof, for, if, ifequal, ifnotequal, include, now, spaceless, ssi, templatetag, trans, widthratio, with
+*Supported tags*: autoescape, block, blocktrans, comment, cycle, extends, filter, firstof, for, if, ifequal, ifnotequal, include, now, spaceless, ssi, templatetag, trans, widthratio, with
_Unsupported tags_: csrf_token, ifchanged, regroup, url
@@ -74,6 +74,15 @@ For example, adding {locale, "en_US"} will call {key2str, Key, "en_US"}
for all string marked as trans (`{% trans "StringValue" %}` on templates).
See README_I18N.
+* `blocktrans_fun` - A two-argument fun to use for translating `blocktrans`
+blocks. This will be called once for each pair of `blocktrans` block and locale
+specified in `blocktrans_locales`. The fun should take the form:
+
+ Fun(BlockName, Locale) -> <<"ErlyDTL code">> | default
+
+* `blocktrans_locales` - A list of locales to be passed to `blocktrans_fun`.
+Defaults to [].
+
Helper compilation
------------------
@@ -105,12 +114,17 @@ values can be atoms, strings, binaries, or (nested) variables.
IOList is the rendered template.
- my_compiled_template:render(Variables, TranslationFun) ->
+ my_compiled_template:render(Variables, Options) ->
{ok, IOList} | {error, Err}
-Same as `render/1`, but TranslationFun is a fun/1 that will be used to
-translate strings appearing inside `{% trans %}` tags. The simplest
-TranslationFun would be `fun(Val) -> Val end`
+Same as `render/1`, but with the following options:
+
+* `translation_fun` - A fun/1 that will be used to translate strings appearing
+inside `{% trans %}` tags. The simplest TranslationFun would be `fun(Val) ->
+Val end`
+
+* `locale` - A string specifying the current locale, for use with the
+`blocktrans_fun` compile-time option.
my_compiled_template:translatable_strings() -> [String]
View
64 src/erlydtl_compiler.erl
@@ -43,6 +43,8 @@
-record(dtl_context, {
local_scopes = [],
block_dict = dict:new(),
+ blocktrans_fun = none,
+ blocktrans_locales = [],
auto_escape = off,
doc_root = "",
parse_trail = [],
@@ -111,14 +113,11 @@ compile_dir(Dir, Module) ->
compile_dir(Dir, Module, Options) ->
Context = init_dtl_context_dir(Dir, Module, Options),
- Files = case file:list_dir(Dir) of
- {ok, FileList} -> FileList;
- _ -> []
- end,
+ Files = filelib:fold_files(Dir, ".*", true, fun(F1,Acc1) -> [F1 | Acc1] end, []),
{ParserResults, ParserErrors} = lists:foldl(fun
("."++_, Acc) -> Acc;
- (File, {ResultAcc, ErrorAcc}) ->
- FilePath = filename:join([Dir, File]),
+ (FilePath, {ResultAcc, ErrorAcc}) ->
+ File = filename:basename(FilePath),
case parse(FilePath, Context) of
ok -> {ResultAcc, ErrorAcc};
{ok, DjangoParseTree, CheckSum} -> {[{File, DjangoParseTree, CheckSum}|ResultAcc], ErrorAcc};
@@ -210,6 +209,8 @@ init_dtl_context(File, Module, Options) ->
doc_root = proplists:get_value(doc_root, Options, filename:dirname(File)),
custom_tags_dir = proplists:get_value(custom_tags_dir, Options, filename:join([erlydtl_deps:get_base_dir(), "priv", "custom_tags"])),
custom_tags_module = proplists:get_value(custom_tags_module, Options, Ctx#dtl_context.custom_tags_module),
+ blocktrans_fun = proplists:get_value(blocktrans_fun, Options, Ctx#dtl_context.blocktrans_fun),
+ blocktrans_locales = proplists:get_value(blocktrans_locales, Options, Ctx#dtl_context.blocktrans_locales),
vars = proplists:get_value(vars, Options, Ctx#dtl_context.vars),
reader = proplists:get_value(reader, Options, Ctx#dtl_context.reader),
compiler_options = proplists:get_value(compiler_options, Options, Ctx#dtl_context.compiler_options),
@@ -226,6 +227,8 @@ init_dtl_context_dir(Dir, Module, Options) ->
doc_root = proplists:get_value(doc_root, Options, Dir),
custom_tags_dir = proplists:get_value(custom_tags_dir, Options, filename:join([erlydtl_deps:get_base_dir(), "priv", "custom_tags"])),
custom_tags_module = proplists:get_value(custom_tags_module, Options, Module),
+ blocktrans_fun = proplists:get_value(blocktrans_fun, Options, Ctx#dtl_context.blocktrans_fun),
+ blocktrans_locales = proplists:get_value(blocktrans_locales, Options, Ctx#dtl_context.blocktrans_locales),
vars = proplists:get_value(vars, Options, Ctx#dtl_context.vars),
reader = proplists:get_value(reader, Options, Ctx#dtl_context.reader),
compiler_options = proplists:get_value(compiler_options, Options, Ctx#dtl_context.compiler_options),
@@ -299,7 +302,7 @@ parse(CheckSum, Data, Context) ->
end.
parse(Data) ->
- case erlydtl_scanner:scan(binary_to_list(Data)) of
+ case erlydtl_scanner:scan(unicode:characters_to_list(Data)) of
{ok, Tokens} ->
erlydtl_parser:parse(Tokens);
Err ->
@@ -369,7 +372,7 @@ custom_forms(Dir, Module, Functions, AstInfo) ->
FunctionAsts = lists:foldl(fun({_, Function1, Function2}, Acc) -> [Function1, Function2 | Acc] end, [], Functions),
[erl_syntax:revert(X) || X <- [ModuleAst, ExportAst, SourceFunctionAst, DependenciesFunctionAst, TranslatableStringsFunctionAst
- | FunctionAsts]].
+ | FunctionAsts] ++ AstInfo#ast_info.pre_render_asts].
forms(File, Module, {BodyAst, BodyInfo}, {CustomTagsFunctionAst, CustomTagsInfo}, CheckSum) ->
MergedInfo = merge_info(BodyInfo, CustomTagsInfo),
@@ -380,16 +383,25 @@ forms(File, Module, {BodyAst, BodyInfo}, {CustomTagsFunctionAst, CustomTagsInfo}
[erl_syntax:clause([erl_syntax:variable("Variables")], none,
[erl_syntax:application(none,
erl_syntax:atom(render),
- [erl_syntax:variable("Variables"), erl_syntax:atom(none)])])]),
+ [erl_syntax:variable("Variables"), erl_syntax:list([])])])]),
Function2 = erl_syntax:application(none, erl_syntax:atom(render_internal),
- [erl_syntax:variable("Variables"), erl_syntax:variable("TranslationFun")]),
+ [erl_syntax:variable("Variables"),
+ erl_syntax:application(
+ erl_syntax:atom(proplists),
+ erl_syntax:atom(get_value),
+ [erl_syntax:atom(translation_fun), erl_syntax:variable("Options"), erl_syntax:atom(none)]),
+ erl_syntax:application(
+ erl_syntax:atom(proplists),
+ erl_syntax:atom(get_value),
+ [erl_syntax:atom(locale), erl_syntax:variable("Options"), erl_syntax:atom(none)])
+ ]),
ClauseOk = erl_syntax:clause([erl_syntax:variable("Val")], none,
[erl_syntax:tuple([erl_syntax:atom(ok), erl_syntax:variable("Val")])]),
ClauseCatch = erl_syntax:clause([erl_syntax:variable("Err")], none,
[erl_syntax:tuple([erl_syntax:atom(error), erl_syntax:variable("Err")])]),
Render2FunctionAst = erl_syntax:function(erl_syntax:atom(render),
[erl_syntax:clause([erl_syntax:variable("Variables"),
- erl_syntax:variable("TranslationFun")], none,
+ erl_syntax:variable("Options")], none,
[erl_syntax:try_expr([Function2], [ClauseOk], [ClauseCatch])])]),
SourceFunctionTuple = erl_syntax:tuple(
@@ -409,7 +421,7 @@ forms(File, Module, {BodyAst, BodyInfo}, {CustomTagsFunctionAst, CustomTagsInfo}
RenderInternalFunctionAst = erl_syntax:function(
erl_syntax:atom(render_internal),
- [erl_syntax:clause([erl_syntax:variable("Variables"), erl_syntax:variable("TranslationFun")], none,
+ [erl_syntax:clause([erl_syntax:variable("Variables"), erl_syntax:variable("TranslationFun"), erl_syntax:variable("CurrentLocale")], none,
[BodyAstTmp])]),
ModuleAst = erl_syntax:attribute(erl_syntax:atom(module), [erl_syntax:atom(Module)]),
@@ -465,6 +477,8 @@ body_ast(DjangoParseTree, Context, TreeWalker) ->
_ -> Contents
end,
body_ast(Block, Context, TreeWalkerAcc);
+ ({'blocktrans', {identifier, _, Name}, Contents}, TreeWalkerAcc) ->
+ blocktrans_ast(Name, Contents, Context, TreeWalkerAcc);
({'call', {'identifier', _, Name}}, TreeWalkerAcc) ->
call_ast(Name, TreeWalkerAcc);
({'call', {'identifier', _, Name}, With}, TreeWalkerAcc) ->
@@ -634,6 +648,27 @@ with_dependency(FilePath, {{Ast, Info}, TreeWalker}) ->
empty_ast(TreeWalker) ->
{{erl_syntax:list([]), #ast_info{}}, TreeWalker}.
+blocktrans_ast(Name, Contents, Context, TreeWalker) ->
+ case Context#dtl_context.blocktrans_fun of
+ none ->
+ body_ast(Contents, Context, TreeWalker);
+ BlockTransFun when is_function(BlockTransFun) ->
+ {{DefaultAst, AstInfo}, TreeWalker1} = body_ast(Contents, Context, TreeWalker),
+ {FinalAstInfo, FinalTreeWalker, Clauses} = lists:foldr(fun(Locale, {AstInfoAcc, ThisTreeWalker, ClauseAcc}) ->
+ case BlockTransFun(Name, Locale) of
+ default ->
+ {AstInfoAcc, ThisTreeWalker, ClauseAcc};
+ Body ->
+ {ok, DjangoParseTree} = parse(Body),
+ {{ThisAst, ThisAstInfo}, TreeWalker2} = body_ast(DjangoParseTree, Context, ThisTreeWalker),
+ {merge_info(ThisAstInfo, AstInfoAcc), TreeWalker2,
+ [erl_syntax:clause([erl_syntax:string(Locale)], none, [ThisAst])|ClauseAcc]}
+ end
+ end, {AstInfo, TreeWalker1, []}, Context#dtl_context.blocktrans_locales),
+ Ast = erl_syntax:case_expr(erl_syntax:variable("CurrentLocale"),
+ Clauses ++ [erl_syntax:clause([erl_syntax:underscore()], none, [DefaultAst])]),
+ {{Ast, FinalAstInfo}, FinalTreeWalker}
+ end.
translated_ast({string_literal, _, String}, Context, TreeWalker) ->
NewStr = unescape_string_literal(String),
@@ -1060,7 +1095,10 @@ call_ast(Module, Variable, AstInfo, TreeWalker) ->
AppAst = erl_syntax:application(
erl_syntax:atom(Module),
erl_syntax:atom(render),
- [Variable, erl_syntax:variable("TranslationFun")]),
+ [Variable, erl_syntax:list([
+ erl_syntax:tuple([erl_syntax:atom(translation_fun), erl_syntax:variable("TranslationFun")]),
+ erl_syntax:tuple([erl_syntax:atom(locale), erl_syntax:variable("CurrentLocale")])
+ ])]),
RenderedAst = erl_syntax:variable("Rendered"),
OkAst = erl_syntax:clause(
[erl_syntax:tuple([erl_syntax:atom(ok), RenderedAst])],
View
8 src/erlydtl_filters.erl
@@ -269,7 +269,7 @@ fix_ampersands(Input) when is_list(Input) ->
fix_ampersands(Input, []).
%% @doc When used without an argument, rounds a floating-point number to one decimal place
-%% @doc -- but only if there's a decimal part to be displayed
+%% -- but only if there's a decimal part to be displayed
floatformat(Number, Place) when is_binary(Number) ->
floatformat(binary_to_list(Number), Place);
floatformat(Number, Place) ->
@@ -420,7 +420,7 @@ lower(Input) ->
string:to_lower(Input).
%% @doc Returns the value turned into a list. For an integer, it's a list of digits.
-%% @doc For a string, it's a list of characters.
+%% For a string, it's a list of characters.
%% Added this for DTL compatibility, but since strings are lists in Erlang, no need for this.
make_list(Input) when is_binary(Input) ->
make_list(binary_to_list(Input));
@@ -750,7 +750,7 @@ truncatewords_html(Input, Max) when is_binary(Input) ->
truncatewords_html(Input, Max) ->
truncatewords_html(Input, Max, [], [], text).
-%% @doc Recursively takes a self-nested list and returns an HTML unordered list -- WITHOUT opening and closing <ul> tags.
+%% @doc Recursively takes a self-nested list and returns an HTML unordered list -- WITHOUT opening and closing `<ul>' tags.
unordered_list(List) ->
String = lists:flatten(unordered_list(List, [])),
string:substr(String, 5, erlang:length(String) - 9).
@@ -784,7 +784,7 @@ wordcount(Input) when is_binary(Input) ->
wordcount(Input) when is_list(Input) ->
wordcount(Input, 0).
-%% @doc Wraps words at specified line length, uses <BR/> html tag to delimit lines
+%% @doc Wraps words at specified line length, uses `<BR/>' html tag to delimit lines
wordwrap(Input, Number) when is_binary(Input) ->
wordwrap(binary_to_list(Input), Number);
wordwrap(Input, Number) when is_list(Input) ->
View
26 src/erlydtl_parser.yrl
@@ -100,6 +100,7 @@ Nonterminals
SSITag
+ BlockTransBlock
TransTag
TemplatetagTag
@@ -120,6 +121,7 @@ Terminals
and_keyword
autoescape_keyword
block_keyword
+ blocktrans_keyword
call_keyword
close_tag
close_var
@@ -129,6 +131,7 @@ Terminals
empty_keyword
endautoescape_keyword
endblock_keyword
+ endblocktrans_keyword
endcomment_keyword
endfilter_keyword
endfor_keyword
@@ -161,14 +164,14 @@ Terminals
string_literal
string
templatetag_keyword
- openblock_keyword
- closeblock_keyword
- openvariable_keyword
- closevariable_keyword
- openbrace_keyword
- closebrace_keyword
- opencomment_keyword
- closecomment_keyword
+ openblock_keyword
+ closeblock_keyword
+ openvariable_keyword
+ closevariable_keyword
+ openbrace_keyword
+ closebrace_keyword
+ opencomment_keyword
+ closecomment_keyword
trans_keyword
widthratio_keyword
with_keyword
@@ -176,7 +179,8 @@ Terminals
'==' '!='
'>=' '<='
'>' '<'
- '(' ')'.
+ '(' ')'
+ '_'.
Rootsymbol
Elements.
@@ -191,6 +195,7 @@ Elements -> '$empty' : [].
Elements -> Elements string : '$1' ++ ['$2'].
Elements -> Elements AutoEscapeBlock : '$1' ++ ['$2'].
Elements -> Elements BlockBlock : '$1' ++ ['$2'].
+Elements -> Elements BlockTransBlock : '$1' ++ ['$2'].
Elements -> Elements CallTag : '$1' ++ ['$2'].
Elements -> Elements CallWithTag : '$1' ++ ['$2'].
Elements -> Elements CommentBlock : '$1' ++ ['$2'].
@@ -313,6 +318,8 @@ SpacelessBlock -> open_tag spaceless_keyword close_tag Elements open_tag endspac
SSITag -> open_tag ssi_keyword Value close_tag : {ssi, '$3'}.
SSITag -> open_tag ssi_keyword string_literal parsed_keyword close_tag : {ssi_parsed, '$3'}.
+BlockTransBlock -> open_tag blocktrans_keyword identifier close_tag Elements open_tag endblocktrans_keyword close_tag : {blocktrans, '$3', '$5'}.
+
TemplatetagTag -> open_tag templatetag_keyword Templatetag close_tag : {templatetag, '$3'}.
Templatetag -> openblock_keyword : '$1'.
@@ -339,6 +346,7 @@ Filter -> identifier : ['$1'].
Filter -> identifier ':' Literal : ['$1', '$3'].
Filter -> identifier ':' Variable : ['$1', '$3'].
+Literal -> '_' '(' string_literal ')' : {trans, '$3'}.
Literal -> string_literal : '$1'.
Literal -> number_literal : '$1'.
View
5 src/erlydtl_scanner.erl
@@ -102,7 +102,7 @@ scan([], Scanned, _, in_text) ->
"call", "with", "endwith",
- "trans", "noop"
+ "trans", "blocktrans", "endblocktrans", "noop"
],
Type = case lists:member(RevString, Keywords) of
true ->
@@ -252,6 +252,9 @@ scan(":" ++ T, Scanned, {Row, Column}, {_, Closer}) ->
scan("." ++ T, Scanned, {Row, Column}, {_, Closer}) ->
scan(T, [{'.', {Row, Column}} | Scanned], {Row, Column + 1}, {in_code, Closer});
+scan("_(" ++ T, Scanned, {Row, Column}, {in_code, Closer}) ->
+ scan(T, lists:reverse([{'_', {Row, Column}}, {'(', {Row, Column + 1}}], Scanned), {Row, Column + 2}, {in_code, Closer});
+
scan(" " ++ T, Scanned, {Row, Column}, {_, Closer}) ->
scan(T, Scanned, {Row, Column + 1}, {in_code, Closer});
View
34 tests/src/erlydtl_unittests.erl
@@ -922,21 +922,29 @@ tests() ->
<<"Hello {% trans \"Hi\" %}">>, [], <<"Hello Hi">>
},
{"trans functional reverse locale",
- <<"Hello {% trans \"Hi\" %}">>, [], none, [{locale, "reverse"}], <<"Hello iH">>
+ <<"Hello {% trans \"Hi\" %}">>, [], [], [{locale, "reverse"}], <<"Hello iH">>
},
{"trans literal at run-time",
- <<"Hello {% trans \"Hi\" %}">>, [], fun("Hi") -> "Konichiwa" end, [],
+ <<"Hello {% trans \"Hi\" %}">>, [], [{translation_fun, fun("Hi") -> "Konichiwa" end}], [],
<<"Hello Konichiwa">>},
{"trans variable at run-time",
- <<"Hello {% trans var1 %}">>, [{var1, "Hi"}], fun("Hi") -> "Konichiwa" end, [],
+ <<"Hello {% trans var1 %}">>, [{var1, "Hi"}], [{translation_fun, fun("Hi") -> "Konichiwa" end}], [],
<<"Hello Konichiwa">>},
{"trans literal at run-time: No-op",
- <<"Hello {% trans \"Hi\" noop %}">>, [], fun("Hi") -> "Konichiwa" end, [],
+ <<"Hello {% trans \"Hi\" noop %}">>, [], [{translation_fun, fun("Hi") -> "Konichiwa" end}], [],
<<"Hello Hi">>},
{"trans variable at run-time: No-op",
- <<"Hello {% trans var1 noop %}">>, [{var1, "Hi"}], fun("Hi") -> "Konichiwa" end, [],
+ <<"Hello {% trans var1 noop %}">>, [{var1, "Hi"}], [{translation_fun, fun("Hi") -> "Konichiwa" end}], [],
<<"Hello Hi">>}
]},
+ {"blocktrans",
+ [
+ {"blocktrans default locale",
+ <<"{% blocktrans foo %}Hello{% endblocktrans %}">>, [], <<"Hello">>},
+ {"blocktrans choose locale",
+ <<"{% blocktrans hello %}Hello, {{ name }}{% endblocktrans %}">>, [{name, "Mr. President"}], [{locale, "de"}],
+ [{blocktrans_locales, ["de"]}, {blocktrans_fun, fun(hello, "de") -> <<"Guten tag, {{ name }}">> end}], <<"Guten tag, Mr. President">>}
+ ]},
{"widthratio", [
{"Literals", <<"{% widthratio 5 10 100 %}">>, [], <<"50">>},
{"Rounds up", <<"{% widthratio a b 100 %}">>, [{a, 175}, {b, 200}], <<"88">>}
@@ -959,23 +967,23 @@ run_tests() ->
lists:foldl(fun
({Name, DTL, Vars, Output}, Acc) ->
process_unit_test(erlydtl:compile(DTL, erlydtl_running_test, []),
- Vars, none, Output, Acc, Group, Name);
- ({Name, DTL, Vars, Dictionary, Output}, Acc) ->
+ Vars, [], Output, Acc, Group, Name);
+ ({Name, DTL, Vars, RenderOpts, Output}, Acc) ->
process_unit_test(erlydtl:compile(DTL, erlydtl_running_test, []),
- Vars, Dictionary, Output, Acc, Group, Name);
- ({Name, DTL, Vars, Dictionary, CompilerOpts, Output}, Acc) ->
+ Vars, RenderOpts, Output, Acc, Group, Name);
+ ({Name, DTL, Vars, RenderOpts, CompilerOpts, Output}, Acc) ->
process_unit_test(erlydtl:compile(DTL, erlydtl_running_test, CompilerOpts),
- Vars, Dictionary, Output, Acc, Group, Name)
+ Vars, RenderOpts, Output, Acc, Group, Name)
end, GroupAcc, Assertions)
end, [], tests()),
io:format("Unit test failures: ~p~n", [lists:reverse(Failures)]).
-process_unit_test(CompiledTemplate, Vars, Dictionary, Output,Acc, Group, Name) ->
+process_unit_test(CompiledTemplate, Vars, RenderOpts, Output,Acc, Group, Name) ->
case CompiledTemplate of
{ok, _} ->
- {ok, IOList} = erlydtl_running_test:render(Vars, Dictionary),
- {ok, IOListBin} = erlydtl_running_test:render(vars_to_binary(Vars), Dictionary),
+ {ok, IOList} = erlydtl_running_test:render(Vars, RenderOpts),
+ {ok, IOListBin} = erlydtl_running_test:render(vars_to_binary(Vars), RenderOpts),
case {iolist_to_binary(IOList), iolist_to_binary(IOListBin)} of
{Output, Output} ->
Acc;
Please sign in to comment.
Something went wrong with that request. Please try again.