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

Release 0.9.4

Merge branch 'master' into stable

Conflicts:
	NEWS.md
	rebar.config
Andreas Stenius 11 лет назад
Родитель
Сommit
6280d21a6d

+ 10 - 2
.travis.yml

@@ -1,11 +1,19 @@
 language: erlang
 otp_release:
-#  - 17.0-rc1
+# Test on all supported releases accepted by the `require_otp_vsn` in rebar.config
+  - 17.0
   - R16B03-1
+#  - R16B03 this version is broken!
   - R16B02
+  - R16B01
+  - R16B
+#  - R15B03-1 not available on travis
   - R15B03
-#  - R14B04 (seems lists:concat/1 is broken on R14B04..)
+  - R15B02
 
 # since Travis is naughty and calls rebar get-deps behind our backs,
 # we'll have to clean it up and build merl our selves..
 script: "make -C deps/merl && make tests"
+
+notifications:
+  irc: "chat.freenode.net#erlydtl"

+ 27 - 0
NEWS.md

@@ -5,6 +5,33 @@ suggested by the [GNU Coding
 Standards](http://www.gnu.org/prep/standards/html_node/NEWS-File.html#NEWS-File).
 
 
+## 0.9.4 (2014-04-15)
+
+* Fix compile time variables and constants (#61)
+
+* The `vars` compile time option has been deprecated in favor of
+  `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).
+
+* Renamed compile options for `translation_fun` and `locales` to align
+  with the render options counter parts.
+
+* Support `_` prefix on template variables to avoid unused variable
+  warnings, Erlang style (#164).
+
+* Switched to `eunit_formatters` by @seancribbs for improved eunit reporting.
+
+* All tests pass on Erlang 17.0! :)
+
+
 ## 0.9.3 (2014-03-27)
 
 * Fix release process to work for non-git installations (#154).

+ 119 - 25
README.markdown

@@ -52,6 +52,12 @@ To compile ErlyDTL, run
 in this directory.
 
 
+#### Do not use Erlang R16B03
+
+The erl syntax tools is broken in Erlang R16B03, use R16B03-1 or any
+other supported version instead.
+
+
 Template compilation
 --------------------
 
@@ -93,22 +99,14 @@ Options is a proplist possibly containing:
 * `binary_strings` - Whether to compile strings as binary terms
   (rather than lists). Defaults to `true`.
 
-* `blocktrans_fun` - A two-argument fun to use for translating
-  `blocktrans` blocks, `trans` tags and `_(..)` expressions. This will
-  be called once for each pair of translated element and locale
-  specified in `blocktrans_locales`. The fun should take the form:
-
-  ```erlang
-  Fun(Block::string(), Locale::string()) -> <<"ErlyDTL code">>::binary() | default
-  ```
-
-* `blocktrans_locales` - A list of locales to be passed to
-  `blocktrans_fun`.  Defaults to [].
-
 * `compiler_options` - Proplist with extra options passed directly to
   `compiler:forms/2`. This can prove useful when using extensions to
   add extra defines etc when compiling the generated code.
 
+* `constants` - Replace template variables with a constant value when
+  compiling the template. This can _not_ be overridden when rendering
+  the template. See also `default_vars`.
+
 * `custom_filters_modules` **deprecated** - A list of modules to be
   used for handling custom filters. The modules will be searched in
   order and take precedence over the built-in filters. Each custom
@@ -165,6 +163,11 @@ Options is a proplist possibly containing:
   by name (when there is a name to module mapping also provided in the
   `libraries` option) or by module.
 
+* `default_vars` - Provide default values for variables. Any value
+  from the render variables takes precedence. Notice: in case the
+  value is a `fun/0`, it will be called at compile time. See also
+  `constants`.
+
 * `doc_root` - Included template paths will be relative to this
   directory; defaults to the compiled template's directory.
 
@@ -184,7 +187,12 @@ Options is a proplist possibly containing:
   decide until render time, using the render option
   `lists_0_based`. See also `tuples_0_based`.
 
-* `locale` **deprecated** - The same as {blocktrans_locales, [Val]}.
+* `locale` - Locale to translate to during compile time. May be
+  specified multiple times as well as together with the `locales`
+  option.
+
+* `locales` - A list of locales to be passed to `translation_fun`.
+  Defaults to [].
 
 * `no_env` - Do not read additional options from the OS environment
   variable `ERLYDTL_COMPILER_OPTIONS`.
@@ -221,6 +229,36 @@ Options is a proplist possibly containing:
 
 * `report_errors` - Print errors as they occur.
 
+* `translation_fun` - A two-argument fun to use for translating
+  `blocktrans` blocks, `trans` tags and `_(..)` expressions at compile
+  time. This will be called once for each pair of translated element
+  and locale specified with `locales` and `locale` options. The fun
+  should take the form:
+
+  ```erlang
+  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`.
+
+  Notice, you may instead pass a `fun/0`, `{Module, Function}` or
+  `{Module, Function, Args}` which will be called recursively until it
+  yields a valid translation function, at which time any needed
+  translation setup actions can be carried out prior to returning the
+  next step (either another setup function/tuple, or the translation
+  function).
+
+  ```erlang
+  %% sample translation setup
+  fun () ->
+      translation_engine:init(),
+      fun translation_engine:translate/2
+  end
+  ```
+
 * `tuples_0_based` - **Compatibility warning** Defaults to `false`,
   giving 1-based tuple access, as is common practice in Erlang. Set it
   to `true` to get 1-based access as in Django, or to `defer` to not
@@ -228,9 +266,10 @@ Options is a proplist possibly containing:
   `tuples_0_based`. See also `lists_0_based`.
 
 
-* `vars` - Variables (and their values) to evaluate at compile-time
-  rather than render-time. (Currently not strictly true, see
-  [#61](https://github.com/erlydtl/erlydtl/issues/61))
+* `vars` **deprecated** - Use `default_vars` instead. Variables (and
+  their values) to evaluate at compile-time rather than
+  render-time.
+
 
 * `verbose` - Enable verbose printing of compilation progress. Add
   several for even more verbose (e.g. debug) output.
@@ -291,11 +330,45 @@ my_compiled_template:render(Variables, Options) -> {ok, IOList} | {error, Err}
 
 Same as `render/1`, but with the following options:
 
-* `translation_fun` - A fun/1 that will be used to translate strings
-  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
-  `}}`.
+* `translation_fun` - A `fun/1` or `fun/2` that will be used to
+  translate strings appearing inside `{% trans %}` and `{% blocktrans
+  %}` tags at render-time. The simplest TranslationFun would be
+  `fun(Val) -> Val end`. Placeholders for blocktrans variable
+  interpolation should be wrapped in `{{` and `}}`. In case of
+  `fun/2`, the extra argument is the current locale, possibly together
+  with a translation context in a tuple:
+
+  ```erlang
+  fun (Val|{Val, {Plural_Val, Count}}, Locale|{Locale, Context}) ->
+      Translated_Val
+  end
+  ```
+
+  The context is present when specified in the translation
+  tag. Example:
+
+  ```django
+  {% trans "Some text to translate" context "app-specific" %}
+    or
+  {% blocktrans context "another-context" %}
+    Translate this for {{ name }}.
+  {% 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 %}
+  ```
+
+  Notice, the translation fun can also be a `fun/0` or a MFA-tuple to
+  setup the translation prior to rendering. See the `translation_fun`
+  compile option for more details.
 
 * `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,
@@ -303,7 +376,7 @@ Same as `render/1`, but with the following options:
   template. See also `tuples_0_based`.
 
 * `locale` - A string specifying the current locale, for use with the
