Просмотр исходного кода

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

Close #88.
Andreas Stenius 11 лет назад
Родитель
Сommit
766e37ffca

+ 22 - 20
README.markdown

@@ -106,7 +106,7 @@ Options is a proplist possibly containing:
   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` blocks at compile-time. This will be called once for each pair of
   `blocktrans` block and locale specified in `blocktrans_locales`. The
   fun should take the form:
 
@@ -198,40 +198,42 @@ IOList is the rendered template.
 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`
+  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
   `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
   when the template's dependencies change.
 
       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

+ 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
 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
         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
         used to trigger template reparsing) trans:
@@ -26,7 +42,8 @@ is also provided.
         directories where generator will search for template files including
         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
         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 
         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}.
 
 blocktrans_ast(ArgList, Contents, Context, TreeWalker) ->
+    %% add new scope using 'with' values
     {NewScope, {ArgInfo, TreeWalker1}} = lists:mapfoldl(fun
                                                             ({{identifier, _, LocalVarName}, Value}, {AstInfo1, TreeWalker1}) ->
                                                                {{Ast, Info}, TreeWalker2} = value_ast(Value, false, false, Context, TreeWalker1),
                                                                {{LocalVarName, Ast}, {merge_info(AstInfo1, Info), TreeWalker2}}
                                                        end, {#ast_info{}, TreeWalker}, ArgList),
     NewContext = Context#dtl_context{ local_scopes = [NewScope|Context#dtl_context.local_scopes] },
+    %% key for translation lookup
     SourceText = lists:flatten(erlydtl_unparser:unparse(Contents)),
     {{DefaultAst, AstInfo}, TreeWalker2} = body_ast(Contents, NewContext, TreeWalker1),
     MergedInfo = merge_info(AstInfo, ArgInfo),
     case Context#dtl_context.blocktrans_fun of
         none ->
-            {{DefaultAst, MergedInfo}, TreeWalker2};
+            %% translate in runtime
+            blocktrans_runtime_ast({DefaultAst, MergedInfo}, TreeWalker2, SourceText, Contents, NewContext);
         BlockTransFun when is_function(BlockTransFun) ->
+            %% translate in compile-time
             {FinalAstInfo, FinalTreeWalker, Clauses} = lists:foldr(fun(Locale, {AstInfoAcc, ThisTreeWalker, ClauseAcc}) ->
                                                                            case BlockTransFun(SourceText, Locale) of
                                                                                default ->
@@ -1223,6 +1227,32 @@ blocktrans_ast(ArgList, Contents, Context, TreeWalker) ->
             {{Ast, FinalAstInfo#ast_info{ translated_blocks = [SourceText] }}, FinalTreeWalker}
     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) ->
     UnescapedStr = unescape_string_literal(String),
     case call_extension(Context, translate_ast, [UnescapedStr, Context, TreeWalker]) of

+ 7 - 2
src/erlydtl_parser.yrl

@@ -113,6 +113,7 @@ Nonterminals
     SSITag
 
     BlockTransBlock
+    BlockTransContent
     TransTag    
 
     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 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'}.
 

+ 46 - 0
src/erlydtl_runtime.erl

@@ -2,6 +2,8 @@
 
 -compile(export_all).
 
+-type translate_fun() :: fun((string() | binary()) -> string() | binary() | undefined).
+
 -define(IFCHANGED_CONTEXT_VARIABLE, erlydtl_ifchanged_context).
 
 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])
     end.
 
+-spec translate(Str, none | translate_fun(), Str) -> Str when
+      Str :: string() | binary().
 translate(_, none, Default) ->
     Default;
 translate(String, TranslationFun, Default) when is_function(TranslationFun) ->
@@ -118,6 +122,48 @@ translate(String, TranslationFun, Default) when is_function(TranslationFun) ->
         Str -> Str
     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 ->
     true;
 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, {apply_filter, _Value, _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, Children2}, Acc) ->
     AccModified = process_ast(Fname, Children, Acc),
@@ -68,3 +70,16 @@ process_token(Fname, {_Instr, _Cond, Children, Children2}, Acc) ->
 process_token(_,_AST,Acc) -> Acc.
 
 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_locales, ["de"]}, {blocktrans_fun, fun("Hello, {{ name }}", "de") -> <<"Guten tag, {{ name }}">> end}], <<"Guten tag, Mr. President">>},
        {"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", [
                    {"Plain verbatim",