Browse Source

Merge remote-tracking branch 'seriyps/runtime-blocktrans' (#130)

Close #88.
Andreas Stenius 11 years ago
parent
commit
766e37ffca

+ 22 - 20
README.markdown

@@ -106,7 +106,7 @@ Options is a proplist possibly containing:
   trans "StringValue" %}` on templates).  See README_I18N.
   trans "StringValue" %}` on templates).  See README_I18N.
 
 
 * `blocktrans_fun` - A two-argument fun to use for translating
 * `blocktrans_fun` - A two-argument fun to use for translating
-  `blocktrans` blocks. This will be called once for each pair of
+  `blocktrans` blocks at compile-time. This will be called once for each pair of
   `blocktrans` block and locale specified in `blocktrans_locales`. The
   `blocktrans` block and locale specified in `blocktrans_locales`. The
   fun should take the form:
   fun should take the form:
 
 
@@ -198,40 +198,42 @@ IOList is the rendered template.
 Same as `render/1`, but with the following options:
 Same as `render/1`, but with the following options:
 
 
 * `translation_fun` - A fun/1 that will be used to translate strings
 * `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`
+  appearing inside `{% trans %}` and `{% blocktrans %}` tags. The simplest
+  TranslationFun would be `fun(Val) -> Val end`. Placeholders for
+  blocktrans variable interpolation should be wrapped to `{{` and `}}`.
 
 
 * `locale` - A string specifying the current locale, for use with the
 * `locale` - A string specifying the current locale, for use with the
   `blocktrans_fun` compile-time option.
   `blocktrans_fun` compile-time option.
 
 
-      my_compiled_template:translatable_strings() -> [String]
+```erlang
+my_compiled_template:translatable_strings() -> [String]
+```
+List of strings appearing in `{% trans %}` tags that can be
+overridden with a dictionary passed to `render/2`.
 
 
-  List of strings appearing in `{% trans %}` tags that can be
-  overridden with a dictionary passed to `render/2`.
+    my_compiled_template:translated_blocks() -> [String]
 
 
-      my_compiled_template:translated_blocks() -> [String]
+List of strings appearing in `{% blocktrans %}...{% endblocktrans %}`
+blocks; the translations (which can contain ErlyDTL code) are
+hard-coded into the module and appear at render-time. To get a list
+of translatable blocks before compile-time, use the provided
+`blocktrans_extractor` module.
 
 
-  List of strings appearing in `{% blocktrans %}...{% endblocktrans
-  %}` blocks; the translations (which can contain ErlyDTL code) are
-  hard-coded into the module and appear at render-time. To get a list
-  of translatable blocks before compile-time, use the provided
-  `blocktrans_extractor` module.
+    my_compiled_template:source() -> {FileName, CheckSum}
 
 
-      my_compiled_template:source() -> {FileName, CheckSum}
+Name and checksum of the original template file.
 
 
-  Name and checksum of the original template file.
+    my_compiled_template:dependencies() -> [{FileName, CheckSum}]
 
 
-      my_compiled_template:dependencies() -> [{FileName, CheckSum}]
-
-  List of names/checksums of templates included by the original
+List of names/checksums of templates included by the original
   template file. Useful for frameworks that recompile a template only
   template file. Useful for frameworks that recompile a template only
   when the template's dependencies change.
   when the template's dependencies change.
 
 
       my_compiled_template:variables() -> [Variable::atom()]
       my_compiled_template:variables() -> [Variable::atom()]
 
 
-  Sorted list of unique variables used in the template's body. The
-  list can be used for determining which variable bindings need to be
-  passed to the render/3 function.
+Sorted list of unique variables used in the template's body. The
+list can be used for determining which variable bindings need to be
+passed to the render/3 function.
 
 
 
 
 Differences from standard Django Template Language
 Differences from standard Django Template Language

+ 21 - 3
README_I18N

