Browse Source

support translation context in `blocktrans` blocks (#131)

Andreas Stenius 11 years ago
parent
commit
e135b32ae9
6 changed files with 134 additions and 86 deletions
  1. 2 0
      NEWS.md
  2. 6 1
      README.markdown
  3. 75 60
      src/erlydtl_beam_compiler.erl
  4. 14 4
      src/erlydtl_parser.yrl
  5. 21 20
      src/erlydtl_runtime.erl
  6. 16 1
      test/erlydtl_test_defs.erl

+ 2 - 0
NEWS.md

@@ -10,3 +10,5 @@ Standards](http://www.gnu.org/prep/standards/html_node/NEWS-File.html#NEWS-File)
 
 * The `vars` compile time option has been deprecated in favor of
   `default_vars`.
+
+* Support for translation contexts (#131)

+ 6 - 1
README.markdown

@@ -106,9 +106,14 @@ Options is a proplist possibly containing:
   the form:
 
   ```erlang
-  Fun(Block::string(), Locale::string()) -> <<"ErlyDTL code">>::binary() | default
+  fun (Block::string(), Locale|{Locale, Context}) ->
+      <<"ErlyDTL code">>::binary() | default
+    when Locale::string(), Context::string().
   ```
 
+  See description of the `translation_fun` render option for more
+  details on the translation `context`.
+
 * `blocktrans_locales` - A list of locales to be passed to
   `blocktrans_fun`.  Defaults to [].
 

+ 75 - 60
src/erlydtl_beam_compiler.erl

@@ -746,7 +746,15 @@ empty_ast(TreeWalker) ->
 
 %%% Note: Context here refers to the translation context, not the #dtl_context{} record
 
-blocktrans_ast(ArgList, Contents, TreeWalker) ->
+blocktrans_ast(Args, Contents, TreeWalker) ->
+    %% get args and context
+    ArgList = proplists:get_value(args, Args, []),
+    Context = case proplists:get_value(context, Args) of
+                  undefined -> undefined;
+                  {string_literal, _, S} ->
+                      unescape_string_literal(S)
+              end,
+
     %% add new scope using 'with' values
     {NewScope, {ArgInfo, TreeWalker1}} =
         lists:mapfoldl(
@@ -760,24 +768,25 @@ blocktrans_ast(ArgList, Contents, TreeWalker) ->
     TreeWalker2 = push_scope(NewScope, TreeWalker1),
 
     %% key for translation lookup
-    SourceText = lists:flatten(erlydtl_unparser:unparse(Contents)),
+    SourceText = erlydtl_unparser:unparse(Contents),
     {{DefaultAst, AstInfo}, TreeWalker3} = body_ast(Contents, TreeWalker2),
     MergedInfo = merge_info(AstInfo, ArgInfo),
 
-    Context = TreeWalker3#treewalker.context,
-    case Context#dtl_context.trans_fun of
-        none ->
+    #dtl_context{
+      trans_fun = TFun,
+      trans_locales = TLocales } = TreeWalker3#treewalker.context,
+    if TFun =:= none ->
             %% translate in runtime
             {FinalAst, FinalTW} = blocktrans_runtime_ast(
-                                    {DefaultAst, MergedInfo},
-                                    SourceText, Contents, TreeWalker3),
+                                    {DefaultAst, MergedInfo}, SourceText,
+                                    Contents, Context, TreeWalker3),
             {FinalAst, restore_scope(TreeWalker1, FinalTW)};
-        BlockTransFun when is_function(BlockTransFun) ->
+       is_function(TFun, 2) ->
             %% translate in compile-time
             {FinalAstInfo, FinalTreeWalker, Clauses} =
                 lists:foldr(
                   fun (Locale, {AstInfoAcc, TreeWalkerAcc, ClauseAcc}) ->
-                          case BlockTransFun(SourceText, Locale) of
+                          case TFun(SourceText, phrase_locale(Locale, Context)) of
                               default ->
                                   {AstInfoAcc, TreeWalkerAcc, ClauseAcc};
                               Body ->
@@ -787,14 +796,13 @@ blocktrans_ast(ArgList, Contents, TreeWalker) ->
                                    [?Q("_@Locale@ -> _@BodyAst")|ClauseAcc]}
                           end
                   end,
-                  {MergedInfo, TreeWalker2, []},
-                  Context#dtl_context.trans_locales),
+                  {MergedInfo, TreeWalker3, []}, TLocales),
             FinalAst = ?Q("case _CurrentLocale of _@_Clauses -> _; _ -> _@DefaultAst end"),
             {{FinalAst, FinalAstInfo#ast_info{ translated_blocks = [SourceText] }},
              restore_scope(TreeWalker1, FinalTreeWalker)}
     end.
 
-blocktrans_runtime_ast({DefaultAst, Info}, SourceText, Contents, TreeWalker) ->
+blocktrans_runtime_ast({DefaultAst, Info}, SourceText, Contents, Context, 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}},
@@ -807,36 +815,60 @@ blocktrans_runtime_ast({DefaultAst, Info}, SourceText, Contents, TreeWalker) ->
                  end,
     {VarAsts, TreeWalker1} = lists:mapfoldl(VarBuilder, TreeWalker, USortedVariables),
     VarListAst = erl_syntax:list(VarAsts),