-  `blocktrans_fun` compile-time option.
+  `translation_fun` compile-time option.
 
 * `tuples_0_based` - If the compile option `tuples_0_based` was set to
   `defer`, pass this option (or set it to true, `{tuples_0_based,
@@ -364,6 +437,27 @@ can be used for determining which variable bindings need to be passed
 to the `render/3` function.
 
 
+### default_variables/0
+
+```erlang
+my_compiled_template:default_variables() -> [Variable::atom()]
+```
+
+Like `variables/0`, but for any variable which have a default value
+provided at compile time.
+
+
+### constants/0
+
+```erlang
+my_compiled_template:constants() -> [Variable::atom()]
+```
+
+Like `default_variables/0`, but these template variables has been
+replaced with a fixed value at compile time and can not be changed
+when rendering the template.
+
+
 Custom tags and filters
 -----------------------
 
@@ -410,6 +504,9 @@ Differences from standard Django Template Language
 * For an up-to-date list, see all
   [issues](https://github.com/erlydtl/erlydtl/issues) marked
   `dtl_compat`.
+* Erlang specifics: Template variables may be prefixed with underscore
+  (`_`) to avoid "unused variable" warnings (see
+  [#164](https://github.com/erlydtl/erlydtl/issues/164)).
 
 
 Tests
@@ -420,6 +517,3 @@ From a Unix shell, run:
     make tests
 
 Note that the tests will create some output in tests/output in case of regressions.
-
-
-[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/erlydtl/erlydtl/trend.png)](https://bitdeli.com/free "Bitdeli Badge")

+ 5 - 1
include/erlydtl_ext.hrl

@@ -14,6 +14,7 @@
           doc_root = "", 
           parse_trail = [],
           vars = [],
+          const = [],
           record_info = [],
           filters = [],
           tags = [],
@@ -37,13 +38,16 @@
           tuples_0_based = false
          }).
 
+%% ALL fields of ast_info{} must be lists (see erlydtl_compiler_utils:merge_info/2)
 -record(ast_info, {
           dependencies = [],
           translatable_strings = [],
           translated_blocks= [],
           custom_tags = [],
           var_names = [],
-          pre_render_asts = []}).
+          def_names = [],
+          const_names = []
+         }).
 
 -record(treewalker, {
           counter = 0,

+ 22 - 4
rebar.config

@@ -1,18 +1,36 @@
 %% -*- mode: erlang -*-
 
+%% accept R15B02.., any R16B except R16B03
+%% also accept OTP v17, altough it may not work properly on that release yet..
+{require_otp_vsn, "R15B0[^1]|R16B$|R16B[^0]|R16B0[^3]|R16B03-1|17"}.
+
 {erl_opts, [debug_info]}.
 {yrl_opts, [{includefile, "include/erlydtl_preparser.hrl"}]}.
 
+{eunit_opts,
+ [%% This turns off the default output, MUST HAVE
+  no_tty,
+  %% Uses the progress formatter with ANSI-colored output
+  {report, {eunit_progress, [colored
+                             %% uncomment to get a list of slowest running tests
+                             %%, profile
+                            ]}}
+ ]}.
+
 {deps,
  [{merl, ".*",
    {git, "git://github.com/erlydtl/merl.git", "28e5b3829168199e8475fa91b997e0c03b90d280"},
-   [raw]}
+   [raw]},
+  {eunit_formatters, ".*",
+   {git, "git://github.com/seancribbs/eunit_formatters", "7f79fa3fb953b94990bd9b41e92cef7cfecf91ef"}}
  ]}.
 
 {pre_hooks,
- [{compile, "make -C $REBAR_DEPS_DIR/merl all"},
-  {"freebsd", compile, "gmake -C $REBAR_DEPS_DIR/merl all"},
+ [{compile, "make -C $REBAR_DEPS_DIR/merl all -W test"},
+  {"freebsd", compile, "gmake -C $REBAR_DEPS_DIR/merl all -W test"},
   {eunit,
    "erlc -I include/erlydtl_preparser.hrl -o test"
-   " test/erlydtl_extension_testparser.yrl"}
+   " test/erlydtl_extension_testparser.yrl"},
+  {eunit, "make -C $REBAR_DEPS_DIR/merl test"},
+  {"freebsd", eunit, "gmake -C $REBAR_DEPS_DIR/merl test"}
  ]}.

+ 1 - 1
src/erlydtl.app.src

@@ -1,7 +1,7 @@
 %% -*- mode: erlang -*-
 {application, erlydtl,
  [{description, "Django Template Language for Erlang"},
-  {vsn, "0.9.3"},
+  {vsn, "0.9.4"},
   {modules, []},
   {applications, [kernel, stdlib, compiler, syntax_tools]},
   {registered, []}

+ 384 - 331
src/erlydtl_beam_compiler.erl

@@ -102,6 +102,15 @@ format_error({bad_tag, Name, {Mod, Fun}, Arity}) ->
     io_lib:format("Invalid tag '~p' (~p:~p/~p)", [Name, Mod, Fun, Arity]);
 format_error({load_code, Error}) ->
     io_lib:format("Failed to load BEAM code: ~p", [Error]);
+format_error({reserved_variable, ReservedName}) ->
+    io_lib:format("Variable '~s' is reserved for internal use.", [ReservedName]);
+format_error({translation_fun, Fun}) ->
+    io_lib:format("Invalid translation function: ~s~n",
+                  [if is_function(Fun) ->
+                           Info = erlang:fun_info(Fun),
+                           io_lib:format("~s:~s/~p", [proplists:get_value(K, Info) || K <- [module, name, arity]]);
+                      true -> io_lib:format("~p", [Fun])
+                   end]);
 format_error(Error) ->
     erlydtl_compiler:format_error(Error).
 
@@ -186,7 +195,7 @@ compile_to_binary(DjangoParseTree, CheckSum, Context) ->
     try body_ast(DjangoParseTree, init_treewalker(Context)) of
         {{BodyAst, BodyInfo}, BodyTreeWalker} ->
             try custom_tags_ast(BodyInfo#ast_info.custom_tags, BodyTreeWalker) of
-                {{CustomTagsAst, CustomTagsInfo},
+                {CustomTags,
                  #treewalker{
                     context=#dtl_context{
                                errors=#error_info{ list=Errors }
@@ -194,8 +203,7 @@ compile_to_binary(DjangoParseTree, CheckSum, Context) ->
                   when length(Errors) == 0 ->
                     Forms = forms(
                               {BodyAst, BodyInfo},
-                              {CustomTagsAst, CustomTagsInfo},
-                              CheckSum,
+                              CustomTags, CheckSum,
                               CustomTagsTreeWalker),
                     compile_forms(Forms, CustomTagsTreeWalker#treewalker.context);
                 {_, #treewalker{ context=Context1 }} ->
@@ -416,60 +424,36 @@ custom_tags_clauses_ast1([Tag|CustomTags], ExcludeTags, ClauseAcc, InfoAcc, Tree
             end
     end.
 
-dependencies_function(Dependencies) ->
-    ?Q("dependencies() -> _@Dependencies@.").
-
-translatable_strings_function(TranslatableStrings) ->
-    ?Q("translatable_strings() -> _@TranslatableStrings@.").
-
-translated_blocks_function(TranslatedBlocks) ->
-    ?Q("translated_blocks() -> _@TranslatedBlocks@.").
-
-variables_function(Variables) ->
-    ?Q("variables() -> _@vars.",
-       [{vars, merl:term(lists:usort(Variables))}]).
-
 custom_forms(Dir, Module, Functions, AstInfo) ->
-    Exported = [erl_syntax:arity_qualifier(erl_syntax:atom(source_dir), erl_syntax:integer(0)),
-                erl_syntax:arity_qualifier(erl_syntax:atom(dependencies), erl_syntax:integer(0)),
-                erl_syntax:arity_qualifier(erl_syntax:atom(translatable_strings), erl_syntax:integer(0)),
-                erl_syntax:arity_qualifier(erl_syntax:atom(render), erl_syntax:integer(1)),
-                erl_syntax:arity_qualifier(erl_syntax:atom(render), erl_syntax:integer(2)),
-                erl_syntax:arity_qualifier(erl_syntax:atom(render), erl_syntax:integer(3))
-                | lists:foldl(
-                    fun({FunctionName, _}, Acc) ->
-                            [erl_syntax:arity_qualifier(erl_syntax:atom(FunctionName), erl_syntax:integer(1)),
-                             erl_syntax:arity_qualifier(erl_syntax:atom(FunctionName), erl_syntax:integer(2))
-                             |Acc]
-                    end, [], Functions)
-               ],
-    ModuleAst = ?Q("-module('@Module@')."),
-    ExportAst = ?Q("-export(['@_Exported'/1])."),
-
-    SourceFunctionAst = ?Q("source_dir() -> _@Dir@."),
-
-    RenderAsts = ?Q(["render(Tag) -> render(Tag, [], []).",
-                     "render(Tag, Vars) -> render(Tag, Vars, []).",
-                     "render(Tag, Vars, Opts) ->",
-                     "    try '@Module@':Tag(Vars, Opts) of",
-                     "      Val -> {ok, Val}",
-                     "    catch",
-                     "      Err -> {error, Err}",
-                     "    end."]),
-
-    DependenciesFunctionAst = dependencies_function(AstInfo#ast_info.dependencies),
-    TranslatableStringsFunctionAst = translatable_strings_function(AstInfo#ast_info.translatable_strings),
-    FunctionAsts = lists:foldl(
-                     fun({_, FunctionDefs}, Acc) ->
-                             FunctionDefs ++ Acc
-                     end,
-                     RenderAsts, Functions),
-
-    [erl_syntax:revert(X)
-     || X <- [ModuleAst, ExportAst, SourceFunctionAst, DependenciesFunctionAst,
-              TranslatableStringsFunctionAst | FunctionAsts]
-            ++ AstInfo#ast_info.pre_render_asts
-    ].
+    Dependencies = AstInfo#ast_info.dependencies,
+    TranslatableStrings = AstInfo#ast_info.translatable_strings,
+
+    erl_syntax:revert_forms(
+      lists:flatten(
+        ?Q(["-module('@Module@').",
+            "-export([source_dir/0, dependencies/0, translatable_strings/0,",
+            "         render/1, render/2, render/3]).",
+            "-export(['@__export_functions'/0]).",
+            "source_dir() -> _@Dir@.",
+            "dependencies() -> _@Dependencies@.",
+            "translatable_strings() -> _@TranslatableStrings@.",
+            "render(Tag) -> render(Tag, [], []).",
+            "render(Tag, Vars) -> render(Tag, Vars, []).",
+            "render(Tag, Vars, Opts) ->",
+            "  try '@Module@':Tag(Vars, Opts) of",
+            "    Val -> {ok, Val}",
+            "  catch",
+            "    Err -> {error, Err}",
+            "  end.",
+            "'@_functions'() -> _."
+           ],
+           [{export_functions,
+             erl_syntax:list(
+               [erl_syntax:arity_qualifier(erl_syntax:atom(FName), erl_syntax:integer(Arity))
+                || {FName, _} <- Functions, Arity <- [1, 2]])},
+            {functions, [Ast || {_, Ast} <- Functions]}
+           ]))
+     ).
 
 stringify(BodyAst, #dtl_context{ binary_strings=BinaryStrings }) ->
     [?Q("erlydtl_runtime:stringify_final(_@BodyAst, '@BinaryStrings@')")].
@@ -483,55 +467,37 @@ forms({BodyAst, BodyInfo}, {CustomTagsFunctionAst, CustomTagsInfo}, CheckSum,
         }=TreeWalker) ->
     MergedInfo = merge_info(BodyInfo, CustomTagsInfo),
 
-    Render0FunctionAst = ?Q("render() -> render([])."),
-    Render1FunctionAst = ?Q("render(Variables) -> render(Variables, [])."),
-
-    Render2FunctionAst = ?Q(["render(Variables, RenderOptions) ->",
-                             "  try render_internal(Variables, RenderOptions) of",
-                             "    Val -> {ok, Val}",
-                             "  catch",
-                             "    Err -> {error, Err}",
-                             "end."
-                            ]),
-
-    SourceFunctionAst = ?Q("source() -> {_@File@, _@CheckSum@}."),
-
-    DependenciesFunctionAst = dependencies_function(MergedInfo#ast_info.dependencies),
-
-    TranslatableStringsAst = translatable_strings_function(MergedInfo#ast_info.translatable_strings),
-
-    TranslatedBlocksAst = translated_blocks_function(MergedInfo#ast_info.translated_blocks),
-
-    VariablesAst = variables_function(MergedInfo#ast_info.var_names),
-
-    MatchAst = options_match_ast(TreeWalker),
-    BodyAstTmp = MatchAst ++ stringify(BodyAst, Context),
-    RenderInternalFunctionAst = ?Q("render_internal(_Variables, RenderOptions) -> _@BodyAstTmp."),
-
-    ModuleAst  = ?Q("-module('@Module@')."),
-
-    ExportAst = erl_syntax:attribute(
-                  erl_syntax:atom(export),
-                  [erl_syntax:list(
-                     [erl_syntax:arity_qualifier(erl_syntax:atom(render), erl_syntax:integer(0)),
-                      erl_syntax:arity_qualifier(erl_syntax:atom(render), erl_syntax:integer(1)),
-                      erl_syntax:arity_qualifier(erl_syntax:atom(render), erl_syntax:integer(2)),
-                      erl_syntax:arity_qualifier(erl_syntax:atom(source), erl_syntax:integer(0)),
-                      erl_syntax:arity_qualifier(erl_syntax:atom(dependencies), erl_syntax:integer(0)),
-                      erl_syntax:arity_qualifier(erl_syntax:atom(translatable_strings), erl_syntax:integer(0)),
-                      erl_syntax:arity_qualifier(erl_syntax:atom(translated_blocks), erl_syntax:integer(0)),
-                      erl_syntax:arity_qualifier(erl_syntax:atom(variables), erl_syntax:integer(0))
-                     ])
-                  ]),
+    Dependencies = MergedInfo#ast_info.dependencies,
+    TranslatableStrings = MergedInfo#ast_info.translatable_strings,
+    TranslatedBlocks = MergedInfo#ast_info.translated_blocks,
+    Variables = lists:usort(MergedInfo#ast_info.var_names),
+    DefaultVariables = lists:usort(MergedInfo#ast_info.def_names),
+    Constants = lists:usort(MergedInfo#ast_info.const_names),
+    FinalBodyAst = options_match_ast(TreeWalker) ++ stringify(BodyAst, Context),
 
     erl_syntax:revert_forms(
-      erl_syntax:form_list(
-        [ModuleAst, ExportAst, Render0FunctionAst, Render1FunctionAst, Render2FunctionAst,
-         SourceFunctionAst, DependenciesFunctionAst, TranslatableStringsAst,
-         TranslatedBlocksAst, VariablesAst, RenderInternalFunctionAst,
-         CustomTagsFunctionAst
-         |BodyInfo#ast_info.pre_render_asts
-        ])).
+      ?Q(["-module('@Module@').",
+          "-export([render/0, render/1, render/2, source/0, dependencies/0,",
+          "         translatable_strings/0, translated_blocks/0, variables/0,",
+          "         default_variables/0, constants/0]).",
+          "source() -> {_@File@, _@CheckSum@}.",
+          "dependencies() -> _@Dependencies@.",
+          "variables() -> _@Variables@.",
+          "default_variables() -> _@DefaultVariables@.",
+          "constants() -> _@Constants@.",
+          "translatable_strings() -> _@TranslatableStrings@.",
+          "translated_blocks() -> _@TranslatedBlocks@.",
+          "'@_CustomTagsFunctionAst'() -> _.",
+          "render() -> render([], []).",
+          "render(Variables) -> render(Variables, []).",
+          "render(Variables, RenderOptions) ->",
+          "  try render_internal(Variables, RenderOptions) of",
+          "    Val -> {ok, Val}",
+          "  catch",
+          "    Err -> {error, Err}",
+          "  end.",
+          "render_internal(_Variables, RenderOptions) -> _@FinalBodyAst."
+         ])).
 
 options_match_ast(#treewalker{ context=Context }=TreeWalker) ->
     options_match_ast(Context, TreeWalker);
@@ -540,8 +506,9 @@ options_match_ast(Context) ->
 
 options_match_ast(Context, TreeWalker) ->
     [
-     ?Q("_TranslationFun = proplists:get_value(translation_fun, RenderOptions, none)"),
-     ?Q("_CurrentLocale = proplists:get_value(locale, RenderOptions, none)"),
+     ?Q(["_TranslationFun = erlydtl_runtime:init_translation(",
+         "  proplists:get_value(translation_fun, RenderOptions, none))"]),
+     ?Q("_CurrentLocale = proplists:get_value(locale, RenderOptions, default)"),
      ?Q("_RecordInfo = _@info", [{info, merl:term(Context#dtl_context.record_info)}])
      | case call_extension(Context, setup_render_ast, [Context, TreeWalker]) of
            undefined -> [];
@@ -588,169 +555,149 @@ body_ast(DjangoParseTree, TreeWalker) ->
 
 body_ast(DjangoParseTree, BodyScope, TreeWalker) ->
     {ScopeId, TreeWalkerScope} = begin_scope(BodyScope, TreeWalker),
-    {AstInfoList, TreeWalker1} =
-        lists:mapfoldl(
-          fun ({'autoescape', {identifier, _, OnOrOff}, Contents}, TW) ->
-                  {Info, BodyTW} = body_ast(Contents, push_auto_escape(OnOrOff, TW)),
-                  {Info, pop_auto_escape(BodyTW)};
-              ({'block', {identifier, Pos, Name}, Contents}, #treewalker{ context=Context }=TW) ->
-                  {Block, BlockScope} =
-                      case dict:find(Name, Context#dtl_context.block_dict) of
-                          {ok, ChildBlock} ->
-                              {{ContentsAst, _ContentsInfo}, _ContentsTW} = body_ast(Contents, TW),
-                              {ChildBlock,
-                               create_scope(
-                                 [{block, ?Q("[{super, _@ContentsAst}]")}],
-                                 Pos, TW)
-                              };
-                          _ ->
-                              {Contents, empty_scope()}
-                      end,
-                  body_ast(Block, BlockScope, TW);
-              ({'blocktrans', Args, Contents}, TW) ->
-                  blocktrans_ast(Args, Contents, TW);
-              ({'call', {identifier, _, Name}}, TW) ->
-                  call_ast(Name, TW);
-              ({'call', {identifier, _, Name}, With}, TW) ->
-                  call_with_ast(Name, With, TW);
-              ({'comment', _Contents}, TW) ->
-                  empty_ast(TW);
-              ({'cycle', Names}, TW) ->
-                  cycle_ast(Names, TW);
-              ({'cycle_compat', Names}, TW) ->
-                  cycle_compat_ast(Names, TW);
-              ({'date', 'now', {string_literal, _Pos, FormatString}}, TW) ->
-                  now_ast(FormatString, TW);
-              ({'filter', FilterList, Contents}, TW) ->
-                  filter_tag_ast(FilterList, Contents, TW);
-              ({'firstof', Vars}, TW) ->
-                  firstof_ast(Vars, TW);
-              ({'for', {'in', IteratorList, Variable, Reversed}, Contents}, TW) ->
-                  {EmptyAstInfo, TW1} = empty_ast(TW),
-                  for_loop_ast(IteratorList, Variable, Reversed, Contents, EmptyAstInfo, TW1);
-              ({'for', {'in', IteratorList, Variable, Reversed}, Contents, EmptyPartContents}, TW) ->
-                  {EmptyAstInfo, TW1} = body_ast(EmptyPartContents, TW),
-                  for_loop_ast(IteratorList, Variable, Reversed, Contents, EmptyAstInfo, TW1);
-              ({'if', Expression, Contents, Elif}, TW) ->
-                  {IfAstInfo, TW1} = body_ast(Contents, TW),
-                  {ElifAstInfo, TW2} = body_ast(Elif, TW1),
-                  ifelse_ast(Expression, IfAstInfo, ElifAstInfo, TW2);
-              ({'if', Expression, Contents}, TW) ->
-                  {IfAstInfo, TW1} = body_ast(Contents, TW),
-                  {ElseAstInfo, TW2} = empty_ast(TW1),
-                  ifelse_ast(Expression, IfAstInfo, ElseAstInfo, TW2);
-              ({'ifchanged', '$undefined', Contents}, TW) ->
-                  {IfAstInfo, TW1} = body_ast(Contents, TW),
-                  {ElseAstInfo, TW2} = empty_ast(TW1),
-                  ifchanged_contents_ast(Contents, IfAstInfo, ElseAstInfo, TW2);
-              ({'ifchanged', Values, Contents}, TW) ->
-                  {IfAstInfo, TW1} = body_ast(Contents, TW),
-                  {ElseAstInfo, TW2} = empty_ast(TW1),
-                  ifchanged_values_ast(Values, IfAstInfo, ElseAstInfo, TW2);
-              ({'ifchangedelse', '$undefined', IfContents, ElseContents}, TW) ->
-                  {IfAstInfo, TW1} = body_ast(IfContents, TW),
-                  {ElseAstInfo, TW2} = body_ast(ElseContents, TW1),
-                  ifchanged_contents_ast(IfContents, IfAstInfo, ElseAstInfo, TW2);
-              ({'ifchangedelse', Values, IfContents, ElseContents}, TW) ->
-                  {IfAstInfo, TW1} = body_ast(IfContents, TW),
-                  {ElseAstInfo, TW2} = body_ast(ElseContents, TW1),
-                  ifchanged_values_ast(Values, IfAstInfo, ElseAstInfo, TW2);
-              ({'ifelse', Expression, IfContents, ElseContents}, TW) ->
-                  {IfAstInfo, TW1} = body_ast(IfContents, TW),
-                  {ElseAstInfo, TW2} = body_ast(ElseContents, TW1),
-                  ifelse_ast(Expression, IfAstInfo, ElseAstInfo, TW2);
-              ({'ifequal', [Arg1, Arg2], Contents}, TW) ->
-                  {IfAstInfo, TW1} = body_ast(Contents, TW),
-                  {ElseAstInfo, TW2} = empty_ast(TW1),
-                  ifelse_ast({'expr', "eq", Arg1, Arg2}, IfAstInfo, ElseAstInfo, TW2);
-              ({'ifequalelse', [Arg1, Arg2], IfContents, ElseContents}, TW) ->
-                  {IfAstInfo, TW1} = body_ast(IfContents, TW),
-                  {ElseAstInfo, TW2} = body_ast(ElseContents,TW1),
-                  ifelse_ast({'expr', "eq", Arg1, Arg2}, IfAstInfo, ElseAstInfo, TW2);
-              ({'ifnotequal', [Arg1, Arg2], Contents}, TW) ->
-                  {IfAstInfo, TW1} = body_ast(Contents, TW),
-                  {ElseAstInfo, TW2} = empty_ast(TW1),
-                  ifelse_ast({'expr', "ne", Arg1, Arg2}, IfAstInfo, ElseAstInfo, TW2);
-              ({'ifnotequalelse', [Arg1, Arg2], IfContents, ElseContents}, TW) ->
-                  {IfAstInfo, TW1} = body_ast(IfContents, TW),
-                  {ElseAstInfo, TW2} = body_ast(ElseContents, TW1),
-                  ifelse_ast({'expr', "ne", Arg1, Arg2}, IfAstInfo, ElseAstInfo, TW2);
-              ({'include', {string_literal, _, File}, Args}, #treewalker{ context=Context }=TW) ->
-                  include_ast(unescape_string_literal(File), Args, Context#dtl_context.local_scopes, TW);
-              ({'include_only', {string_literal, _, File}, Args}, TW) ->
-                  {Info, IncTW} = include_ast(unescape_string_literal(File), Args, [], TW),
-                  {Info, restore_scope(TW, IncTW)};
-              ({'load_libs', Libs}, TW) ->
-                  load_libs_ast(Libs, TW);
-              ({'load_from_lib', What, Lib}, TW) ->
-                  load_from_lib_ast(What, Lib, TW);
-              ({'regroup', {ListVariable, Grouper, {identifier, _, NewVariable}}}, TW) ->
-                  regroup_ast(ListVariable, Grouper, NewVariable, TW);
-              ('end_regroup', TW) ->
-                  {{end_scope, #ast_info{}}, TW};
-              ({'spaceless', Contents}, TW) ->
-                  spaceless_ast(Contents, TW);
-              ({'ssi', Arg}, TW) ->
-                  ssi_ast(Arg, TW);
-              ({'ssi_parsed', {string_literal, _, FileName}}, #treewalker{ context=Context }=TW) ->
-                  include_ast(unescape_string_literal(FileName), [], Context#dtl_context.local_scopes, TW);
-              ({'string', _Pos, String}, TW) ->
-                  string_ast(String, TW);
-              ({'tag', Name, Args}, TW) ->
-                  tag_ast(Name, Args, TW);
-              ({'templatetag', {_, _, TagName}}, TW) ->
-                  templatetag_ast(TagName, TW);
-              ({'trans', Value}, TW) ->
-                  translated_ast(Value, TW);
-              ({'widthratio', Numerator, Denominator, Scale}, TW) ->
-                  widthratio_ast(Numerator, Denominator, Scale, TW);
-              ({'with', Args, Contents}, TW) ->
-                  with_ast(Args, Contents, TW);
-              ({'scope_as', {identifier, _, Name}, Contents}, TW) ->
-                  scope_as(Name, Contents, TW);
-              ({'extension', Tag}, TW) ->
-                  extension_ast(Tag, TW);
-              ({'extends', _}, TW) ->
-                  empty_ast(?ERR(unexpected_extends_tag, TW));
-              (ValueToken, TW) ->
-                  format(value_ast(ValueToken, true, true, TW))
-          end,
-          TreeWalkerScope,
-          DjangoParseTree),
+    BodyFun =
+        fun ({'autoescape', {identifier, _, OnOrOff}, Contents}, TW) ->
+                {Info, BodyTW} = body_ast(Contents, push_auto_escape(OnOrOff, TW)),
+                {Info, pop_auto_escape(BodyTW)};
+            ({'block', {identifier, Pos, Name}, Contents}, #treewalker{ context=Context }=TW) ->
+                {Block, BlockScope} =
+                    case dict:find(Name, Context#dtl_context.block_dict) of
+                        {ok, ChildBlock} ->
+                            {{ContentsAst, _ContentsInfo}, _ContentsTW} = body_ast(Contents, TW),
+                            {ChildBlock,
+                             create_scope(
+                               [{block, ?Q("[{super, _@ContentsAst}]")}],
+                               Pos, TW)
+                            };
+                        _ ->
+                            {Contents, empty_scope()}
+                    end,
+                body_ast(Block, BlockScope, 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) ->
+                call_with_ast(Name, With, TW);
+            ({'comment', _Contents}, TW) ->
+                empty_ast(TW);
+            ({'comment_tag', _, _}, TW) ->
+                empty_ast(TW);
+            ({'cycle', Names}, TW) ->
+                cycle_ast(Names, TW);
+            ({'cycle_compat', Names}, TW) ->
+                cycle_compat_ast(Names, TW);
+            ({'date', 'now', {string_literal, _Pos, FormatString}}, TW) ->
+                now_ast(FormatString, TW);
+            ({'filter', FilterList, Contents}, TW) ->
+                filter_tag_ast(FilterList, Contents, TW);
+            ({'firstof', Vars}, TW) ->
+                firstof_ast(Vars, TW);
+            ({'for', {'in', IteratorList, Variable, Reversed}, Contents}, TW) ->
+                {EmptyAstInfo, TW1} = empty_ast(TW),
+                for_loop_ast(IteratorList, Variable, Reversed, Contents, EmptyAstInfo, TW1);
+            ({'for', {'in', IteratorList, Variable, Reversed}, Contents, EmptyPartContents}, TW) ->
+                {EmptyAstInfo, TW1} = body_ast(EmptyPartContents, TW),
+                for_loop_ast(IteratorList, Variable, Reversed, Contents, EmptyAstInfo, TW1);
+            ({'if', Expression, Contents, Elif}, TW) ->
+                {IfAstInfo, TW1} = body_ast(Contents, TW),
+                {ElifAstInfo, TW2} = body_ast(Elif, TW1),
+                ifelse_ast(Expression, IfAstInfo, ElifAstInfo, TW2);
+            ({'if', Expression, Contents}, TW) ->
+                {IfAstInfo, TW1} = body_ast(Contents, TW),
+                {ElseAstInfo, TW2} = empty_ast(TW1),
+                ifelse_ast(Expression, IfAstInfo, ElseAstInfo, TW2);
+            ({'ifchanged', '$undefined', Contents}, TW) ->
+                {IfAstInfo, TW1} = body_ast(Contents, TW),
+                {ElseAstInfo, TW2} = empty_ast(TW1),
+                ifchanged_contents_ast(Contents, IfAstInfo, ElseAstInfo, TW2);
+            ({'ifchanged', Values, Contents}, TW) ->
+                {IfAstInfo, TW1} = body_ast(Contents, TW),
+                {ElseAstInfo, TW2} = empty_ast(TW1),
+                ifchanged_values_ast(Values, IfAstInfo, ElseAstInfo, TW2);
+            ({'ifchangedelse', '$undefined', IfContents, ElseContents}, TW) ->
+                {IfAstInfo, TW1} = body_ast(IfContents, TW),
+                {ElseAstInfo, TW2} = body_ast(ElseContents, TW1),
+                ifchanged_contents_ast(IfContents, IfAstInfo, ElseAstInfo, TW2);
+            ({'ifchangedelse', Values, IfContents, ElseContents}, TW) ->
+                {IfAstInfo, TW1} = body_ast(IfContents, TW),
+                {ElseAstInfo, TW2} = body_ast(ElseContents, TW1),
+                ifchanged_values_ast(Values, IfAstInfo, ElseAstInfo, TW2);
+            ({'ifelse', Expression, IfContents, ElseContents}, TW) ->
+                {IfAstInfo, TW1} = body_ast(IfContents, TW),
+                {ElseAstInfo, TW2} = body_ast(ElseContents, TW1),
+                ifelse_ast(Expression, IfAstInfo, ElseAstInfo, TW2);
+            ({'ifequal', [Arg1, Arg2], Contents}, TW) ->
+                {IfAstInfo, TW1} = body_ast(Contents, TW),
+                {ElseAstInfo, TW2} = empty_ast(TW1),
+                ifelse_ast({'expr', "eq", Arg1, Arg2}, IfAstInfo, ElseAstInfo, TW2);
+            ({'ifequalelse', [Arg1, Arg2], IfContents, ElseContents}, TW) ->
+                {IfAstInfo, TW1} = body_ast(IfContents, TW),
+                {ElseAstInfo, TW2} = body_ast(ElseContents,TW1),
+                ifelse_ast({'expr', "eq", Arg1, Arg2}, IfAstInfo, ElseAstInfo, TW2);
+            ({'ifnotequal', [Arg1, Arg2], Contents}, TW) ->
+                {IfAstInfo, TW1} = body_ast(Contents, TW),
+                {ElseAstInfo, TW2} = empty_ast(TW1),
+                ifelse_ast({'expr', "ne", Arg1, Arg2}, IfAstInfo, ElseAstInfo, TW2);
+            ({'ifnotequalelse', [Arg1, Arg2], IfContents, ElseContents}, TW) ->
+                {IfAstInfo, TW1} = body_ast(IfContents, TW),
+                {ElseAstInfo, TW2} = body_ast(ElseContents, TW1),
+                ifelse_ast({'expr', "ne", Arg1, Arg2}, IfAstInfo, ElseAstInfo, TW2);
+            ({'include', {string_literal, _, File}, Args}, #treewalker{ context=Context }=TW) ->
+                include_ast(unescape_string_literal(File), Args, Context#dtl_context.local_scopes, TW);
+            ({'include_only', {string_literal, _, File}, Args}, TW) ->
+                {Info, IncTW} = include_ast(unescape_string_literal(File), Args, [], TW),
+                {Info, restore_scope(TW, IncTW)};
+            ({'load_libs', Libs}, TW) ->
+                load_libs_ast(Libs, TW);
+            ({'load_from_lib', What, Lib}, TW) ->
+                load_from_lib_ast(What, Lib, TW);
+            ({'regroup', {ListVariable, Grouper, {identifier, _, NewVariable}}}, TW) ->
+                regroup_ast(ListVariable, Grouper, NewVariable, TW);
+            ('end_regroup', TW) ->
+                {{end_scope, #ast_info{}}, TW};
+            ({'spaceless', Contents}, TW) ->
+                spaceless_ast(Contents, TW);
+            ({'ssi', Arg}, TW) ->
+                ssi_ast(Arg, TW);
+            ({'ssi_parsed', {string_literal, _, FileName}}, #treewalker{ context=Context }=TW) ->
+                include_ast(unescape_string_literal(FileName), [], Context#dtl_context.local_scopes, TW);
+            ({'string', _Pos, String}, TW) ->
+                string_ast(String, TW);
+            ({'tag', Name, Args}, TW) ->
+                tag_ast(Name, Args, TW);
+            ({'templatetag', {_, _, TagName}}, TW) ->
+                templatetag_ast(TagName, TW);
+            ({'trans', Value}, TW) ->
+                translated_ast(Value, TW);
+            ({'trans', Value, Context}, TW) ->
+                translated_ast(Value, Context, TW);
+            ({'widthratio', Numerator, Denominator, Scale}, TW) ->
+                widthratio_ast(Numerator, Denominator, Scale, TW);
+            ({'with', Args, Contents}, TW) ->
+                with_ast(Args, Contents, TW);
+            ({'scope_as', {identifier, _, Name}, Contents}, TW) ->
+                scope_as(Name, Contents, TW);
+            ({'extension', Tag}, TW) ->
+                extension_ast(Tag, TW);
+            ({'extends', _}, TW) ->
+                empty_ast(?ERR(unexpected_extends_tag, TW));
+            (ValueToken, TW) ->
+                format(value_ast(ValueToken, true, true, TW))
+        end,
+
+    {AstInfoList, TreeWalker1} = lists:mapfoldl(BodyFun, TreeWalkerScope, DjangoParseTree),
 
-    Vars = TreeWalker1#treewalker.context#dtl_context.vars,
-    {AstList, {Info, TreeWalker2}} =
+    {AstList, Info} =
         lists:mapfoldl(
-          fun ({Ast, Info}, {InfoAcc, TreeWalkerAcc}) ->
-                  PresetVars = lists:foldl(
-                                 fun (X, Acc) ->
-                                         case proplists:lookup(X, Vars) of
-                                             none -> Acc;
-                                             Val -> [Val|Acc]
-                                         end
-                                 end,
-                                 [],
-                                 Info#ast_info.var_names),
-                  if length(PresetVars) == 0 ->
-                          {Ast, {merge_info(Info, InfoAcc), TreeWalkerAcc}};
-                     true ->
-                          Counter = TreeWalkerAcc#treewalker.counter,
-                          Name = list_to_atom(lists:concat([pre_render, Counter])),
-                          Ast1 = ?Q("'@Name@'(_@PresetVars@, RenderOptions)"),
-                          PreRenderAst = ?Q("'@Name@'(_Variables, RenderOptions) -> _@match, _@Ast.",
-                                            [{match, options_match_ast(TreeWalkerAcc)}]),
-                          PreRenderAsts = Info#ast_info.pre_render_asts,
-                          Info1 = Info#ast_info{pre_render_asts = [PreRenderAst | PreRenderAsts]},
-                          {Ast1, {merge_info(Info1, InfoAcc), TreeWalkerAcc#treewalker{counter = Counter + 1}}}
-                  end
-          end,
-          {#ast_info{}, TreeWalker1},
-          AstInfoList),
+          fun ({Ast, Info}, InfoAcc) ->
+                  {Ast, merge_info(Info, InfoAcc)}
+          end, #ast_info{}, AstInfoList),
 
-    {Ast, TreeWalker3} = end_scope(
+    {Ast, TreeWalker2} = end_scope(
                            fun ([ScopeVars|ScopeBody]) -> [?Q("begin _@ScopeVars, [_@ScopeBody] end")] end,
-                           ScopeId, AstList, TreeWalker2),
-    {{erl_syntax:list(Ast), Info}, TreeWalker3}.
+                           ScopeId, AstList, TreeWalker1),
+    {{erl_syntax:list(Ast), Info}, TreeWalker2}.
 
 
 value_ast(ValueToken, AsString, EmptyIfUndefined, TreeWalker) ->
@@ -804,38 +751,58 @@ with_dependency(FilePath, {{Ast, Info}, TreeWalker}) ->
 empty_ast(TreeWalker) ->
     {{erl_syntax:list([]), #ast_info{}}, TreeWalker}.
 
-blocktrans_ast(ArgList, Contents, TreeWalker) ->
+
+%%% Note: Context here refers to the translation context, not the #dtl_context{} record
+
+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} ->
+                      unescape_string_literal(S)
+              end,
+
     %% 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),
+          case Count of
+              {{identifier, _, Name}, Value} ->
+                  [{Name, Value}|ArgList];
+              _ ->
+                  ArgList
+          end),
 
     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; PluralContents =/= undefined ->
             %% translate in runtime
             {FinalAst, FinalTW} = blocktrans_runtime_ast(
-                                    {DefaultAst, MergedInfo},
-                                    SourceText, Contents, TreeWalker3),
+                                    {DefaultAst, MergedInfo}, SourceText, Contents, Context,
+                                    plural_contents(PluralContents, Count, TreeWalker3)),
             {FinalAst, restore_scope(TreeWalker1, FinalTW)};
-        BlockTransFun when is_function(BlockTransFun) ->
+       is_function(TFun, 2) ->
             %% translate in compile-time
-            {FinalAstInfo, FinalTreeWalker, Clauses} = 
+            {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 ->
@@ -845,74 +812,131 @@ 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)}
+             restore_scope(TreeWalker1, FinalTreeWalker)};
+       true ->
+            empty_ast(?ERR({translation_fun, TFun}, TreeWalker3))
     end.
 
-blocktrans_runtime_ast({DefaultAst, Info}, SourceText, Contents, 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}
                  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, Info}, TreeWalker1}.
-
-translated_ast({string_literal, _, String}, TreeWalker) ->
-    UnescapedStr = unescape_string_literal(String),
-    case call_extension(TreeWalker, translate_ast, [UnescapedStr, TreeWalker]) of
+    BlockTransAst = ?Q(["begin",
+                        "  case erlydtl_runtime:translate_block(",
+                        "         _@phrase, _@locale,",
+                        "         _@VarListAst, _TranslationFun) of",
+                        "    default -> _@DefaultAst;",
+                        "    Text -> Text",
+                        "  end",
+                        "end"],
+                       [{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)]).
+
+phrase_locale(Locale, undefined) -> Locale;
+phrase_locale(Locale, Context) -> {Locale, Context}.
+
+translated_ast(Text, TreeWalker) ->
+    translated_ast(Text, undefined, TreeWalker).
+
+translated_ast({noop, Value}, _, TreeWalker) ->
+    value_ast(Value, true, true, 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 ->
-            AstInfo = #ast_info{translatable_strings = [UnescapedStr]},
             case TreeWalker#treewalker.context#dtl_context.trans_fun of
-                none -> runtime_trans_ast({{erl_syntax:string(UnescapedStr), AstInfo}, TreeWalker});
-                _ -> compiletime_trans_ast(UnescapedStr, AstInfo, TreeWalker)
+                none -> runtime_trans_ast(Text, Context, TreeWalker);
+                Fun when is_function(Fun, 2) ->
+                    compiletime_trans_ast(Fun, Text, Context, TreeWalker);
+                Fun when is_function(Fun, 1) ->
+                    compiletime_trans_ast(fun (T, _) -> Fun(T) end,
+                                          Text, Context, TreeWalker);
+                Fun ->
+                    empty_ast(?ERR({translation_fun, Fun}, TreeWalker))
             end;
-        Translated ->
-            Translated
+        TranslatedAst ->
+            TranslatedAst
     end;
-translated_ast(ValueToken, TreeWalker) ->
-    runtime_trans_ast(value_ast(ValueToken, true, false, TreeWalker)).
-
-runtime_trans_ast({{ValueAst, AstInfo}, TreeWalker}) ->
-    {{?Q("erlydtl_runtime:translate(_@ValueAst, _TranslationFun)"),
+translated_ast(Value, Context, TreeWalker) ->
+    runtime_trans_ast(value_ast(Value, true, false, 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)"),
+      AstInfo}, TreeWalker};
+runtime_trans_ast({{ValueAst, AstInfo}, TreeWalker}, Context) ->
+    {{?Q("erlydtl_runtime:translate(_@ValueAst, {_CurrentLocale, _@Context@}, _TranslationFun)"),
       AstInfo}, TreeWalker}.
 
-compiletime_trans_ast(String, AstInfo,
+compiletime_trans_ast(TFun, Text, LContext,
                       #treewalker{
                          context=#dtl_context{
-                                    trans_fun=TFun,
                                     trans_locales=TLocales
                                    }=Context
                         }=TreeWalker) ->
     ClAst = lists:foldl(
               fun(Locale, ClausesAcc) ->
                       [?Q("_@Locale@ -> _@translated",
-                          [{translated, case TFun(String, Locale) of
-                                            default -> string_ast(String, Context);
+                          [{translated, case TFun(Text, phrase_locale(Locale, LContext)) of
+                                            default -> string_ast(Text, Context);
                                             Translated -> string_ast(Translated, Context)
                                         end}])
                        |ClausesAcc]
               end,
               [], TLocales),
-    CaseAst = ?Q(["case _CurrentLocale of",
-                  "  _@_ClAst -> _;",
-                  " _ -> _@string",
-                  "end"],
-                 [{string, string_ast(String, Context)}]),
-    {{CaseAst, AstInfo}, TreeWalker}.
+    {{?Q(["case _CurrentLocale of",
+          "  _@_ClAst -> _;",
+          " _ -> _@string",
+          "end"],
+         [{string, string_ast(Text, Context)}]),
+      #ast_info{ translatable_strings = [Text] }},
+     TreeWalker}.
+
+%%% end of context being translation context
+
 
 %% Completely unnecessary in ErlyDTL (use {{ "{%" }} etc), but implemented for compatibility.
 templatetag_ast("openblock", TreeWalker) ->
@@ -1127,8 +1151,7 @@ resolve_variable_ast1({attribute, {{_, Pos, Attr}, Variable}}, {Runtime, Finder}
     FileName = get_current_file(TreeWalker1),
     {{?Q(["'@Runtime@':'@Finder@'(",
           "  _@Attr@, _@VarAst,",
-          "  [",
-          "   {lists_0_based, _@Lists0Based@},",
+          "  [{lists_0_based, _@Lists0Based@},",
           "   {tuples_0_based, _@Tuples0Based@},",
           "   {render_options, RenderOptions},",
           "   {record_info, _RecordInfo},",
@@ -1140,20 +1163,47 @@ resolve_variable_ast1({attribute, {{_, Pos, Attr}, Variable}}, {Runtime, Finder}
 
 resolve_variable_ast1({variable, {identifier, Pos, VarName}}, {Runtime, Finder}, TreeWalker) ->
     Ast = case resolve_variable(VarName, TreeWalker) of
-              undefined ->
+              {_, undefined} ->
                   FileName = get_current_file(TreeWalker),
                   {?Q(["'@Runtime@':'@Finder@'(",
                        "  _@VarName@, _Variables,",
                        "  [{filename, _@FileName@},",
                        "   {pos, _@Pos@},",
                        "   {record_info, _RecordInfo},",
-                       "   {render_options, RenderOptions}])"]),
+                       "   {render_options, RenderOptions}])"
+                      ]),
                    #ast_info{ var_names=[VarName] }};
-              Val ->
+              {default_vars, Val} ->
+                  FileName = get_current_file(TreeWalker),
+                  {?Q(["'@Runtime@':fetch_value(",
+                       "  _@VarName@, _Variables,",
+                       "  [{filename, _@FileName@},",
+                       "   {pos, _@Pos@},",
+                       "   {record_info, _RecordInfo},",
+                       "   {render_options, RenderOptions}],",
+                       "   _@val)"
+                      ],
+                      [{val, merl:term(erlydtl_filters:format_number(Val))}]),
+                   #ast_info{ var_names=[VarName], def_names=[VarName] }};
+              {constant, Val} ->
+                  {merl:term(erlydtl_filters:format_number(Val)),
+                   #ast_info{ const_names=[VarName] }};
+              {scope, Val} ->
                   {Val, #ast_info{}}
           end,
     {Ast, TreeWalker}.
 
+resolve_reserved_variable(ReservedName, TreeWalker) ->
+    resolve_reserved_variable(ReservedName, merl:term(undefined), TreeWalker).
+
+resolve_reserved_variable(ReservedName, Default, TreeWalker) ->
+    case resolve_variable(ReservedName, Default, TreeWalker) of
+        {Src, Value} when Src =:= scope; Value =:= Default ->
+            {Value, TreeWalker};
+        _ ->
+            {Default, ?ERR({reserved_variable, ReservedName}, TreeWalker)}
+    end.
+
 format({{Ast, Info}, TreeWalker}) ->
     auto_escape({{format_number_ast(Ast), Info}, TreeWalker}).
 
@@ -1207,7 +1257,7 @@ with_ast(ArgList, Contents, TreeWalker) ->
 
     NewScope = lists:map(
                  fun ({{identifier, _, LocalVarName}, _Value}) ->
-                         {LocalVarName, merl:var(lists:concat(["Var_", LocalVarName]))}
+                         {LocalVarName, varname_ast(LocalVarName)}
                  end,
                  ArgList),
 
@@ -1223,7 +1273,7 @@ with_ast(ArgList, Contents, TreeWalker) ->
 
 scope_as(VarName, Contents, TreeWalker) ->
     {{ContentsAst, ContentsInfo}, TreeWalker1} = body_ast(Contents, TreeWalker),
-    VarAst = merl:var(lists:concat(["Var_", VarName])),
+    VarAst = varname_ast(VarName),
     {Id, TreeWalker2} = begin_scope(
                           {[{VarName, VarAst}],
                            [?Q("_@VarAst = _@ContentsAst")]},
@@ -1232,7 +1282,7 @@ scope_as(VarName, Contents, TreeWalker) ->
 
 regroup_ast(ListVariable, GrouperVariable, LocalVarName, TreeWalker) ->
     {{ListAst, ListInfo}, TreeWalker1} = value_ast(ListVariable, false, true, TreeWalker),
-    LocalVarAst = merl:var(lists:concat(["Var_", LocalVarName])),
+    LocalVarAst = varname_ast(LocalVarName),
 
     {Id, TreeWalker2} = begin_scope(
                           {[{LocalVarName, LocalVarAst}],
@@ -1271,10 +1321,8 @@ for_loop_ast(IteratorList, LoopValue, IsReversed, Contents,
     %% setup
     VarScope = lists:map(
                  fun({identifier, {R, C}, Iterator}) ->
-                         {Iterator, merl:var(
-                                      lists:concat(["Var_", Iterator,
-                                                    "/", Level, "_", R, ":", C
-                                                   ]))}
+                         {Iterator, varname_ast(lists:concat([
+                                    Iterator,"/", Level, "_", R, ":", C]))}
                  end, IteratorList),
     {Iterators, IteratorVars} = lists:unzip(VarScope),
     IteratorCount = length(IteratorVars),
@@ -1290,7 +1338,7 @@ for_loop_ast(IteratorList, LoopValue, IsReversed, Contents,
 
     LoopValueAst0 = to_list_ast(LoopValueAst, merl:term(IsReversed), TreeWalker2),
 
-    ParentLoop = resolve_variable('forloop', erl_syntax:atom(undefined), TreeWalker2),
+    {ParentLoop, TreeWalker3} = resolve_reserved_variable('forloop', TreeWalker2),
 
     %% call for loop
     {{?Q(["case erlydtl_runtime:forloop(",
@@ -1313,7 +1361,7 @@ for_loop_ast(IteratorList, LoopValue, IsReversed, Contents,
                               ?Q("() when true -> {_@Vars}")
                       end}]),
       merge_info(merge_info(Info, EmptyContentsInfo), LoopValueInfo)},
-     TreeWalker2}.
+     TreeWalker3}.
 
 ifchanged_values_ast(Values, {IfContentsAst, IfContentsInfo}, {ElseContentsAst, ElseContentsInfo}, TreeWalker) ->
     Info = merge_info(IfContentsInfo, ElseContentsInfo),
@@ -1355,10 +1403,10 @@ cycle_ast(Names, #treewalker{ context=Context }=TreeWalker) ->
                 (_, VarNamesAcc) ->
                     {[], VarNamesAcc}
             end, [], Names),
-    {{?Q("erlydtl_runtime:cycle({_@NamesTuple}, _@forloop)",
-        [{forloop, resolve_variable('forloop', TreeWalker)}]),
+    {ForLoop, TreeWalker1} = resolve_reserved_variable('forloop', TreeWalker),
+    {{?Q("erlydtl_runtime:cycle({_@NamesTuple}, _@ForLoop)"),
       #ast_info{ var_names = VarNames }},
-     TreeWalker}.
+     TreeWalker1}.
 
 %% Older Django templates treat cycle with comma-delimited elements as strings
 cycle_compat_ast(Names, #treewalker{ context=Context }=TreeWalker) ->
@@ -1366,10 +1414,10 @@ cycle_compat_ast(Names, #treewalker{ context=Context }=TreeWalker) ->
                    fun ({identifier, _, X}) ->
                            string_ast(X, Context)
                    end, Names),
-    {{?Q("erlydtl_runtime:cycle({_@NamesTuple}, _@forloop)",
-        [{forloop, resolve_variable('forloop', TreeWalker)}]),
+    {ForLoop, TreeWalker1} = resolve_reserved_variable('forloop', TreeWalker),
+    {{?Q("erlydtl_runtime:cycle({_@NamesTuple}, _@ForLoop)"),
       #ast_info{}},
-     TreeWalker}.
+     TreeWalker1}.
 
 now_ast(FormatString, TreeWalker) ->
     %% Note: we can't use unescape_string_literal here
@@ -1473,7 +1521,7 @@ create_scope(Vars, VarScope) ->
     {Scope, Values} =
         lists:foldl(
           fun ({Name, Value}, {VarAcc, ValueAcc}) ->
-                  NameAst = merl:var(lists:concat(["_Var_", Name, VarScope])),
+                  NameAst = varname_ast(lists:concat(["_", Name, VarScope])),
                   {[{Name, NameAst}|VarAcc],
                    [?Q("_@NameAst = _@Value")|ValueAcc]
                   }
@@ -1485,3 +1533,8 @@ create_scope(Vars, VarScope) ->
 create_scope(Vars, {Row, Col}, #treewalker{ context=Context }) ->
     Level = length(Context#dtl_context.local_scopes),
     create_scope(Vars, lists:concat(["/", Level, "_", Row, ":", Col])).
+
+varname_ast([$_|VarName]) ->
+    merl:var(lists:concat(["_Var__", VarName]));
+varname_ast(VarName) ->
+    merl:var(lists:concat(["Var_", VarName])).

+ 41 - 18
src/erlydtl_compiler.erl

@@ -92,6 +92,10 @@ format_error({read_file, File, Error}) ->
     io_lib:format(
       "Failed to include file ~s: ~s",
       [File, file:format_error(Error)]);
+format_error({deprecated_option, Opt, NewOpt}) ->
+    io_lib:format(
+      "Compile option '~s' has been deprecated. Use '~s' instead.",
+      [Opt, NewOpt]);
 format_error(Error) ->
     erlydtl_compiler_utils:format_error(Error).
 
@@ -103,7 +107,7 @@ format_error(Error) ->
 process_opts(File, Module, Options0) ->
     Options1 = proplists:normalize(
                  update_defaults(Options0),
-                 [{aliases, [{outdir, out_dir}]}
+                 [{aliases, deprecated_opts()}
                  ]),
     Source0 = filename:absname(
                 case File of
@@ -122,12 +126,32 @@ process_opts(File, Module, Options0) ->
     Source = shorten_filename(Source0),
     Options = [{compiler_options, [{source, Source}]}
                |compiler_opts(Options1, [])],
-    case File of
-        {dir, _} ->
-            init_context([], Source, Module, Options);
-        _ ->
-            init_context([Source], filename:dirname(Source), Module, Options)
-    end.
+    Context =
+        case File of
+            {dir, _} ->
+                init_context([], Source, Module, Options);
+            _ ->
+                init_context([Source], filename:dirname(Source), Module, Options)
+        end,
+
+    %% check original options here now that we have a context to
+    %% process any warnings/errors generated.
+    check_opts(Options0, Context).
+
+deprecated_opts() ->
+    [{outdir, out_dir},
+     {vars, default_vars},
+     {blocktrans_fun, translation_fun},
+     {blocktrans_locales, locales}].
+
+check_opts(Options, Context) ->
+    lists:foldl(
+      fun ({Opt, NewOpt}, Ctx) ->
+              case proplists:get_value(Opt, Options) of
+                  undefined -> Ctx;
+                  _ -> ?WARN({deprecated_option, Opt, NewOpt}, Ctx)
+              end
+      end, Context, deprecated_opts()).
 
 compiler_opts([CompilerOption|Os], Acc)
   when
@@ -217,14 +241,11 @@ init_context(ParseTrail, DefDir, Module, Options) when is_list(Module) ->
     init_context(ParseTrail, DefDir, list_to_atom(Module), Options);
 init_context(ParseTrail, DefDir, Module, Options) ->
     Ctx = #dtl_context{},
-    Locale = proplists:get_value(locale, Options),
-    BlocktransLocales = proplists:get_value(blocktrans_locales, Options),
-    TransLocales = case {Locale, BlocktransLocales} of
-                       {undefined, undefined} -> Ctx#dtl_context.trans_locales;
-                       {undefined, Val} -> Val;
-                       {Val, undefined} -> [Val];
-                       _ -> lists:usort([Locale | BlocktransLocales])
-                   end,
+    Locales = lists:usort(
+                lists:concat(
+                  [proplists:get_all_values(locale, Options),
+                   proplists:get_value(locales, Options, Ctx#dtl_context.trans_locales)]
+                 )),
     Context0 =
         #dtl_context{
            all_options = Options,
@@ -239,9 +260,11 @@ init_context(ParseTrail, DefDir, Module, Options) ->
            custom_tags_dir = proplists:get_value(
                                custom_tags_dir, Options,
                                filename:join([erlydtl_deps:get_base_dir(), "priv", "custom_tags"])),
-           trans_fun = proplists:get_value(blocktrans_fun, Options, Ctx#dtl_context.trans_fun),
-           trans_locales = TransLocales,
-           vars = proplists:get_value(vars, Options, Ctx#dtl_context.vars),
+           trans_fun = erlydtl_runtime:init_translation(
+                         proplists:get_value(translation_fun, Options, Ctx#dtl_context.trans_fun)),
+           trans_locales = Locales,
+           vars = proplists:get_value(default_vars, Options, Ctx#dtl_context.vars),
+           const = proplists:get_value(constants, Options, Ctx#dtl_context.const),
            reader = proplists:get_value(reader, Options, Ctx#dtl_context.reader),
            compiler_options = proplists:append_values(compiler_options, Options),
            binary_strings = proplists:get_value(binary_strings, Options, Ctx#dtl_context.binary_strings),

+ 26 - 32
src/erlydtl_compiler_utils.erl

@@ -187,38 +187,25 @@ call_extension(#dtl_context{ extension_module=Mod }, Fun, Args)
             undefined
     end.
 
-merge_info(Info1, Info2) ->
-    #ast_info{
-       dependencies =
-           lists:merge(
-             lists:sort(Info1#ast_info.dependencies),
-             lists:sort(Info2#ast_info.dependencies)),
-       var_names =
-           lists:merge(
-             lists:sort(Info1#ast_info.var_names),
-             lists:sort(Info2#ast_info.var_names)),
-       translatable_strings =
-           lists:merge(
-             lists:sort(Info1#ast_info.translatable_strings),
-             lists:sort(Info2#ast_info.translatable_strings)),
-       translated_blocks =
-           lists:merge(
-             lists:sort(Info1#ast_info.translated_blocks),
-             lists:sort(Info2#ast_info.translated_blocks)),
-       custom_tags =
-           lists:merge(
-             lists:sort(Info1#ast_info.custom_tags),
-             lists:sort(Info2#ast_info.custom_tags)),
-       pre_render_asts =
-           lists:merge(
-             Info1#ast_info.pre_render_asts,
-             Info2#ast_info.pre_render_asts)}.
+merge_info(Info1, Info2) when is_record(Info1, ast_info), is_record(Info2, ast_info) ->
+    merge_info1(record_info(size, ast_info), Info1, Info2, #ast_info{}).
 
 resolve_variable(VarName, TreeWalker) ->
     resolve_variable(VarName, undefined, TreeWalker).
 
 resolve_variable(VarName, Default, #treewalker{ context=Context }) ->
-    resolve_variable1(Context#dtl_context.local_scopes, VarName, Default).
+    case resolve_variable1(Context#dtl_context.local_scopes, VarName) of
+        undefined ->
+            case proplists:get_value(VarName, Context#dtl_context.const) of
+                undefined ->
+                    case proplists:get_value(VarName, Context#dtl_context.vars) of
+                        undefined -> {default, Default};
+                        Value -> {default_vars, Value}
+                    end;
+                Value -> {constant, Value}
+            end;
+        Value -> {scope, Value}
+    end.
 
 push_scope(Scope, #treewalker{ context=Context }=TreeWalker) ->
     TreeWalker#treewalker{ context=push_scope(Scope, Context) };
@@ -397,14 +384,21 @@ pos_info(Line) when is_integer(Line) ->
 pos_info({Line, Col}) when is_integer(Line), is_integer(Col) ->
     io_lib:format("~b:~b ", [Line, Col]).
 
-resolve_variable1([], _VarName, Default) -> Default;
-resolve_variable1([Scope|Scopes], VarName, Default) ->
-    case proplists:get_value(VarName, get_scope(Scope), Default) of
-        Default ->
-            resolve_variable1(Scopes, VarName, Default);
+resolve_variable1([], _VarName) -> undefined;
+resolve_variable1([Scope|Scopes], VarName) ->
+    case proplists:get_value(VarName, get_scope(Scope)) of
+        undefined ->
+            resolve_variable1(Scopes, VarName);
         Value -> Value
     end.
 
+merge_info1(1, _, _, Info) -> Info;
+merge_info1(FieldIdx, Info1, Info2, Info) ->
+    Value = lists:merge(
+              lists:sort(element(FieldIdx, Info1)),
+              lists:sort(element(FieldIdx, Info2))),
+    merge_info1(FieldIdx - 1, Info1, Info2, setelement(FieldIdx, Info, Value)).
+
 get_scope({_Id, Scope, _Values}) -> Scope;
 get_scope(Scope) -> Scope.
 

+ 47 - 22
src/erlydtl_parser.yrl

@@ -52,7 +52,7 @@ Nonterminals
     BlockBraced
     EndBlockBraced
 
-    CommentInline
+    CommentTag
     CommentBlock
     CommentBraced
     EndCommentBraced
@@ -103,6 +103,7 @@ Nonterminals
 
     CustomTag
     CustomArgs
+    Arg
     Args
 
     RegroupTag
@@ -112,9 +113,17 @@ Nonterminals
     SSITag
 
     BlockTransBlock
-    BlockTransContent
-    TransTag    
+    BlockTransBraced
+    EndBlockTransBraced
+    BlockTransArgs
+    BlockTransContents
+
+    PluralTag
+
+    TransTag
+    TransArgs
     TransText
+    TransValue
 
     TemplatetagTag
     Templatetag
@@ -143,8 +152,10 @@ Terminals
     call_keyword
     close_tag
     close_var
-    comment_inline
+    comment_tag
     comment_keyword
+    context_keyword
+    count_keyword
     cycle_keyword
     elif_keyword
     else_keyword
@@ -184,6 +195,7 @@ Terminals
     open_tag
     open_var
     parsed_keyword
+    plural_keyword
     regroup_keyword
     reversed_keyword
     spaceless_keyword
@@ -226,7 +238,7 @@ Elements -> Elements BlockTransBlock : '$1' ++ ['$2'].
 Elements -> Elements CallTag : '$1' ++ ['$2'].
 Elements -> Elements CallWithTag : '$1' ++ ['$2'].
 Elements -> Elements CommentBlock : '$1' ++ ['$2'].
-Elements -> Elements CommentInline : '$1' ++ ['$2'].
+Elements -> Elements CommentTag : '$1' ++ ['$2'].
 Elements -> Elements CustomTag : '$1' ++ ['$2'].
 Elements -> Elements CycleTag : '$1' ++ ['$2'].
 Elements -> Elements ExtendsTag : '$1' ++ ['$2'].
@@ -299,7 +311,7 @@ CommentBlock -> CommentBraced Elements EndCommentBraced : {comment, '$2'}.
 CommentBraced -> open_tag comment_keyword close_tag.
 EndCommentBraced -> open_tag endcomment_keyword close_tag.
 
-CommentInline -> comment_inline : {comment, inline_comment_to_string('$1')}.
+CommentTag -> comment_tag : '$1'.
 
 CycleTag -> open_tag cycle_keyword CycleNamesCompat close_tag : {cycle_compat, '$3'}.
 CycleTag -> open_tag cycle_keyword CycleNames close_tag : {cycle, '$3'}.
@@ -383,12 +395,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 -> 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 %}
+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'}|'$3'].
+BlockTransArgs -> context_keyword string_literal BlockTransArgs : [{context, '$2'}|'$3'].
+
+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'}.
 
@@ -401,12 +422,17 @@ Templatetag -> closebrace_keyword : '$1'.
 Templatetag -> opencomment_keyword : '$1'.
 Templatetag -> closecomment_keyword : '$1'.
 
-TransTag -> open_tag trans_keyword TransText close_tag : {trans, '$3'}.
-TransTag -> open_tag trans_keyword TransText as_keyword identifier close_tag : {scope_as, '$5', [{trans, '$3'}]}.
-TransTag -> open_tag trans_keyword TransText noop_keyword close_tag : '$3'.
+TransTag -> open_tag trans_keyword TransArgs close_tag : '$3'.
+TransTag -> open_tag trans_keyword TransArgs as_keyword identifier close_tag : {scope_as, '$5', ['$3']}.
 
-TransText -> string_literal : '$1'.
-TransText -> Variable : '$1'.
+TransArgs -> TransText : {trans, '$1'}.
+TransArgs -> TransText context_keyword string_literal: {trans, '$1', '$3'}.
+
+TransText -> TransValue : '$1'.
+TransText -> TransValue noop_keyword : {noop, '$1'}.
+
+TransValue -> string_literal : '$1'.
+TransValue -> Variable : '$1'.
 
 WidthRatioTag -> open_tag widthratio_keyword Value Value number_literal close_tag : {widthratio, '$3', '$4', '$5'}.
 
@@ -421,15 +447,14 @@ 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'}.
 
 Erlang code.
 
-inline_comment_to_string({comment_inline, Pos, S}) ->
-    %% inline comment converted to block comment for simplicity
-    [{string, Pos, S}].
-
 %% vim: syntax=erlang

+ 69 - 27
src/erlydtl_runtime.erl

@@ -2,7 +2,19 @@
 
 -compile(export_all).
 
--type translate_fun() :: fun((string() | binary()) -> string() | binary() | undefined).
+-type text() :: string() | binary().
+-type phrase() :: text() | {text(), {PluralPhrase::text(), non_neg_integer()}}.
+-type locale() :: term() | {Locale::term(), Context::binary()}.
+
+-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().
+
+-type init_translation() :: none
+                          | fun (() -> init_translation())
+                          | {M::atom(), F::atom()}
+                          | {M::atom(), F::atom(), A::list()}
+                          | translate_fun().
 
 -define(IFCHANGED_CONTEXT_VARIABLE, erlydtl_ifchanged_context).
 
@@ -98,8 +110,11 @@ find_value(Key, Tuple) when is_tuple(Tuple) ->
     end.
 
 fetch_value(Key, Data, Options) ->
+    fetch_value(Key, Data, Options, []).
+
+fetch_value(Key, Data, Options, Default) ->
     case find_value(Key, Data, Options) of
-        undefined -> [];
+        undefined -> Default;
         Val -> Val
     end.
 
@@ -127,36 +142,64 @@ 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 when
-      Str :: string() | binary().
-translate(String, none) -> String;
-translate(String, TranslationFun)
-  when is_function(TranslationFun) ->
-    case TranslationFun(String) of
-        undefined -> String;
-        <<"">> -> String;
-        "" -> String;
-        Str -> Str
+-spec init_translation(init_translation()) -> none | translate_fun().
+init_translation(none) -> none;
+init_translation(Fun) when is_function(Fun, 0) ->
+    init_translation(Fun());
+init_translation({M, F}) ->
+    init_translation({M, F, []});
+init_translation({M, F, A}) ->
+    init_translation(apply(M, F, A));
+init_translation(Fun)
+  when is_function(Fun, 1); is_function(Fun, 2) -> Fun;
+init_translation(Other) ->
+    throw({translation_fun, Other}).
+
+-spec translate(Phrase, Locale, Fun) -> iodata() | default when
+      Phrase :: phrase(),
+      Locale :: locale(),
+      Fun :: none | translate_fun().
+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
+        default -> Default;
+        <<"">> -> Default;
+        "" -> Default;
+        Translated ->
+            Translated
     end.
 
+trans_text({Text, _}) -> Text;
+trans_text(Text) -> Text.
+
+do_translate(Phrase, _Locale, TranslationFun)
+  when is_function(TranslationFun, 1) ->
+    TranslationFun(trans_text(Phrase));
+do_translate(Phrase, Locale, TranslationFun)
+  when is_function(TranslationFun, 2) ->
+    TranslationFun(Phrase, Locale).
+
 %% @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)
+-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
+                {no_close_var, T} ->
+                    io:format(standard_error, "Warning: template translation: variable not closed: \"~s\"~n", [T]),
+                    default;
+                _:_ -> default
+            end
     end.
 
 interpolate_variables(Tpl, []) ->
@@ -168,11 +211,10 @@ interpolate_variables(Tpl, Variables) ->
 interpolate_variables1(Tpl, Vars) ->
     %% pre-compile binary patterns?
     case binary:split(Tpl, <<"{{">>) of
-        [NotFound] ->
-            [NotFound];
+        [Tpl]=NoVars -> NoVars; %% need to enclose in list due to list tail call below..
         [Pre, Post] ->
             case binary:split(Post, <<"}}">>) of
-                [_] -> throw({no_close_var, Post});
+                [_] -> throw({no_close_var, Tpl});
                 [Var, Post1] ->
                     Var1 = string:strip(binary_to_list(Var)),
                     Value = orddict:fetch(Var1, Vars),

+ 8 - 6
src/erlydtl_scanner.erl

@@ -36,7 +36,7 @@
 %%%-------------------------------------------------------------------
 -module(erlydtl_scanner).
 
-%% This file was generated 2014-03-21 23:07:32 UTC by slex 0.2.1.
+%% This file was generated 2014-04-15 19:15:09 UTC by slex 0.2.1.
 %% http://github.com/erlydtl/slex
 -slex_source(["src/erlydtl_scanner.slex"]).
 
@@ -87,9 +87,11 @@ 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(any, "noop") -> true;
 is_keyword(close, "only") -> true;
 is_keyword(close, "parsed") -> true;
-is_keyword(close, "noop") -> true;
 is_keyword(close, "reversed") -> true;
 is_keyword(close, "openblock") -> true;
 is_keyword(close, "closeblock") -> true;
@@ -138,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}) ->
@@ -181,11 +184,10 @@ scan("#}" ++ T, S, {R, C}, {_, "#}"}) ->
 scan([H | T], S, {R, C} = P, {in_comment, E} = St) ->
     scan(T,
 	 case S of
-	   [{comment_inline, _, L} = M | Ss] ->
+	   [{comment_tag, _, L} = M | Ss] ->
 	       [setelement(3, M, [H | L]) | Ss];
 	   _ ->
-	       [{comment_inline, P, [H]} | post_process(S,
-							comment_inline)]
+	       [{comment_tag, P, [H]} | post_process(S, comment_tag)]
 	 end,
 	 case H of
 	   $\n -> {R + 1, 1};
@@ -540,7 +542,7 @@ post_process(_, {string, _, L} = T, _) ->
     setelement(3, T, begin L1 = lists:reverse(L), L1 end);
 post_process(_, {string_literal, _, L} = T, _) ->
     setelement(3, T, begin L1 = lists:reverse(L), L1 end);
-post_process(_, {comment_inline, _, L} = T, _) ->
+post_process(_, {comment_tag, _, L} = T, _) ->
     setelement(3, T, begin L1 = lists:reverse(L), L1 end);
 post_process(_, {number_literal, _, L} = T, _) ->
     setelement(3, T,

+ 7 - 4
src/erlydtl_scanner.slex

@@ -72,7 +72,7 @@ end.
 30 #} any+: skip, in_text-.
 
 %% must come before the `space any' rule
-40 any in_comment: +comment_inline.
+40 any in_comment: +comment_tag.
 %% end comment rules
 
 %% The rest is "just" text..
@@ -257,7 +257,7 @@ end.
 
 string: lists reverse.
 string_literal: lists reverse.
-comment_inline: lists reverse.
+comment_tag: lists reverse.
 number_literal: lists reverse, list_to_integer.
 open_var: to_atom.
 close_var: to_atom.
@@ -309,10 +309,12 @@ 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(any, "noop") -> true; \
   \
   is_keyword(close, "only") -> true; \
   is_keyword(close, "parsed") -> true; \
-  is_keyword(close, "noop") -> true; \
   is_keyword(close, "reversed") -> true; \
   is_keyword(close, "openblock") -> true; \
   is_keyword(close, "closeblock") -> true; \
@@ -362,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.
 
@@ -373,4 +376,4 @@ end.
 
 form format_where(in_comment) -> "in comment"; \
      format_where(in_code) -> "in code block" \
-end.
+end.

+ 26 - 5
src/erlydtl_unparser.erl

@@ -5,23 +5,29 @@ unparse(DjangoParseTree) ->
     unparse(DjangoParseTree, []).
 
 unparse([], Acc) ->
-    lists:reverse(Acc);
+    lists:flatten(lists:reverse(Acc));
 unparse([{'extends', Value}|Rest], Acc) ->
     unparse(Rest, [["{% extends ", unparse_value(Value), " %}"]|Acc]);
 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) ->
     unparse(Rest, [["{% call ", unparse_identifier(Identifier), " with ", unparse_args(With), " %}"]|Acc]);
 unparse([{'comment', Contents}|Rest], Acc) ->
     unparse(Rest, [["{% comment %}", unparse(Contents), "{% endcomment %}"]|Acc]);