@@ -5,10 +5,26 @@ Erlydtl allows templates to use i18n features based on gettext. Standard po
 files can be used to generate i18ized templates. A template parser/po generator
 files can be used to generate i18ized templates. A template parser/po generator
 is also provided.
 is also provided.
 
 
+Translation done by `{%trans%}`, `{%blocktrans%}` tags and by wrapping variables
+in `_(...)` construction.
+
+Translation may be applied in compile time (translated strings are embedded in
+compiled template code) or in runtime (template will query gettext server during
+template rendering). 1'st is faster in terms of template rendering time, 2'nd is
+more flexible (you may update locales without any template recompilation).
+
+In order to apply compile-time translation, you must pass `blocktrans_fun` plus
+`blocktrans_locales` (this will translate `{%blocktrans%}`) and/or `locale` (this
+translates `{%trans%}` and `_("string literal")` values) to `erlydtl:compile/3`.
+Next, you should pass `locale` option to `my_compiled_template:render/2`.
+
+If you prefer runtime translation, just don't pass `blocktrans_fun` and/or `locale`
+compilation options and add `translation_fun` option to `my_compiled_template:render/2`.
+
     1.  In order to enable i18n you first, you'll need gettext library to be
     1.  In order to enable i18n you first, you'll need gettext library to be
         available on your lib_path. 
         available on your lib_path. 
 
 
-        Library can be downloaded from http://github.com/noss/erlang-gettext
+        Library can be downloaded from http://github.com/etnt/gettext
 
 
     2.  Then you'll need to add a parse target on your makefile (or the script
     2.  Then you'll need to add a parse target on your makefile (or the script
         used to trigger template reparsing) trans:
         used to trigger template reparsing) trans:
@@ -26,7 +42,8 @@ is also provided.
         directories where generator will search for template files including
         directories where generator will search for template files including
         trans tags.
         trans tags.
 
 
-    3.  Before template parsing gettext server must be running and it must be
+    3.  If you wish to translate templates at compile-time, gettext server must be
+        running before template parsing and it must be
         populated with the content of the po files. Consider adding this
         populated with the content of the po files. Consider adding this
         snipplet to the code before template parsing
         snipplet to the code before template parsing
 
 
@@ -44,4 +61,5 @@ is also provided.
     4.  Update strings. Edit po files on $(GETTEXT_DIR)/lang/default/$(LOCALE)/gettext.po 
     4.  Update strings. Edit po files on $(GETTEXT_DIR)/lang/default/$(LOCALE)/gettext.po 
         translating msgstr to the translated version of their corresponding msgstr.
         translating msgstr to the translated version of their corresponding msgstr.
 
 
-    5.  Generate localized templates providing locale compile option.
+    5.  Generate localized templates providing `locale` compile option or use runtime
+        translation via `translation_fun` rendering option.

+ 31 - 1
src/erlydtl_compiler.erl