-    BlockTransAst = ?Q(["if _TranslationFun =:= none -> _@DefaultAst;",
-                        "  true -> erlydtl_runtime:translate_block(",
-                        "    _@SourceText@, _TranslationFun, _@VarListAst)",
-                        "end"]),
+    BlockTransAst = ?Q(["begin",
+                        "  case erlydtl_runtime:translate_block(",
+                        "         _@SourceText@, _@locale,",
+                        "         _@VarListAst, _TranslationFun) of",
+                        "    default -> _@DefaultAst;",
+                        "    Text -> Text",
+                        "  end",
+                        "end"],
+                      [{locale, phrase_locale_ast(Context)}]),
     {{BlockTransAst, Info}, TreeWalker1}.
 
-translated_ast(Phrase, TreeWalker) ->
-    translated_ast(Phrase, undefined, TreeWalker).
-
-translated_ast(Phrase, {string_literal, _, Context}, TreeWalker) ->
-    translated_ast(Phrase, unescape_string_literal(Context), TreeWalker);
-translated_ast(Phrase, Context, TreeWalker) ->
-    case extract_phrase(Phrase, TreeWalker) of
-        {compiletime, Phrase1, TreeWalker1} ->
-            case call_extension(TreeWalker, translate_ast, [Phrase1, Context, TreeWalker1]) of
-                undefined ->
-                    case TreeWalker#treewalker.context#dtl_context.trans_fun of
-                        none -> runtime_trans_ast(Phrase1, Context, TreeWalker1);
-                        _ -> compiletime_trans_ast(Phrase1, Context, TreeWalker1)
-                    end;
-                TranslatedAst ->
-                    TranslatedAst
+%% 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_locale_ast(undefined) -> merl:var('_CurrentLocale');
+phrase_locale_ast(Context) -> erl_syntax:tuple([merl:var('_CurrentLocale'), merl:term(Context)]).
+
+phrase_locale(Locale, undefined) -> Locale;
+phrase_locale(Locale, Context) -> {Locale, Context}.
+
+translated_ast(Text, TreeWalker) ->
+    translated_ast(Text, undefined, TreeWalker).
+
+translated_ast(Text, {string_literal, _, Context}, TreeWalker) ->
+    translated_ast(Text, unescape_string_literal(Context), TreeWalker);
+translated_ast({string_literal, _, String}, Context, TreeWalker) ->
+    Text = unescape_string_literal(String),
+    case call_extension(TreeWalker, translate_ast, [Text, Context, TreeWalker]) of
+        undefined ->
+            case TreeWalker#treewalker.context#dtl_context.trans_fun of
+                none -> runtime_trans_ast(Text, Context, TreeWalker);
+                Fun when is_function(Fun, 2) ->
+                    compiletime_trans_ast(Text, Context, TreeWalker)
             end;