+unparse([{'comment_tag', _Pos, Text}|Rest], Acc) ->
+    unparse(Rest, [["{#", Text, "#}"]|Acc]);
 unparse([{'cycle', Names}|Rest], Acc) ->
     unparse(Rest, [["{% cycle ", unparse(Names), " %}"]|Acc]);
 unparse([{'cycle_compat', Names}|Rest], Acc) ->
@@ -193,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).

+ 68 - 38
src/i18n/sources_parser.erl

@@ -38,8 +38,8 @@
 -include("include/erlydtl_ext.hrl").
 
 -record(phrase, {msgid :: string(),
-                 msgid_plural :: string() | undefined, %TODO
-                 context :: string() | undefined,      %TODO
+                 msgid_plural :: string() | undefined,
+                 context :: string() | undefined,
                  comment :: string() | undefined,
                  file :: string(),
                  line :: non_neg_integer(),
@@ -53,7 +53,7 @@
 -define(bail(Fmt, Args),
         throw(lists:flatten(io_lib:format(Fmt, Args)))).
 
--define(GET_FIELD(Name), phrase_info(Name, P) -> P#phrase.Name).
+-define(GET_FIELD(Key), phrase_info(Key, #phrase{ Key = Value }) -> Value).
 
 %%
 %% API Functions
@@ -73,19 +73,17 @@ process_content(Path, Content) ->
 %% @doc convert new API output to old one.
 -spec to_compat([phrase()]) -> [compat_phrase()].
 to_compat(Phrases) ->
-    Convert = fun(#phrase{msgid=Str, file=File, line=Line, col=Col}) ->
-                      {Str, {File, Line, Col}}
-              end,
-    lists:map(Convert, Phrases).
+    [{Str, {File, Line, Col}}
+     || #phrase{msgid=Str, file=File, line=Line, col=Col}
+            <- Phrases].
 
 %% New API
 
 %% @doc extract info about phrase.
 %% See `field()' type for list of available info field names.
--spec phrase_info([field()] | field(), phrase()) -> [Info] | Info
-                                                        when
+-spec phrase_info([field()] | field(), phrase()) -> [Info] | Info when
       Info :: non_neg_integer() | string() | undefined.
-?GET_FIELD(msgid);                                  %little magick
+?GET_FIELD(msgid);
 ?GET_FIELD(msgid_plural);
 ?GET_FIELD(context);
 ?GET_FIELD(comment);
@@ -94,16 +92,16 @@ to_compat(Phrases) ->
 ?GET_FIELD(col);
 phrase_info(Fields, Phrase) when is_list(Fields) ->
     %% you may pass list of fields
-    lists:map(fun(Field) -> phrase_info(Field, Phrase) end, Fields).
+    [phrase_info(Field, Phrase) || Field <- Fields].
 
 %% @doc list files, using wildcard and extract phrases from them
 -spec parse_pattern([string()]) -> [phrase()].
 parse_pattern(Pattern) ->
     %%We assume a basedir
-    GetFiles = fun(Path,Acc) -> Acc ++ filelib:wildcard(Path) end,
+    GetFiles = fun(Path,Acc) -> Acc ++ [F || F <- filelib:wildcard(Path), filelib:is_regular(F)] end,
     Files = lists:foldl(GetFiles,[],Pattern),
     io:format("Parsing files ~p~n",[Files]),
-    ParsedFiles = lists:map(fun(File)-> parse_file(File) end, Files),
+    ParsedFiles = [parse_file(File) || File <- Files],
     lists:flatten(ParsedFiles).
 
 %% @doc extract phrases from single file
@@ -120,8 +118,13 @@ parse_file(Path) ->
 parse_content(Path,Content)->
     case erlydtl_compiler:do_parse_template(Content, #dtl_context{}) of
         {ok, Data} ->
-            {ok, Result} = process_ast(Path, Data),
-            Result;
+            try process_ast(Path, Data) of
+                {ok, Result} -> Result
+            catch
+                Error:Reason ->
+                    io:format("~s: Template processing failed~nData: ~p~n", [Path, Data]),
+                    erlang:raise(Error, Reason, erlang:get_stacktrace())
+            end;
         Error ->
             ?bail("Template parsing failed for template ~s, cause ~p~n", [Path, Error])
     end.
@@ -132,26 +135,48 @@ parse_content(Path,Content)->
 %%
 
 process_ast(Fname, Tokens) ->
-    {ok, (process_ast(Fname, Tokens, #state{}))#state.acc }.
-process_ast(_Fname, [], St) -> St;
-process_ast(Fname,[Head|Tail], St) ->
-    NewSt = process_token(Fname,Head,St),
-    process_ast(Fname, Tail, NewSt).
+    State = process_ast(Fname, Tokens, #state{}),
+    {ok, State#state.acc}.
+
+process_ast(Fname, Tokens, State) when is_list(Tokens) ->
+    lists:foldl(
+      fun (Token, St) ->
+              process_token(Fname, Token, St)
+      end, State, Tokens);
+process_ast(Fname, Token, State) ->
+    process_token(Fname, Token, State).
+
 
 %%Block are recursivelly processed, trans are accumulated and other tags are ignored
 process_token(Fname, {block,{identifier,{_Line,_Col},_Identifier},Children}, St) -> process_ast(Fname, Children, St);
-process_token(Fname, {trans,{string_literal,{Line,Col},String}}, #state{acc=Acc, translators_comment=Comment}=St) ->
+process_token(Fname, {trans,Text}, #state{acc=Acc, translators_comment=Comment}=St) ->
+    {{Line, Col}, String} = trans(Text),
     Phrase = #phrase{msgid=unescape(String),
                      comment=Comment,
                      file=Fname,
                      line=Line,
                      col=Col},
     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,
+              {trans,Text,{string_literal, _, Context}},
+              #state{acc=Acc, translators_comment=Comment}=St) ->
+    {{Line, Col}, String} = trans(Text),
+    Phrase = #phrase{msgid=unescape(String),
+                     context=unescape(Context),
+                     comment=Comment,
+                     file=Fname,
+                     line=Line,
+                     col=Col},
+    St#state{acc=[Phrase | Acc], translators_comment=undefined};
+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=lists:flatten(erlydtl_unparser:unparse(Contents)),
+    Phrase = #phrase{msgid=unparse(Contents),
+                     msgid_plural=unparse(PluralContents),
+                     context=case proplists:get_value(context, Args) of
+                                 {string_literal, _, String} ->
+                                     erlydtl_compiler_utils:unescape_string_literal(String);
+                                 undefined -> undefined
+                             end,
                      comment=Comment,
                      file=Fname,
                      line=Line,
@@ -159,13 +184,22 @@ process_token(Fname, {blocktrans, Args, Contents}, #state{acc=Acc, translators_c
     St#state{acc=[Phrase | Acc], translators_comment=undefined};
 process_token(_, {comment, Comment}, St) ->
     St#state{translators_comment=maybe_translators_comment(Comment)};
+process_token(_Fname, {comment_tag, _Pos, Comment}, St) ->
+    St#state{translators_comment=translators_comment_text(Comment)};
 process_token(Fname, {_Instr, _Cond, Children}, St) -> process_ast(Fname, Children, St);
 process_token(Fname, {_Instr, _Cond, Children, Children2}, St) ->
     StModified = process_ast(Fname, Children, St),
     process_ast(Fname, Children2, StModified);
 process_token(_,_AST,St) -> St.
 
-unescape(String) ->string:sub_string(String, 2, string:len(String) -1).
+trans({noop, Value}) ->
+    trans(Value);
+trans({string_literal,Pos,String}) -> {Pos, String}.
+
+unescape(String) -> string:sub_string(String, 2, string:len(String) -1).
+
+unparse(undefined) -> undefined;
+unparse(Contents) -> erlydtl_unparser:unparse(Contents).
 
 %% hack to guess ~position of blocktrans
 guess_blocktrans_lc(Fname, [{{identifier, {L, C}, _}, _} | _], _) ->
@@ -182,19 +216,15 @@ guess_blocktrans_lc(Fname, _, _) ->
 
 
 maybe_translators_comment([{string, _Pos, S}]) ->
-    %% fast path
-    case is_translators(S) of
-        true -> S;
-        false -> undefined
-    end;
+    translators_comment_text(S);
 maybe_translators_comment(Other) ->
     %% smth like "{%comment%}Translators: Hey, {{var}} is variable substitution{%endcomment%}"
-    Unparsed = lists:flatten(erlydtl_unparser:unparse(Other)),
-    case is_translators(Unparsed) of
-        true -> Unparsed;
-        false -> undefined
-    end.
+    Unparsed = erlydtl_unparser:unparse(Other),
+    translators_comment_text(Unparsed).
 
-is_translators(S) ->
+translators_comment_text(S) ->
     Stripped = string:strip(S, left),
-    "translators:" == string:to_lower(string:substr(Stripped, 1, 12)).
+    case "translators:" == string:to_lower(string:substr(Stripped, 1, 12)) of
+        true -> S;
+        false -> undefined
+    end.

+ 9 - 2
test/erlydtl_eunit_testrunner.erl

@@ -47,10 +47,17 @@ run_test(T) ->
         error_ok -> ok
     end.
 
+compile_opts(#test{ compile_vars = undefined, compile_opts = Opts }) ->
+    Opts;
+compile_opts(#test{ compile_vars = Vars, compile_opts = Opts }) ->
+    [{default_vars, Vars}|Opts].
+
 run_compile(T) ->
     case erlydtl:compile(
-           T#test.source, T#test.module,
-           [{vars, T#test.compile_vars}|T#test.compile_opts]) of
+           T#test.source,
+           T#test.module,
+           compile_opts(T))
+    of
         {ok, M, W} ->
             ?assertEqual(T#test.module, M),
             ?assertEqual(T#test.warnings, W);

+ 174 - 5
test/erlydtl_test_defs.erl

@@ -311,6 +311,13 @@ all_test_defs() ->
         <<"{% for outer in list %}{% for inner in outer %}{{ inner }}\n{% endfor %}{% endfor %}">>,
         [{'list', [["Al", "Albert"], ["Jo", "Joseph"]]}],
         <<"Al\nAlbert\nJo\nJoseph\n">>},
+       {"Unused variable in foreach proplist",
+        <<"{% for k,v in plist %}{{v}}{% endfor %}">>,
+        [{'plist',[{1,"one"},{2,"two"}]}], [], [], <<"onetwo">>,
+        [error_info([{0, erl_lint, {unused_var, 'Var_k/1_1:8'}}])]},
+       {"Unused variable in foreach proplist, prefixed with underscore",
+        <<"{% for _k,v in plist %}{{v}}{% endfor %}">>,
+        [{'plist',[{1,"one"},{2,"two"}]}], [], [], <<"onetwo">>},
        {"Access parent loop counters",
         <<"{% for outer in list %}{% for inner in outer %}({{ forloop.parentloop.counter0 }}, {{ forloop.counter0 }})\n{% endfor %}{% endfor %}">>,
         [{'list', [["One", "two"], ["One", "two"]]}], [], [], <<"(0, 0)\n(0, 1)\n(1, 0)\n(1, 1)\n">>,
@@ -1273,7 +1280,7 @@ all_test_defs() ->
        },
        {"trans functional reverse locale",
         <<"Hello {% trans \"Hi\" %}">>, [], [{locale, "reverse"}],
-        [{blocktrans_locales, ["reverse"]}, {blocktrans_fun, fun("Hi"=Key, "reverse") -> list_to_binary(lists:reverse(Key)) end}],
+        [{locales, ["reverse"]}, {translation_fun, fun("Hi"=Key, "reverse") -> list_to_binary(lists:reverse(Key)) end}],
         <<"Hello iH">>
        },
        {"trans literal at run-time",
@@ -1289,15 +1296,18 @@ all_test_defs() ->
         <<"Hello {% trans var1 noop %}">>, [{var1, <<"Hi">>}], [{translation_fun, fun(<<"Hi">>) -> <<"Konichiwa">> end}], [],
         <<"Hello Hi">>},
        {"trans as",
-        <<"{% trans 'Hans' as name %}Hello {{ name }}">>, [], <<"Hello Hans">>
-       }
+        <<"{% trans 'Hans' as name %}Hello {{ name }}">>, [],
+        <<"Hello Hans">>},
+       {"trans value",
+        <<"{{ _('foo') }}">>, [], [], [{locale, default}, {translation_fun, fun ("foo") -> "bar" end}],
+        <<"bar">>}
       ]},
      {"blocktrans",
       [{"blocktrans default locale",
         <<"{% blocktrans %}Hello{% endblocktrans %}">>, [], <<"Hello">>},
        {"blocktrans choose locale",
         <<"{% 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">>},
+        [{locales, ["de"]}, {translation_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">>},
        #test{
@@ -1316,6 +1326,98 @@ all_test_defs() ->
         [{translation_fun, fun("Hello, {{ name }}! See {{ v1 }}.") -> <<"Guten tag, {{name}}! Sehen {{    v1   }}.">> end}],
         [], <<"Guten tag, Mr. President! Sehen rubber-duck.">>}
       ]},
+     {"extended translation features (#131)",
+      [{"trans default locale",
+        <<"test {% trans 'message' %}">>,
+        [], [{translation_fun, fun ("message", default) -> "ok" end}],
+        <<"test ok">>},
+       {"trans foo locale",
+        <<"test {% trans 'message' %}">>,
+        [], [{locale, "foo"}, {translation_fun, fun ("message", "foo") -> "ok" end}],
+        <<"test ok">>},
+       {"trans context (run-time)",
+        <<"test {% trans 'message' context 'foo' %}">>,
+        [], [{translation_fun, fun ("message", {default, "foo"}) -> "ok" end}],
+        <<"test ok">>},
+       {"trans context (compile-time)",
+        <<"test {% trans 'message' context 'foo' %}">>,
+        [], [{locale, "baz"}],
+        [{locales, ["bar", "baz"]},
+         {translation_fun, fun ("message", {L, "foo"}) ->
+                                  case L of
+                                      "bar" -> "rab";
+                                      "baz" -> "ok"
+                                  end
+                          end}],
+        <<"test ok">>},
+       {"trans context noop",
+        <<"{% trans 'message' noop context 'foo' %}">>, [], [],
+        <<"message">>},
+       {"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"}, {translation_fun,
+                           fun ("translate this", {"foo", "bar"}) ->
+                                   "got it"
+                           end}],
+        <<"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">>},
+       {"blocktrans a lot of stuff",
+        <<"{% blocktrans with foo=a.b count c=a|length context 'quux' %}"
+          "foo={{ foo }};bar={{ bar }};c={{ c }}:"
+          "{% plural %}"
+          "FOO:{{ foo }},BAR:{{ bar }},C:{{ c }}."
+          "{% endblocktrans %}">>,
+        [{a, [{b, "B"}]}, {bar, "BAR"}],
+        [{locale, "rub"},
+         {translation_fun, fun ({Single, {Plural, "1"=_Count}}, {Locale, Context}) ->
+                                   [Single, Plural, Locale, Context]
+                           end}],
+        <<"foo=B;bar=BAR;c=1:"
+          "FOO:B,BAR:BAR,C:1."
+          "rub" "quux">>},
+       {"new translation options",
+        <<"{% trans foo %}{% blocktrans %}abc{% endblocktrans %}">>,
+        [{foo, "1234"}], [{locale, "test"}, {translation_fun, fun (Msg) -> lists:reverse(Msg) end}],
+        [{locale, "foo"}, {locale, "test"}, {locales, ["bar", "baz"]},
+         {translation_fun, fun (Msg, _) -> [Msg, lists:reverse(Msg)] end}],
+        <<"4321" "abccba">>}
+
+       %% This does work, but always prints a warning to std err.. :/
+       %% Warning: template translation: variable not closed: "bar {{ 123"
+       %% {"variable error",
+       %%  <<"{% blocktrans %}foo{{ bar }}{% endblocktrans %}">>,
+       %%  [], [{translation_fun, fun (_) -> "bar {{ 123" end}],
+       %%  <<"foo">>}
+      ]},
+     {"i18n",
+      [{"setup translation context, using fun, at render time",
+        <<"{% trans 'foo' %}">>, [],
+        [{translation_fun, fun () -> fun (Msg) -> string:to_upper(Msg) end end}],
+        <<"FOO">>},
+       {"setup translation context, using fun, at compile time",
+        <<"{% trans 'foo' %}">>, [], [],
+        [{locale, default}, {translation_fun, fun () -> fun lists:reverse/1 end}],
+        <<"oof">>}
+      ]},
      {"verbatim",
       [{"Plain verbatim",
         <<"{% verbatim %}{{ oh no{% foobar %}{% endverbatim %}">>, [],
@@ -1459,6 +1561,15 @@ all_test_defs() ->
        {"ssi file not found",
         <<"{% ssi 'foo' %}">>, [],
         {error, {read_file, <<"./foo">>, enoent}}
+       },
+       {"deprecated compile options",
+        <<"">>, [], [],
+        [{blocktrans_locales, []}, {blocktrans_fun, fun (_) -> [] end}],
+        <<"">>,
+        [error_info([{deprecated_option, O, N}
+                     || {O, N} <- [{blocktrans_locales, locales},
+                                   {blocktrans_fun, translation_fun}]],
+                    erlydtl_compiler)]
        }
       ]},
      {"load",
@@ -1479,6 +1590,62 @@ all_test_defs() ->
         <<"ytrewQ">>
        }
       ]},
+     {"compile time default vars/constants",
+      begin
+          Tpl = <<"Test {{ var1 }}:{{ var2 }}.">>,
+          Txt = <<"Test 123:abc.">>,
+          Fun = fun (F) ->
+                        fun (#test{ module=M }) ->
+                                M:F()
+                        end
+                end,
+          [{"default vars",
+            Tpl, [], [],
+            [{default_vars, [{var1, 123}, {var2, abc}]}], Txt},
+           {"default vars (using fun)",
+            Tpl, [], [],
+            [{default_vars, [{var1, 123}, {var2, fun () -> abc end}]}], Txt},
+           {"override default vars",
+            Tpl, [{var2, abc}], [],
+            [{default_vars, [{var1, 123}, {var2, 456}]}], Txt},
+           {"constants",
+            Tpl, [], [],
+            [{constants, [{var1, 123}, {var2, abc}]}], Txt},
+           {"constants (using fun)",
+            Tpl, [], [],
+            [{constants, [{var1, 123}, {var2, fun () -> abc end}]}], Txt},
+           {"constants non-overridable",
+            Tpl, [{var1, ohno}, {var2, noway}], [],
+            [{constants, [{var1, 123}, {var2, "abc"}]}], Txt}
+           |[#test{ title = T,
+                    source = Tpl,
+                    compile_vars = undefined,
+                    compile_opts = CO ++ (#test{})#test.compile_opts,
+                    renderer = Fun(F),
+                    output = O
+                  }
+             || {T, F, O, CO} <-
+                    [{"variables/0",
+                      variables, [var1, var2], []},
+                     {"variables/0 w. defaults",
+                      variables, [var1, var2], [{default_vars, [{var1, aaa}]}]},
+                     {"variables/0 w. constants",
+                      variables, [var2], [{constants, [{var1, bbb}]}]},
+                     {"default_variables/0",
+                      default_variables, [], []},
+                     {"default_variables/0 w. defaults",
+                      default_variables, [var1], [{default_vars, [{var1, aaa}]}]},
+                     {"default_variables/0 w. constants",
+                      default_variables, [], [{constants, [{var1, bbb}]}]},
+                     {"constants/0",
+                      constants, [], []},
+                     {"constants/0 w. defaults",
+                      constants, [], [{default_vars, [{var1, aaa}]}]},
+                     {"constants/0 w. constants",
+                      constants, [var1], [{constants, [{var1, bbb}]}]}
+                    ]
+            ]]
+      end},
      {"functional",
       [functional_test(F)
        %% order is important.
@@ -1507,7 +1674,8 @@ all_test_defs() ->
                    renderer = fun(#test{ module=M, render_vars=V, render_opts=O }) ->
                                       M:render(base1, V, O)
                               end
-                  }]
+                  }
+               ]
       ]}
     ].
 
@@ -1530,6 +1698,7 @@ def_to_test(Group, {Name, DTL, Vars, RenderOpts, CompilerOpts, Output, Warnings}
        source = {template, DTL},
        render_vars = Vars,
        render_opts = RenderOpts,
+       compile_vars = undefined,
        compile_opts = CompilerOpts ++ (#test{})#test.compile_opts,
        output = Output,
        warnings = Warnings

+ 156 - 6
test/sources_parser_tests.erl

@@ -1,15 +1,12 @@
 -module(sources_parser_tests).
 
 -include_lib("eunit/include/eunit.hrl").
+-include("include/erlydtl_ext.hrl").
 
 all_sources_parser_test_() ->
     [{Title, [test_fun(Test) || Test <- Tests]}
      || {Title, Tests} <- test_defs()].
 
-all_sources_parser_ext_test_() ->
-    [test_ext_fun(Test) || Test <- ext_test_defs()].
-
-
 test_fun({Name, Content, Output}) ->
     {Name, fun () ->
                    Tokens = (catch sources_parser:process_content("dummy_path", Content)),
@@ -35,11 +32,14 @@ test_defs() ->
         [{"Hello inside an if inside a for",{"dummy_path",1,73}}]},
        {"if and else both with trans",
         <<"<html>{% block content %}{% if thing %} {% trans \"Hello inside an if\" %} {% else %} {% trans \"Hello inside an else\" %} {% endif %} {% endblock %}</html>">>,
-        [ {"Hello inside an else",{"dummy_path",1,94}}, {"Hello inside an if",{"dummy_path",1,50}}]}
+        [{"Hello inside an else",{"dummy_path",1,94}}, {"Hello inside an if",{"dummy_path",1,50}}]}
       ]}
     ].
 
 
+all_sources_parser_ext_test_() ->
+    [test_ext_fun(Test) || Test <- ext_test_defs()].
+
 test_ext_fun({Name, Tpl, {Fields, Output}}) ->
     {Name, fun() ->
                    Tokens = [sources_parser:phrase_info(Fields, P)
@@ -56,5 +56,155 @@ ext_test_defs() ->
       {[msgid, comment], [["phrase", "translators: com{{ me }}nt"]]}},
      {"blocktrans with comments",
       <<"{%comment%}translators: comment{%endcomment%}{%blocktrans with a=b%}B={{b}}{%endblocktrans%}">>,
-      {[msgid, comment], [["B={{ b }}", "translators: comment"]]}}
+      {[msgid, comment], [["B={{ b }}", "translators: comment"]]}},
+     {"blocktrans with context",
+      <<"{%blocktrans context 'ctxt'%}msg{%endblocktrans%}">>,
+      {[msgid, context], [["msg", "ctxt"]]}},
+     {"blocktrans with plural form",
+      <<"{%blocktrans%}msg{%plural%}msgs{%endblocktrans%}">>,
+      {[msgid, msgid_plural], [["msg", "msgs"]]}},
+     {"trans with context",
+      <<"{% trans 'msg' context 'ctxt' %}">>,
+      {[msgid, context], [["msg", "ctxt"]]}},
+     {"trans noop",
+      <<"{% trans 'msg' noop %}">>,
+      {[msgid], [["msg"]]}},
+     {"trans noop with context",
+      <<"{% trans 'msg' noop context 'ctxt' %}">>,
+      {[msgid, context], [["msg", "ctxt"]]}}
+    ].
+
+unparser_test_() ->
+    [test_unparser_fun(Test) || Test <- unparser_test_defs()].
+
+test_unparser_fun({Name, Tpl}) ->
+    {Name, fun() ->
+                   %% take input Tpl value, parse it, "unparse" it, then parse it again.
+                   %% the both parsed values should be equvialent, even if the source versions
+                   %% are not an exact match (there can be whitespace differences)
+                   {ok, Dpt} = erlydtl_compiler:do_parse_template(
+                                 Tpl, #dtl_context{}),
+                   Unparsed = erlydtl_unparser:unparse(Dpt),
+                   {ok, DptU} = erlydtl_compiler:do_parse_template(
+                                 Unparsed, #dtl_context{}),
+                   compare_tree(Dpt, DptU)
+           end}.
+
+unparser_test_defs() ->
+    [{"comment tag", <<"here it is: {# this is my comment #} <-- it was right there.">>}
     ].
+
+
+compare_tree([], []) -> ok;
+compare_tree([H1|T1], [H2|T2]) ->
+    compare_token(H1, H2),
+    compare_tree(T1, T2).
+
+compare_token({'extends', Value1}, {'extends', Value2}) ->
+    ?assertEqual(Value1, Value2);
+compare_token({'autoescape', OnOrOff1, Contents1}, {'autoescape', OnOrOff2, Contents2}) ->
+    ?assertEqual(OnOrOff1, OnOrOff2),
+    compare_tree(Contents1, Contents2);
+compare_token({'block', Identifier1, Contents1}, {'block', Identifier2, Contents2}) ->
+    compare_identifier(Identifier1, Identifier2),
+    compare_tree(Contents1, Contents2);
+compare_token({'blocktrans', Args1, Contents1}, {'blocktrans', Args2, Contents2}) ->
+    compare_args(Args1, Args2),
+    compare_tree(Contents1, Contents2);
+compare_token({'call', Identifier1}, {'call', Identifier2}) ->
+    compare_identifier(Identifier1, Identifier2);
+compare_token({'call', Identifier1, With1}, {'call', Identifier2, With2}) ->
+    ?assertEqual(With1, With2),
+    compare_identifier(Identifier1, Identifier2);
+compare_token({'comment', Contents1}, {'comment', Contents2}) ->
+    compare_tree(Contents1, Contents2);
+compare_token({'comment_tag', _Pos, Text1}, {'comment_tag', _Pos, Text2}) ->
+    ?assertEqual(Text1, Text2);
+compare_token({'cycle', Names1}, {'cycle', Names2}) ->
+    compare_tree(Names1, Names2);
+compare_token({'cycle_compat', Names1}, {'cycle_compat', Names2}) ->
+    compare_cycle_compat_names(Names1, Names2);
+compare_token({'date', 'now', Value1}, {'date', 'now', Value2}) ->
+    compare_value(Value1, Value2);
+compare_token({'filter', FilterList1, Contents1}, {'filter', FilterList2, Contents2}) ->
+    compare_filters(FilterList1, FilterList2),
+    compare_tree(Contents1, Contents2);
+compare_token({'firstof', Vars1}, {'firstof', Vars2}) ->
+    compare_tree(Vars1, Vars2);
+%% TODO...
+%% compare_token({'for', {'in', IteratorList, Identifier}, Contents}, {'for', {'in', IteratorList, Identifier}, Contents}) -> ok;
+%% compare_token({'for', {'in', IteratorList, Identifier}, Contents, EmptyPartsContents}, {'for', {'in', IteratorList, Identifier}, Contents, EmptyPartsContents}) -> ok;
+compare_token({'if', Expression1, Contents1}, {'if', Expression2, Contents2}) ->
+    compare_expression(Expression1, Expression2),
+    compare_tree(Contents1, Contents2);
+%% compare_token({'ifchanged', Expression, IfContents}, {'ifchanged', Expression, IfContents}) -> ok;
+%% compare_token({'ifchangedelse', Expression, IfContents, ElseContents}, {'ifchangedelse', Expression, IfContents, ElseContents}) -> ok;
+%% compare_token({'ifelse', Expression, IfContents, ElseContents}, {'ifelse', Expression, IfContents, ElseContents}) -> ok;
+%% compare_token({'ifequal', [Arg1, Arg2], Contents}, {'ifequal', [Arg1, Arg2], Contents}) -> ok;
+%% compare_token({'ifequalelse', [Arg1, Arg2], IfContents, ElseContents}, {'ifequalelse', [Arg1, Arg2], IfContents, ElseContents}) -> ok;
+%% compare_token({'ifnotequal', [Arg1, Arg2], Contents}, {'ifnotequal', [Arg1, Arg2], Contents}) -> ok;
+%% compare_token({'ifnotequalelse', [Arg1, Arg2], IfContents, ElseContents}, {'ifnotequalelse', [Arg1, Arg2], IfContents, ElseContents}) -> ok;
+%% compare_token({'include', Value, []}, {'include', Value, []}) -> ok;
+%% compare_token({'include', Value, Args}, {'include', Value, Args}) -> ok;
+%% compare_token({'include_only', Value, []}, {'include_only', Value, []}) -> ok;
+%% compare_token({'include_only', Value, Args}, {'include_only', Value, Args}) -> ok;
+%% compare_token({'regroup', {Variable, Identifier1, Identifier2}, Contents}, {'regroup', {Variable, Identifier1, Identifier2}, Contents}) -> ok;
+%% compare_token({'spaceless', Contents}, {'spaceless', Contents}) -> ok;
+%% compare_token({'ssi', Arg}, {'ssi', Arg}) -> ok;
+%% compare_token({'ssi_parsed', Arg}, {'ssi_parsed', Arg}) -> ok;
+compare_token({'string', _, String1}, {'string', _, String2}) ->
+    ?assertEqual(String1, String2);
+%% compare_token({'tag', Identifier, []}, {'tag', Identifier, []}) -> ok;
+%% compare_token({'tag', Identifier, Args}, {'tag', Identifier, Args}) -> ok;
+%% compare_token({'templatetag', Identifier}, {'templatetag', Identifier}) -> ok;
+%% compare_token({'trans', Value}, {'trans', Value}) -> ok;
+%% compare_token({'widthratio', Numerator, Denominator, Scale}, {'widthratio', Numerator, Denominator, Scale}) -> ok;
+%% compare_token({'with', Args, Contents}, {'with', Args, Contents}) -> ok;
+compare_token(ValueToken1, ValueToken2) ->
+    compare_value(ValueToken1, ValueToken2).
+
+compare_identifier({identifier, _, Name1}, {identifier, _, Name2}) ->
+    ?assertEqual(Name1, Name2).
+
+compare_filters(FilterList1, FilterList2) ->
+    [compare_filter(F1, F2)
+     || {F1, F2} <- lists:zip(FilterList1, FilterList2)].
+
+compare_filter([Identifier1], [Identifier2]) ->
+    compare_identifier(Identifier1, Identifier2);
+compare_filter([Identifier1, Arg1], [Identifier2, Arg2]) ->
+    compare_identifier(Identifier1, Identifier2),
+    compare_value(Arg1, Arg2).
+
+compare_expression({'expr', _, Arg11, Arg12}, {'expr', _, Arg21, Arg22}) ->
+    compare_value(Arg11, Arg21),
+    compare_value(Arg12, Arg22);
+compare_expression({'expr', "not", Expr1}, {'expr', "not", Expr2}) ->
+    compare_expression(Expr1, Expr2);
+compare_expression(Other1, Other2) ->
+    compare_value(Other1, Other2).
+
+compare_value({'string_literal', _, Value1}, {'string_literal', _, Value2}) ->
+    ?assertEqual(Value1, Value2);
+compare_value({'number_literal', _, Value1}, {'number_literal', _, Value2}) ->
+    ?assertEqual(Value1, Value2);
+compare_value({'apply_filter', Variable1, Filter1}, {'apply_filter', Variable2, Filter2}) ->
+    compare_value(Variable1, Variable2),
+    compare_filter(Filter1, Filter2);
+compare_value({'attribute', {Variable1, Identifier1}}, {'attribute', {Variable2, Identifier2}}) ->
+    compare_value(Variable1, Variable2),
+    compare_identifier(Identifier1, Identifier2);
+compare_value({'variable', Identifier1}, {'variable', Identifier2}) ->
+    compare_identifier(Identifier1, Identifier2).
+
+compare_args(Args1, Args2) ->
+    [compare_arg(A1, A2)
+     || {A1, A2} <- lists:zip(Args1, Args2)].
+
+compare_arg({{identifier, _, Name1}, Value1}, {{identifier, _, Name2}, Value2}) ->
+    ?assertEqual(Name1, Name2),
+    compare_value(Value1, Value2).
+
+compare_cycle_compat_names(Names1, Names2) ->
+    [compare_identifier(N1, N2)
+     || {N1, N2} <- lists:zip(Names1, Names2)].