@@ -1194,19 +1194,23 @@ empty_ast(TreeWalker) ->
     {{erl_syntax:list([]), #ast_info{}}, TreeWalker}.
     {{erl_syntax:list([]), #ast_info{}}, TreeWalker}.
 
 
 blocktrans_ast(ArgList, Contents, Context, TreeWalker) ->
 blocktrans_ast(ArgList, Contents, Context, TreeWalker) ->
+    %% add new scope using 'with' values
     {NewScope, {ArgInfo, TreeWalker1}} = lists:mapfoldl(fun
     {NewScope, {ArgInfo, TreeWalker1}} = lists:mapfoldl(fun
                                                             ({{identifier, _, LocalVarName}, Value}, {AstInfo1, TreeWalker1}) ->
                                                             ({{identifier, _, LocalVarName}, Value}, {AstInfo1, TreeWalker1}) ->
                                                                {{Ast, Info}, TreeWalker2} = value_ast(Value, false, false, Context, TreeWalker1),
                                                                {{Ast, Info}, TreeWalker2} = value_ast(Value, false, false, Context, TreeWalker1),
                                                                {{LocalVarName, Ast}, {merge_info(AstInfo1, Info), TreeWalker2}}
                                                                {{LocalVarName, Ast}, {merge_info(AstInfo1, Info), TreeWalker2}}
                                                        end, {#ast_info{}, TreeWalker}, ArgList),
                                                        end, {#ast_info{}, TreeWalker}, ArgList),
     NewContext = Context#dtl_context{ local_scopes = [NewScope|Context#dtl_context.local_scopes] },
     NewContext = Context#dtl_context{ local_scopes = [NewScope|Context#dtl_context.local_scopes] },
+    %% key for translation lookup
     SourceText = lists:flatten(erlydtl_unparser:unparse(Contents)),
     SourceText = lists:flatten(erlydtl_unparser:unparse(Contents)),
     {{DefaultAst, AstInfo}, TreeWalker2} = body_ast(Contents, NewContext, TreeWalker1),
     {{DefaultAst, AstInfo}, TreeWalker2} = body_ast(Contents, NewContext, TreeWalker1),
     MergedInfo = merge_info(AstInfo, ArgInfo),
     MergedInfo = merge_info(AstInfo, ArgInfo),
     case Context#dtl_context.blocktrans_fun of
     case Context#dtl_context.blocktrans_fun of
         none ->
         none ->
-            {{DefaultAst, MergedInfo}, TreeWalker2};
+            %% translate in runtime
+            blocktrans_runtime_ast({DefaultAst, MergedInfo}, TreeWalker2, SourceText, Contents, NewContext);
         BlockTransFun when is_function(BlockTransFun) ->
         BlockTransFun when is_function(BlockTransFun) ->
+            %% translate in compile-time
             {FinalAstInfo, FinalTreeWalker, Clauses} = lists:foldr(fun(Locale, {AstInfoAcc, ThisTreeWalker, ClauseAcc}) ->
             {FinalAstInfo, FinalTreeWalker, Clauses} = lists:foldr(fun(Locale, {AstInfoAcc, ThisTreeWalker, ClauseAcc}) ->
                                                                            case BlockTransFun(SourceText, Locale) of
                                                                            case BlockTransFun(SourceText, Locale) of
                                                                                default ->
                                                                                default ->
@@ -1223,6 +1227,32 @@ blocktrans_ast(ArgList, Contents, Context, TreeWalker) ->
             {{Ast, FinalAstInfo#ast_info{ translated_blocks = [SourceText] }}, FinalTreeWalker}
             {{Ast, FinalAstInfo#ast_info{ translated_blocks = [SourceText] }}, FinalTreeWalker}
     end.
     end.
 
 
+blocktrans_runtime_ast({DefaultAst, Info}, Walker, SourceText, Contents, Context) ->
+    %% Contents is flat - only strings and '{{var}}' allowed.
+    %% build sorted list (orddict) of pre-resolved variables to pass to runtime translation function
+    USortedVariables = lists:usort(fun({variable, {identifier, _, A}},
+                                       {variable, {identifier, _, B}}) ->
+                                           A =< B
+                                   end, [Var || {variable, _}=Var <- Contents]),
+    VarBuilder = fun({variable, {identifier, _, Name}}=Var, Walker1) ->
+                         {{Ast2, _InfoIgn}, Walker2}  = resolve_variable_ast(Var, Context, Walker1, false),
+                         KVAst = erl_syntax:tuple([erl_syntax:string(atom_to_list(Name)), Ast2]),
+                         {KVAst, Walker2}
+                 end,
+    {VarAsts, Walker2} = lists:mapfoldl(VarBuilder, Walker, USortedVariables),
+    VarListAst = erl_syntax:list(VarAsts),
+    RuntimeTransAst =  [erl_syntax:application(
+                          erl_syntax:atom(erlydtl_runtime),
+                          erl_syntax:atom(translate_block),
+                          [erl_syntax:string(SourceText),
+                           erl_syntax:variable("_TranslationFun"),
+                           VarListAst])],
+    Ast1 = erl_syntax:case_expr(erl_syntax:variable("_TranslationFun"),
+                                [erl_syntax:clause([erl_syntax:atom(none)], none, [DefaultAst]),
+                                 erl_syntax:clause([erl_syntax:underscore()], none,
+                                                   RuntimeTransAst)]),
+    {{Ast1, Info}, Walker2}.
+
 translated_ast({string_literal, _, String}, Context, TreeWalker) ->
 translated_ast({string_literal, _, String}, Context, TreeWalker) ->
     UnescapedStr = unescape_string_literal(String),
     UnescapedStr = unescape_string_literal(String),
     case call_extension(Context, translate_ast, [UnescapedStr, Context, TreeWalker]) of
     case call_extension(Context, translate_ast, [UnescapedStr, Context, TreeWalker]) of

+ 7 - 2
src/erlydtl_parser.yrl

@@ -113,6 +113,7 @@ Nonterminals
     SSITag
     SSITag
 
 
     BlockTransBlock
     BlockTransBlock
+    BlockTransContent
     TransTag    
     TransTag    
 
 
     TemplatetagTag
     TemplatetagTag
@@ -367,8 +368,12 @@ 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 Value close_tag : {ssi, '$3'}.
 SSITag -> open_tag ssi_keyword string_literal parsed_keyword close_tag : {ssi_parsed, '$3'}.
 SSITag -> open_tag ssi_keyword string_literal parsed_keyword close_tag : {ssi_parsed, '$3'}.
 
 
-BlockTransBlock -> open_tag blocktrans_keyword close_tag Elements open_tag endblocktrans_keyword close_tag : {blocktrans, [], '$4'}.
-BlockTransBlock -> open_tag blocktrans_keyword with_keyword Args close_tag Elements open_tag endblocktrans_keyword close_tag : {blocktrans, '$4', '$6'}.
+BlockTransBlock -> open_tag blocktrans_keyword close_tag BlockTransContent open_tag endblocktrans_keyword close_tag : {blocktrans, [], '$4'}.
+BlockTransBlock -> open_tag blocktrans_keyword with_keyword Args close_tag BlockTransContent open_tag endblocktrans_keyword close_tag : {blocktrans, '$4', '$6'}.
+BlockTransContent -> '$empty' : [].
+BlockTransContent -> BlockTransContent open_var identifier close_var : '$1' ++ [{variable, '$3'}].
+BlockTransContent -> BlockTransContent string : '$1' ++ ['$2'].
+%% TODO: {% plural %}
 
 
 TemplatetagTag -> open_tag templatetag_keyword Templatetag close_tag : {templatetag, '$3'}.
 TemplatetagTag -> open_tag templatetag_keyword Templatetag close_tag : {templatetag, '$3'}.
 
 

+ 46 - 0
src/erlydtl_runtime.erl

@@ -2,6 +2,8 @@
 
 
 -compile(export_all).
 -compile(export_all).
 
 
+-type translate_fun() :: fun((string() | binary()) -> string() | binary() | undefined).
+
 -define(IFCHANGED_CONTEXT_VARIABLE, erlydtl_ifchanged_context).
 -define(IFCHANGED_CONTEXT_VARIABLE, erlydtl_ifchanged_context).
 
 
 find_value(Key, Data, Options) when is_atom(Key), is_tuple(Data) ->
 find_value(Key, Data, Options) when is_atom(Key), is_tuple(Data) ->
@@ -108,6 +110,8 @@ regroup([Item|Rest], Attribute, [[{grouper, PrevGrouper}, {list, PrevList}]|Acc]
             regroup(Rest, Attribute, [[{grouper, Value}, {list, [Item]}], [{grouper, PrevGrouper}, {list, lists:reverse(PrevList)}]|Acc])
             regroup(Rest, Attribute, [[{grouper, Value}, {list, [Item]}], [{grouper, PrevGrouper}, {list, lists:reverse(PrevList)}]|Acc])
     end.
     end.
 
 
+-spec translate(Str, none | translate_fun(), Str) -> Str when
+      Str :: string() | binary().
 translate(_, none, Default) ->
 translate(_, none, Default) ->
     Default;
     Default;
 translate(String, TranslationFun, Default) when is_function(TranslationFun) ->
 translate(String, TranslationFun, Default) when is_function(TranslationFun) ->
@@ -118,6 +122,48 @@ translate(String, TranslationFun, Default) when is_function(TranslationFun) ->
         Str -> Str
         Str -> Str
     end.
     end.
 
 
+%% @doc Translate and interpolate 'blocktrans' content.
+%% Pre-requisites:
+%%  * `Variables' should be sorted
+%%  * Each interpolation variable should exist
+%%    (String="{{a}}", Variables=[{"b", "b-val"}] will fall)
+%%  * Orddict keys should be string(), not binary()
+-spec translate_block(string() | binary(), translate_fun(), orddict:orddict()) -> iodata().
+translate_block(String, TranslationFun, Variables) ->
+    TransString = case TranslationFun(String) of
+                      No when (undefined == No)
+                              orelse (<<"">> == No)
+                              orelse ("" == No) -> String;
+                      Str -> Str
+                  end,
+    try interpolate_variables(TransString, Variables)
+    catch _:_ ->
+            %% Fallback to default language in case of errors (like Djando does)
+            interpolate_variables(String, Variables)
+    end.
+
+interpolate_variables(Tpl, []) ->
+    Tpl;
+interpolate_variables(Tpl, Variables) ->
+    BTpl = iolist_to_binary(Tpl),
+    interpolate_variables1(BTpl, Variables).
+
+interpolate_variables1(Tpl, Vars) ->
+    %% pre-compile binary patterns?
+    case binary:split(Tpl, <<"{{">>) of
+        [NotFound] ->
+            [NotFound];
+        [Pre, Post] ->
+            case binary:split(Post, <<"}}">>) of
+                [_] -> throw({no_close_var, Post});
+                [Var, Post1] ->
+                    Var1 = string:strip(binary_to_list(Var)),
+                    Value = orddict:fetch(Var1, Vars),
+                    [Pre, Value | interpolate_variables1(Post1, Vars)]
+            end
+    end.
+
+
 are_equal(Arg1, Arg2) when Arg1 =:= Arg2 ->
 are_equal(Arg1, Arg2) when Arg1 =:= Arg2 ->
     true;
     true;
 are_equal(Arg1, Arg2) when is_binary(Arg1) ->
 are_equal(Arg1, Arg2) when is_binary(Arg1) ->

+ 15 - 0
src/i18n/sources_parser.erl

@@ -61,6 +61,8 @@ process_token(Fname, {block,{identifier,{_Line,_Col},_Identifier},Children}, Acc
 process_token(Fname, {trans,{string_literal,{Line,Col},String}}, Acc ) -> [{unescape(String), {Fname, Line, Col}} | Acc];
 process_token(Fname, {trans,{string_literal,{Line,Col},String}}, Acc ) -> [{unescape(String), {Fname, Line, Col}} | Acc];
 process_token(_Fname, {apply_filter, _Value, _Filter}, Acc) -> Acc;
 process_token(_Fname, {apply_filter, _Value, _Filter}, Acc) -> Acc;
 process_token(_Fname, {date, now, _Filter}, Acc) -> Acc;
 process_token(_Fname, {date, now, _Filter}, Acc) -> Acc;
+process_token(Fname, {blocktrans, Args, Contents}, Acc) -> [{lists:flatten(erlydtl_unparser:unparse(Contents)),
+                                                             guess_blocktrans_lc(Fname, Args, Contents)} | Acc];
 process_token(Fname, {_Instr, _Cond, Children}, Acc) -> process_ast(Fname, Children, Acc);
 process_token(Fname, {_Instr, _Cond, Children}, Acc) -> process_ast(Fname, Children, Acc);
 process_token(Fname, {_Instr, _Cond, Children, Children2}, Acc) ->
 process_token(Fname, {_Instr, _Cond, Children, Children2}, Acc) ->
     AccModified = process_ast(Fname, Children, Acc),
     AccModified = process_ast(Fname, Children, Acc),
@@ -68,3 +70,16 @@ process_token(Fname, {_Instr, _Cond, Children, Children2}, Acc) ->
 process_token(_,_AST,Acc) -> Acc.
 process_token(_,_AST,Acc) -> Acc.
 
 
 unescape(String) ->string:sub_string(String, 2, string:len(String) -1).
 unescape(String) ->string:sub_string(String, 2, string:len(String) -1).
+
+%% hack to guess ~position of blocktrans
+guess_blocktrans_lc(Fname, [{{identifier, {L, C}, _}, _} | _], _) ->
+    %% guess by 1'st with
+    {Fname, L, C - length("blocktrans with ")};
+guess_blocktrans_lc(Fname, _, [{string, {L, C}, _} | _]) ->
+    %% guess by 1'st string
+    {Fname, L, C - length("blocktrans %}")};
+guess_blocktrans_lc(Fname, _, [{variable, {identifier, {L, C}, _}} | _]) ->
+    %% guess by 1'st {{...}}
+    {Fname, L, C - length("blocktrans %}")};
+guess_blocktrans_lc(Fname, _, _) ->
+    {Fname, -1, -1}.

+ 12 - 1
tests/src/erlydtl_unittests.erl

@@ -1190,7 +1190,18 @@ tests() ->
         <<"{% blocktrans %}Hello, {{ name }}{% endblocktrans %}">>, [{name, "Mr. President"}], [{locale, "de"}],
         <<"{% blocktrans %}Hello, {{ name }}{% endblocktrans %}">>, [{name, "Mr. President"}], [{locale, "de"}],
         [{blocktrans_locales, ["de"]}, {blocktrans_fun, fun("Hello, {{ name }}", "de") -> <<"Guten tag, {{ name }}">> end}], <<"Guten tag, Mr. President">>},
         [{blocktrans_locales, ["de"]}, {blocktrans_fun, fun("Hello, {{ name }}", "de") -> <<"Guten tag, {{ name }}">> end}], <<"Guten tag, Mr. President">>},
        {"blocktrans with args",
        {"blocktrans with args",
-        <<"{% blocktrans with var1=foo %}{{ var1 }}{% endblocktrans %}">>, [{foo, "Hello"}], <<"Hello">>}
+        <<"{% blocktrans with var1=foo %}{{ var1 }}{% endblocktrans %}">>, [{foo, "Hello"}], <<"Hello">>},
+       {"blocktrans blocks in content not allowed",
+        <<"{% blocktrans %}Hello{%if name%}, {{ name }}{%endif%}!{% endblocktrans %}">>, [],
+        {error, [error_info([{{1, 24}, erlydtl_parser, ["syntax error before: ",["\"if\""]]}])], []}},
+       {"blocktrans nested variables not allowed",
+        <<"{% blocktrans %}Hello, {{ user.name }}!{% endblocktrans %}">>, [],
+        {error, [error_info([{{1,31}, erlydtl_parser, ["syntax error before: ","'.'"]}])], []}},
+       {"blocktrans runtime",
+        <<"{% blocktrans with v1=foo%}Hello, {{ name }}! See {{v1}}.{%endblocktrans%}">>,
+        [{name, "Mr. President"}, {foo, <<"rubber-duck">>}],
+        [{translation_fun, fun("Hello, {{ name }}! See {{ v1 }}.") -> <<"Guten tag, {{name}}! Sehen {{    v1   }}.">> end}],
+        [], <<"Guten tag, Mr. President! Sehen rubber-duck.">>}
       ]},
       ]},
      {"verbatim", [
      {"verbatim", [
                    {"Plain verbatim",
                    {"Plain verbatim",