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

support count and plural forms in blocktrans blocks (#131)

open question: we ought to support multiple plural tags
  to support languages that have more than two variations
  dependent on the count.
Andreas Stenius 11 лет назад
Родитель
Сommit
cc35ef898c

+ 7 - 0
NEWS.md

@@ -12,3 +12,10 @@ Standards](http://www.gnu.org/prep/standards/html_node/NEWS-File.html#NEWS-File)
   `default_vars`.
 
 * Support for translation contexts (#131)
+
+  `context` is now a reserved keyword.
+
+* Support for plural forms in `blocktrans` blocks (#131)
+
+  As a side effect of the this, `count` and `plural` are now reserved
+  keywords (the latter only as the tag name).

+ 13 - 2
README.markdown

@@ -322,8 +322,8 @@ Same as `render/1`, but with the following options:
   with a translation context in a tuple:
 
   ```erlang
-  fun (Val, {Locale, Context}) -> Translated_Val;
-      (Val, Locale) -> Translated_Val
+  fun (Val|{Val, {Plural_Val, Count}}, Locale|{Locale, Context}) ->
+      Translated_Val
   end
   ```
 
@@ -338,6 +338,17 @@ Same as `render/1`, but with the following options:
   {% endblocktrans %}
   ```
 
+  The plural form is present when using `count` and `plural` in a
+  `blocktrans` block:
+
+  ```django
+  {% blocktrans count counter=var|length %}
+    There is {{ counter }} element in the list.
+  {% plural %}
+    There are {{ counter }} elements in the list.
+  {% endblocktrans %}
+  ```
+
 * `lists_0_based` - If the compile option `lists_0_based` was set to
   `defer`, pass this option (or set it to true, `{lists_0_based,
   true}`) to get 0-based list indexing when rendering the

+ 45 - 29
src/erlydtl_beam_compiler.erl

@@ -565,8 +565,8 @@ body_ast(DjangoParseTree, BodyScope, TreeWalker) ->
                             {Contents, empty_scope()}
                     end,
                 body_ast(Block, BlockScope, TW);
-            ({'blocktrans', Args, Contents}, TW) ->
-                blocktrans_ast(Args, Contents, TW);
+            ({'blocktrans', Args, Contents, PluralContents}, TW) ->
+                blocktrans_ast(Args, Contents, PluralContents, TW);
             ({'call', {identifier, _, Name}}, TW) ->
                 call_ast(Name, TW);
             ({'call', {identifier, _, Name}, With}, TW) ->
@@ -746,9 +746,12 @@ empty_ast(TreeWalker) ->
 
 %%% Note: Context here refers to the translation context, not the #dtl_context{} record
 
-blocktrans_ast(Args, Contents, TreeWalker) ->
-    %% get args and context
-    ArgList = proplists:get_value(args, Args, []),
+blocktrans_ast(Args, Contents, PluralContents, TreeWalker) ->
+    %% get args, count and context
+    ArgList = [{Name, Value}
+               || {{identifier, _, Name}, Value}
+                      <- proplists:get_value(args, Args, [])],
+    Count = proplists:get_value(count, Args),
     Context = case proplists:get_value(context, Args) of
                   undefined -> undefined;
                   {string_literal, _, S} ->
@@ -758,12 +761,16 @@ blocktrans_ast(Args, Contents, TreeWalker) ->
     %% add new scope using 'with' values
     {NewScope, {ArgInfo, TreeWalker1}} =
         lists:mapfoldl(
-          fun ({{identifier, _, LocalVarName}, Value}, {AstInfoAcc, TreeWalkerAcc}) ->
+          fun ({LocalVarName, Value}, {AstInfoAcc, TreeWalkerAcc}) ->
                   {{Ast, Info}, TW} = value_ast(Value, false, false, TreeWalkerAcc),
                   {{LocalVarName, Ast}, {merge_info(AstInfoAcc, Info), TW}}
           end,
           {#ast_info{}, TreeWalker},
-          ArgList),
+          if is_tuple(Count) ->
+                  [Count|ArgList];
+             true ->
+                  ArgList
+          end),
 
     TreeWalker2 = push_scope(NewScope, TreeWalker1),
 
@@ -775,11 +782,11 @@ blocktrans_ast(Args, Contents, TreeWalker) ->
     #dtl_context{
       trans_fun = TFun,
       trans_locales = TLocales } = TreeWalker3#treewalker.context,
-    if TFun =:= none ->
+    if TFun =:= none; PluralContents =/= undefined ->
             %% translate in runtime
             {FinalAst, FinalTW} = blocktrans_runtime_ast(
-                                    {DefaultAst, MergedInfo}, SourceText,
-                                    Contents, Context, TreeWalker3),
+                                    {DefaultAst, MergedInfo}, SourceText, Contents, Context,
+                                    plural_contents(PluralContents, Count, TreeWalker3)),
             {FinalAst, restore_scope(TreeWalker1, FinalTW)};
        is_function(TFun, 2) ->
             %% translate in compile-time
@@ -802,13 +809,14 @@ blocktrans_ast(Args, Contents, TreeWalker) ->
              restore_scope(TreeWalker1, FinalTreeWalker)}
     end.
 
-blocktrans_runtime_ast({DefaultAst, Info}, SourceText, Contents, Context, TreeWalker) ->
+blocktrans_runtime_ast({DefaultAst, Info}, SourceText, Contents, Context, {Plural, TreeWalker}) ->
     %% 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]),
+                                   end, [Var || {variable, _}=Var
+                                                    <- Contents ++ maybe_plural_contents(Plural)]),
     VarBuilder = fun({variable, {identifier, _, Name}}=Var, TW) ->
                          {{VarAst, _VarInfo}, VarTW}  = resolve_variable_ast(Var, false, TW),
                          {?Q("{_@name, _@VarAst}", [{name, merl:term(atom_to_list(Name))}]), VarTW}
@@ -817,28 +825,36 @@ blocktrans_runtime_ast({DefaultAst, Info}, SourceText, Contents, Context, TreeWa
     VarListAst = erl_syntax:list(VarAsts),
     BlockTransAst = ?Q(["begin",
                         "  case erlydtl_runtime:translate_block(",
-                        "         _@SourceText@, _@locale,",
+                        "         _@phrase, _@locale,",
                         "         _@VarListAst, _TranslationFun) of",
                         "    default -> _@DefaultAst;",
                         "    Text -> Text",
                         "  end",
                         "end"],
-                      [{locale, phrase_locale_ast(Context)}]),
-    {{BlockTransAst, Info}, TreeWalker1}.
-
-%% extract_phrase({{string_literal, _, String}, {string_literal, _, Plural}}, TreeWalker) ->
-%%     {compiletime, {unescape_string_literal(String), unescape_string_literal(Plural)}, TreeWalker};
-%% extract_phrase({string_literal, _, String}, TreeWalker) ->
-%%     {compiletime, unescape_string_literal(String), TreeWalker};
-%% extract_phrase({Value, {string_literal, _, Plural}}, TreeWalker) ->
-%%     {{ValueAst, ValueInfo}, TreeWalker1} = value_ast(Value, true, false, TreeWalker),
-%%     ContextAst = string_ast(unescape_string_literal(Plural), TreeWalker1),
-%%     {runtime, {{erl_syntax:tuple([ValueAst, ContextAst]), ValueInfo}, TreeWalker1}};
-%% extract_phrase(Value, TreeWalker) ->
-%%     {runtime, value_ast(Value, true, false, TreeWalker)}.
-
-%% phrase_ast({Text, _}, TreeWalker) -> string_ast(Text, TreeWalker);
-%% phrase_ast(Text, TreeWalker) -> string_ast(Text, TreeWalker).
+                       [{phrase, phrase_ast(SourceText, Plural)},
+                        {locale, phrase_locale_ast(Context)}]),
+    {{BlockTransAst, merge_count_info(Info, Plural)}, TreeWalker1}.
+
+maybe_plural_contents(undefined) -> [];
+maybe_plural_contents({Contents, _}) -> Contents.
+
+merge_count_info(Info, undefined) -> Info;
+merge_count_info(Info, {_Contents, {_CountAst, CountInfo}}) ->
+    merge_info(Info, CountInfo).
+
+plural_contents(undefined, _, TreeWalker) -> {undefined, TreeWalker};
+plural_contents(Contents, {_CountVarName, Value}, TreeWalker) ->
+    {CountAst, TW} = value_ast(Value, false, false, TreeWalker),
+    {{Contents, CountAst}, TW}.
+
+phrase_ast(Text, undefined) -> merl:term(Text);
+phrase_ast(Text, {Contents, {CountAst, _CountInfo}}) ->
+    erl_syntax:tuple(
+      [merl:term(Text),
+       erl_syntax:tuple(
+         [merl:term(erlydtl_unparser:unparse(Contents)),
+          CountAst])
+      ]).
 
 phrase_locale_ast(undefined) -> merl:var('_CurrentLocale');
 phrase_locale_ast(Context) -> erl_syntax:tuple([merl:var('_CurrentLocale'), merl:term(Context)]).

+ 18 - 7
src/erlydtl_parser.yrl

@@ -103,6 +103,7 @@ Nonterminals
 
     CustomTag
     CustomArgs
+    Arg
     Args
 
     RegroupTag
@@ -115,7 +116,9 @@ Nonterminals
     BlockTransBraced
     EndBlockTransBraced
     BlockTransArgs
-    BlockTransContent
+    BlockTransContents
+
+    PluralTag
 
     TransTag
     TransArgs
@@ -151,6 +154,7 @@ Terminals
     comment_tag
     comment_keyword
     context_keyword
+    count_keyword
     cycle_keyword
     elif_keyword
     else_keyword
@@ -190,6 +194,7 @@ Terminals
     open_tag
     open_var
     parsed_keyword
+    plural_keyword
     regroup_keyword
     reversed_keyword
     spaceless_keyword
@@ -389,18 +394,21 @@ 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 -> BlockTransBraced BlockTransContent EndBlockTransBraced : {blocktrans, '$1', '$2'}.
+BlockTransBlock -> BlockTransBraced BlockTransContents EndBlockTransBraced : {blocktrans, '$1', '$2', undefined}.
+BlockTransBlock -> BlockTransBraced BlockTransContents PluralTag BlockTransContents EndBlockTransBraced : {blocktrans, '$1', '$2', '$4'}.
 BlockTransBraced -> open_tag blocktrans_keyword BlockTransArgs close_tag : '$3'.
 EndBlockTransBraced -> open_tag endblocktrans_keyword close_tag.
 
 BlockTransArgs -> '$empty' : [].
+BlockTransArgs -> count_keyword Arg BlockTransArgs : [{count, '$2'}|'$3'].
 BlockTransArgs -> with_keyword Args BlockTransArgs : [{args, '$2'}|'$2'].
 BlockTransArgs -> context_keyword string_literal BlockTransArgs : [{context, '$2'}|'$3'].
 
-BlockTransContent -> '$empty' : [].
-BlockTransContent -> open_var identifier close_var BlockTransContent : [{variable, '$2'}|'$4'].
-BlockTransContent -> string BlockTransContent : ['$1'|'$2'].
-%% TODO: {% plural %}
+BlockTransContents -> '$empty' : [].
+BlockTransContents -> open_var identifier close_var BlockTransContents : [{variable, '$2'}|'$4'].
+BlockTransContents -> string BlockTransContents : ['$1'|'$2'].
+
+PluralTag -> open_tag plural_keyword close_tag.
 
 TemplatetagTag -> open_tag templatetag_keyword Templatetag close_tag : {templatetag, '$3'}.
 
@@ -436,7 +444,10 @@ CustomArgs -> identifier '=' Value CustomArgs : [{'$1', '$3'}|'$4'].
 CustomArgs -> Value CustomArgs : ['$1'|'$2'].
 
 Args -> '$empty' : [].
-Args -> Args identifier '=' Value : '$1' ++ [{'$2', '$4'}].
+Args -> Arg Args : ['$1'|'$2'].
+
+Arg -> identifier '=' Value : {'$1', '$3'}.
+%% Arg -> identifier : {'$1', true}.
 
 CallTag -> open_tag call_keyword identifier close_tag : {call, '$3'}.
 CallWithTag -> open_tag call_keyword identifier with_keyword Value close_tag : {call, '$3', '$5'}.

+ 3 - 1
src/erlydtl_scanner.erl

@@ -36,7 +36,7 @@
 %%%-------------------------------------------------------------------
 -module(erlydtl_scanner).
 
-%% This file was generated 2014-04-10 04:34:52 UTC by slex 0.2.1.
+%% This file was generated 2014-04-10 14:54:46 UTC by slex 0.2.1.
 %% http://github.com/erlydtl/slex
 -slex_source(["src/erlydtl_scanner.slex"]).
 
@@ -87,6 +87,7 @@ is_keyword(any, "as") -> true;
 is_keyword(any, "by") -> true;
 is_keyword(any, "with") -> true;
 is_keyword(any, "from") -> true;
+is_keyword(any, "count") -> true;
 is_keyword(any, "context") -> true;
 is_keyword(close, "only") -> true;
 is_keyword(close, "parsed") -> true;
@@ -139,6 +140,7 @@ is_keyword(open, "trans") -> true;
 is_keyword(open, "blocktrans") -> true;
 is_keyword(open, "endblocktrans") -> true;
 is_keyword(open, "load") -> true;
+is_keyword(open, "plural") -> true;
 is_keyword(_, _) -> false.
 
 format_error({illegal_char, C}) ->

+ 2 - 0
src/erlydtl_scanner.slex

@@ -309,6 +309,7 @@ form \
   is_keyword(any, "by") -> true; \
   is_keyword(any, "with") -> true; \
   is_keyword(any, "from") -> true; \
+  is_keyword(any, "count") -> true; \
   is_keyword(any, "context") -> true; \
   \
   is_keyword(close, "only") -> true; \
@@ -363,6 +364,7 @@ form \
   is_keyword(open, "blocktrans") -> true; \
   is_keyword(open, "endblocktrans") -> true; \
   is_keyword(open, "load") -> true; \
+  is_keyword(open, "plural") -> true; \
   is_keyword(_, _) -> false \
 end.
 

+ 23 - 4
src/erlydtl_unparser.erl

@@ -12,10 +12,14 @@ unparse([{'autoescape', OnOrOff, Contents}|Rest], Acc) ->
     unparse(Rest, [["{% autoescape ", unparse_identifier(OnOrOff), " %}", unparse(Contents), "{% endautoescape %}"]|Acc]);
 unparse([{'block', Identifier, Contents}|Rest], Acc) ->
     unparse(Rest, [["{% block ", unparse_identifier(Identifier), " %}", unparse(Contents), "{% endblock %}"]|Acc]);
-unparse([{'blocktrans', [], Contents}|Rest], Acc) ->
-    unparse(Rest, [["{% blocktrans %}", unparse(Contents), "{% endblocktrans %}"]|Acc]);
-unparse([{'blocktrans', Args, Contents}|Rest], Acc) ->
-    unparse(Rest, [["{% blocktrans ", unparse_args(Args), " %}", unparse(Contents), "{% endblocktrans %}"]|Acc]);
+unparse([{'blocktrans', Args, Contents, undefined}|Rest], Acc) ->
+    unparse(Rest, [["{% blocktrans ", unparse_blocktrans_args(Args), "%}", unparse(Contents), "{% endblocktrans %}"]|Acc]);
+unparse([{'blocktrans', Args, Contents, PluralContents}|Rest], Acc) ->
+    unparse(Rest, [["{% blocktrans ", unparse_args(Args), " %}",
+                    unparse(Contents),
+                    "{% plural %}",
+                    unparse(PluralContents),
+                    "{% endblocktrans %}"]|Acc]);
 unparse([{'call', Identifier}|Rest], Acc) ->
     unparse(Rest, [["{% call ", unparse_identifier(Identifier), " %}"]|Acc]);
 unparse([{'call', Identifier, With}|Rest], Acc) ->
@@ -195,3 +199,18 @@ unparse_cycle_compat_names([{identifier, _, Name}], Acc) ->
     unparse_cycle_compat_names([], [atom_to_list(Name)|Acc]);
 unparse_cycle_compat_names([{identifier, _, Name}|Rest], Acc) ->
     unparse_cycle_compat_names(Rest, lists:reverse([atom_to_list(Name), ", "], Acc)).
+
+unparse_blocktrans_args(Args) ->
+    unparse_blocktrans_args(Args, []).
+
+unparse_blocktrans_args([{args, WithArgs}|Args], Acc) ->
+    unparse_blocktrans_args(
+      Args, [["with ", unparse_args(WithArgs)]|Acc]);
+unparse_blocktrans_args([{count, Count}|Args], Acc) ->
+    unparse_blocktrans_args(
+      Args, [["count ", unparse_args([Count])]|Acc]);
+unparse_blocktrans_args([{context, Context}|Args], Acc) ->
+    unparse_blocktrans_args(
+      Args, [["context ", unparse_value(Context)]|Acc]);
+unparse_blocktrans_args([], Acc) ->
+    lists:reverse(Acc).

+ 1 - 1
src/i18n/sources_parser.erl

@@ -150,7 +150,7 @@ process_token(Fname, {trans,{string_literal,{Line,Col},String}}, #state{acc=Acc,
     St#state{acc=[Phrase | Acc], translators_comment=undefined};
 process_token(_Fname, {apply_filter, _Value, _Filter}, St) -> St;
 process_token(_Fname, {date, now, _Filter}, St) -> St;
-process_token(Fname, {blocktrans, Args, Contents}, #state{acc=Acc, translators_comment=Comment}=St) ->
+process_token(Fname, {blocktrans, Args, Contents, _PluralContents}, #state{acc=Acc, translators_comment=Comment}=St) ->
     {Fname, Line, Col} = guess_blocktrans_lc(Fname, Args, Contents),
     Phrase = #phrase{msgid=erlydtl_unparser:unparse(Contents),
                      comment=Comment,

+ 13 - 1
test/erlydtl_test_defs.erl

@@ -1354,7 +1354,19 @@ all_test_defs() ->
                            fun ("translate this", {"foo", "bar"}) ->
                                    "got it"
                            end}],
-        <<"got it">>}
+        <<"got it">>},
+       {"blocktrans plural",
+        <<"{% blocktrans count foo=bar %}",
+          "There is just one foo..",
+          "{% plural %}",
+          "There are many foo's..",
+          "{% endblocktrans %}">>,
+        [{bar, 2}], [{locale, "baz"},
+                     {translation_fun,
+                      fun ({"There is just one foo..", {"There are many foo's..", 2}}, "baz") ->
+                              "ok"
+                      end}],
+        <<"ok">>}
       ]},
      {"verbatim",
       [{"Plain verbatim",