-        {runtime, PhraseAst} ->
-            runtime_trans_ast(PhraseAst, Context)
-    end.
+        TranslatedAst ->
+            TranslatedAst
+    end;
+translated_ast(Value, Context, TreeWalker) ->
+    runtime_trans_ast(value_ast(Value, true, false, TreeWalker), Context).
 
-runtime_trans_ast(Phrase, Context, TreeWalker) ->
-    Info = #ast_info{ translatable_strings = [Phrase] },
-    runtime_trans_ast({{merl:term(Phrase), Info}, TreeWalker}, Context).
+runtime_trans_ast(Text, Context, TreeWalker) ->
+    Info = #ast_info{ translatable_strings = [Text] },
+    runtime_trans_ast({{merl:term(Text), Info}, TreeWalker}, Context).
 
 runtime_trans_ast({{ValueAst, AstInfo}, TreeWalker}, undefined) ->
     {{?Q("erlydtl_runtime:translate(_@ValueAst, _CurrentLocale, _TranslationFun)"),
@@ -845,7 +877,7 @@ runtime_trans_ast({{ValueAst, AstInfo}, TreeWalker}, Context) ->
     {{?Q("erlydtl_runtime:translate(_@ValueAst, {_CurrentLocale, _@Context@}, _TranslationFun)"),
       AstInfo}, TreeWalker}.
 
-compiletime_trans_ast(Phrase, LContext,
+compiletime_trans_ast(Text, LContext,
                       #treewalker{
                          context=#dtl_context{
                                     trans_fun=TFun,
@@ -855,8 +887,8 @@ compiletime_trans_ast(Phrase, LContext,
     ClAst = lists:foldl(
               fun(Locale, ClausesAcc) ->
                       [?Q("_@Locale@ -> _@translated",
-                          [{translated, case TFun(Phrase, phrase_context(Locale, LContext)) of
-                                            default -> phrase_ast(Phrase, Context);
+                          [{translated, case TFun(Text, phrase_locale(Locale, LContext)) of
+                                            default -> string_ast(Text, Context);
                                             Translated -> string_ast(Translated, Context)
                                         end}])
                        |ClausesAcc]
@@ -866,27 +898,10 @@ compiletime_trans_ast(Phrase, LContext,
           "  _@_ClAst -> _;",
           " _ -> _@string",
           "end"],
-         [{string, phrase_ast(Phrase, Context)}]),
-      #ast_info{translatable_strings = [Phrase]}},
+         [{string, string_ast(Text, Context)}]),
+      #ast_info{ translatable_strings = [Text] }},
      TreeWalker}.
 
-phrase_context(Locale, undefined) -> Locale;
-phrase_context(Locale, Context) -> {Locale, Context}.
-
-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).
-
 %%% end of context being translation context
 
 

+ 14 - 4
src/erlydtl_parser.yrl

@@ -112,7 +112,11 @@ Nonterminals
     SSITag
 
     BlockTransBlock
+    BlockTransBraced
+    EndBlockTransBraced
+    BlockTransArgs
     BlockTransContent
+
     TransTag
     TransArgs
     TransText
@@ -385,11 +389,17 @@ 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 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'}.
+BlockTransBlock -> BlockTransBraced BlockTransContent EndBlockTransBraced : {blocktrans, '$1', '$2'}.
+BlockTransBraced -> open_tag blocktrans_keyword BlockTransArgs close_tag : '$3'.
+EndBlockTransBraced -> open_tag endblocktrans_keyword close_tag.
+
+BlockTransArgs -> '$empty' : [].
+BlockTransArgs -> with_keyword Args BlockTransArgs : [{args, '$2'}|'$2'].
+BlockTransArgs -> context_keyword string_literal BlockTransArgs : [{context, '$2'}|'$3'].
+
 BlockTransContent -> '$empty' : [].
-BlockTransContent -> BlockTransContent open_var identifier close_var : '$1' ++ [{variable, '$3'}].
-BlockTransContent -> BlockTransContent string : '$1' ++ ['$2'].
+BlockTransContent -> open_var identifier close_var BlockTransContent : [{variable, '$2'}|'$4'].
+BlockTransContent -> string BlockTransContent : ['$1'|'$2'].
 %% TODO: {% plural %}
 
 TemplatetagTag -> open_tag templatetag_keyword Templatetag close_tag : {templatetag, '$3'}.

+ 21 - 20
src/erlydtl_runtime.erl

@@ -6,8 +6,8 @@
 -type phrase() :: text() | {text(), {PluralPhrase::text(), non_neg_integer()}}.
 -type locale() :: term() | {Locale::term(), Context::binary()}.
 
--type old_translate_fun() :: fun((text()) -> text() | undefined).
--type new_translate_fun() :: fun((phrase(), locale()) -> text() | undefined).
+-type old_translate_fun() :: fun((text()) -> iodata() | default).
+-type new_translate_fun() :: fun((phrase(), locale()) -> iodata() | default).
 -type translate_fun() :: new_translate_fun() | old_translate_fun().
 
 -define(IFCHANGED_CONTEXT_VARIABLE, erlydtl_ifchanged_context).
@@ -136,17 +136,21 @@ 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(Phrase, Locale, Fun) -> text() | undefined when
+-spec translate(Phrase, Locale, Fun) -> iodata() | default when
       Phrase :: phrase(),
       Locale :: locale(),
       Fun :: none | translate_fun().
-translate(Phrase, _Locale, none) -> trans_text(Phrase);
 translate(Phrase, Locale, TranslationFun) ->
+    translate(Phrase, Locale, TranslationFun, trans_text(Phrase)).
+
+translate(_Phrase, _Locale, none, Default) -> Default;
+translate(Phrase, Locale, TranslationFun, Default) ->
     case do_translate(Phrase, Locale, TranslationFun) of
-        undefined -> trans_text(Phrase);
-        <<"">> -> trans_text(Phrase);
-        "" -> trans_text(Phrase);
-        Translated -> Translated
+        default -> Default;
+        <<"">> -> Default;
+        "" -> Default;
+        Translated ->
+            Translated
     end.
 
 trans_text({Text, _}) -> Text;
@@ -166,18 +170,15 @@ do_translate(Phrase, Locale, TranslationFun)
 %%  * 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)
+-spec translate_block(phrase(), locale(), orddict:orddict(), none | translate_fun()) -> iodata().
+translate_block(Phrase, Locale, Variables, TranslationFun) ->
+    case translate(Phrase, Locale, TranslationFun, default) of
+        default -> default;
+        Translated ->
+            try interpolate_variables(Translated, Variables)
+            catch _:_ ->
+                    default
+            end
     end.
 
 interpolate_variables(Tpl, []) ->

+ 16 - 1
test/erlydtl_test_defs.erl

@@ -1339,7 +1339,22 @@ all_test_defs() ->
                                       "baz" -> "ok"
                                   end
                           end}],
-        <<"test ok">>}
+        <<"test ok">>},
+       {"blocktrans context (run-time)",
+        <<"{% blocktrans context 'bar' %}translate this{% endblocktrans %}">>,
+        [], [{locale, "foo"}, {translation_fun,
+                               fun ("translate this", {"foo", "bar"}) ->
+                                       "got it"
+                               end}],
+        <<"got it">>},
+       {"blocktrans context (compile-time)",
+        <<"{% blocktrans context 'bar' %}translate this{% endblocktrans %}">>,
+        [], [{locale, "foo"}],
+        [{locale, "foo"}, {blocktrans_fun,
+                           fun ("translate this", {"foo", "bar"}) ->
+                                   "got it"
+                           end}],
+        <<"got it">>}
       ]},
      {"verbatim",
       [{"Plain verbatim",