Browse Source

Release 0.9.4

Merge branch 'master' into stable

Conflicts:
	NEWS.md
	rebar.config
Andreas Stenius 11 years ago
parent
commit
6280d21a6d

+ 10 - 2
.travis.yml

@@ -1,11 +1,19 @@
 language: erlang
 language: erlang
 otp_release:
 otp_release:
-#  - 17.0-rc1
+# Test on all supported releases accepted by the `require_otp_vsn` in rebar.config
+  - 17.0
   - R16B03-1
   - R16B03-1
+#  - R16B03 this version is broken!
   - R16B02
   - R16B02
+  - R16B01
+  - R16B
+#  - R15B03-1 not available on travis
   - R15B03
   - R15B03
-#  - R14B04 (seems lists:concat/1 is broken on R14B04..)
+  - R15B02
 
 
 # since Travis is naughty and calls rebar get-deps behind our backs,
 # since Travis is naughty and calls rebar get-deps behind our backs,
 # we'll have to clean it up and build merl our selves..
 # we'll have to clean it up and build merl our selves..
 script: "make -C deps/merl && make tests"
 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).
 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)
 ## 0.9.3 (2014-03-27)
 
 
 * Fix release process to work for non-git installations (#154).
 * 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.
 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
 Template compilation
 --------------------
 --------------------
 
 
@@ -93,22 +99,14 @@ Options is a proplist possibly containing:
 * `binary_strings` - Whether to compile strings as binary terms
 * `binary_strings` - Whether to compile strings as binary terms
   (rather than lists). Defaults to `true`.
   (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_options` - Proplist with extra options passed directly to
   `compiler:forms/2`. This can prove useful when using extensions to
   `compiler:forms/2`. This can prove useful when using extensions to
   add extra defines etc when compiling the generated code.
   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
 * `custom_filters_modules` **deprecated** - A list of modules to be
   used for handling custom filters. The modules will be searched in
   used for handling custom filters. The modules will be searched in
   order and take precedence over the built-in filters. Each custom
   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
   by name (when there is a name to module mapping also provided in the
   `libraries` option) or by module.
   `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
 * `doc_root` - Included template paths will be relative to this
   directory; defaults to the compiled template's directory.
   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
   decide until render time, using the render option
   `lists_0_based`. See also `tuples_0_based`.
   `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
 * `no_env` - Do not read additional options from the OS environment
   variable `ERLYDTL_COMPILER_OPTIONS`.
   variable `ERLYDTL_COMPILER_OPTIONS`.
@@ -221,6 +229,36 @@ Options is a proplist possibly containing:
 
 
 * `report_errors` - Print errors as they occur.
 * `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`,
 * `tuples_0_based` - **Compatibility warning** Defaults to `false`,
   giving 1-based tuple access, as is common practice in Erlang. Set it
   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
   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`.
   `tuples_0_based`. See also `lists_0_based`.
 
 
 
 
-* `vars` - Variables (and their values) to evaluate at compile-time
+* `vars` **deprecated** - Use `default_vars` instead. Variables (and
-  rather than render-time. (Currently not strictly true, see
+  their values) to evaluate at compile-time rather than
-  [#61](https://github.com/erlydtl/erlydtl/issues/61))
+  render-time.
+
 
 
 * `verbose` - Enable verbose printing of compilation progress. Add
 * `verbose` - Enable verbose printing of compilation progress. Add
   several for even more verbose (e.g. debug) output.
   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:
 Same as `render/1`, but with the following options:
 
 
-* `translation_fun` - A fun/1 that will be used to translate strings
+* `translation_fun` - A `fun/1` or `fun/2` that will be used to
-  appearing inside `{% trans %}` and `{% blocktrans %}` tags. The
+  translate strings appearing inside `{% trans %}` and `{% blocktrans
-  simplest TranslationFun would be `fun(Val) -> Val end`. Placeholders
+  %}` tags at render-time. The simplest TranslationFun would be
-  for blocktrans variable interpolation should be wrapped to `{{` and
+  `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
 * `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,
   `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`.
   template. See also `tuples_0_based`.
 
 
 * `locale` - A string specifying the current locale, for use with the
 * `locale` - A string specifying the current locale, for use with the
-  `blocktrans_fun` compile-time option.
+  `translation_fun` compile-time option.
 
 
 * `tuples_0_based` - If the compile option `tuples_0_based` was set to
 * `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,
   `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.
 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
 Custom tags and filters
 -----------------------
 -----------------------
 
 
@@ -410,6 +504,9 @@ Differences from standard Django Template Language
 * For an up-to-date list, see all
 * For an up-to-date list, see all
   [issues](https://github.com/erlydtl/erlydtl/issues) marked
   [issues](https://github.com/erlydtl/erlydtl/issues) marked
   `dtl_compat`.
   `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
 Tests
@@ -420,6 +517,3 @@ From a Unix shell, run:
     make tests
     make tests
 
 
 Note that the tests will create some output in tests/output in case of regressions.
 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 = "", 
           doc_root = "", 
           parse_trail = [],
           parse_trail = [],
           vars = [],
           vars = [],
+          const = [],
           record_info = [],
           record_info = [],
           filters = [],
           filters = [],
           tags = [],
           tags = [],
@@ -37,13 +38,16 @@
           tuples_0_based = false
           tuples_0_based = false
          }).
          }).
 
 
+%% ALL fields of ast_info{} must be lists (see erlydtl_compiler_utils:merge_info/2)
 -record(ast_info, {
 -record(ast_info, {
           dependencies = [],
           dependencies = [],
           translatable_strings = [],
           translatable_strings = [],
           translated_blocks= [],
           translated_blocks= [],
           custom_tags = [],
           custom_tags = [],
           var_names = [],
           var_names = [],
-          pre_render_asts = []}).
+          def_names = [],
+          const_names = []
+         }).
 
 
 -record(treewalker, {
 -record(treewalker, {
           counter = 0,
           counter = 0,

+ 22 - 4
rebar.config

@@ -1,18 +1,36 @@
 %% -*- mode: erlang -*-
 %% -*- 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]}.
 {erl_opts, [debug_info]}.
 {yrl_opts, [{includefile, "include/erlydtl_preparser.hrl"}]}.
 {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,
 {deps,
  [{merl, ".*",
  [{merl, ".*",
    {git, "git://github.com/erlydtl/merl.git", "28e5b3829168199e8475fa91b997e0c03b90d280"},
    {git, "git://github.com/erlydtl/merl.git", "28e5b3829168199e8475fa91b997e0c03b90d280"},
-   [raw]}
+   [raw]},
+  {eunit_formatters, ".*",
+   {git, "git://github.com/seancribbs/eunit_formatters", "7f79fa3fb953b94990bd9b41e92cef7cfecf91ef"}}
  ]}.
  ]}.
 
 
 {pre_hooks,
 {pre_hooks,
- [{compile, "make -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"},
+  {"freebsd", compile, "gmake -C $REBAR_DEPS_DIR/merl all -W test"},
   {eunit,
   {eunit,
    "erlc -I include/erlydtl_preparser.hrl -o test"
    "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 -*-
 %% -*- mode: erlang -*-
 {application, erlydtl,
 {application, erlydtl,
  [{description, "Django Template Language for Erlang"},
  [{description, "Django Template Language for Erlang"},
-  {vsn, "0.9.3"},
+  {vsn, "0.9.4"},
   {modules, []},
   {modules, []},
   {applications, [kernel, stdlib, compiler, syntax_tools]},
   {applications, [kernel, stdlib, compiler, syntax_tools]},
   {registered, []}
   {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]);
     io_lib:format("Invalid tag '~p' (~p:~p/~p)", [Name, Mod, Fun, Arity]);
 format_error({load_code, Error}) ->
 format_error({load_code, Error}) ->
     io_lib:format("Failed to load BEAM code: ~p", [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) ->
 format_error(Error) ->
     erlydtl_compiler: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
     try body_ast(DjangoParseTree, init_treewalker(Context)) of
         {{BodyAst, BodyInfo}, BodyTreeWalker} ->
         {{BodyAst, BodyInfo}, BodyTreeWalker} ->
             try custom_tags_ast(BodyInfo#ast_info.custom_tags, BodyTreeWalker) of
             try custom_tags_ast(BodyInfo#ast_info.custom_tags, BodyTreeWalker) of
-                {{CustomTagsAst, CustomTagsInfo},
+                {CustomTags,
                  #treewalker{
                  #treewalker{
                     context=#dtl_context{
                     context=#dtl_context{
                                errors=#error_info{ list=Errors }
                                errors=#error_info{ list=Errors }
@@ -194,8 +203,7 @@ compile_to_binary(DjangoParseTree, CheckSum, Context) ->
                   when length(Errors) == 0 ->
                   when length(Errors) == 0 ->
                     Forms = forms(
                     Forms = forms(
                               {BodyAst, BodyInfo},
                               {BodyAst, BodyInfo},
-                              {CustomTagsAst, CustomTagsInfo},
+                              CustomTags, CheckSum,
-                              CheckSum,
                               CustomTagsTreeWalker),
                               CustomTagsTreeWalker),
                     compile_forms(Forms, CustomTagsTreeWalker#treewalker.context);
                     compile_forms(Forms, CustomTagsTreeWalker#treewalker.context);
                 {_, #treewalker{ context=Context1 }} ->
                 {_, #treewalker{ context=Context1 }} ->
@@ -416,60 +424,36 @@ custom_tags_clauses_ast1([Tag|CustomTags], ExcludeTags, ClauseAcc, InfoAcc, Tree
             end
             end
     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) ->
 custom_forms(Dir, Module, Functions, AstInfo) ->
-    Exported = [erl_syntax:arity_qualifier(erl_syntax:atom(source_dir), erl_syntax:integer(0)),
+    Dependencies = AstInfo#ast_info.dependencies,
-                erl_syntax:arity_qualifier(erl_syntax:atom(dependencies), erl_syntax:integer(0)),
+    TranslatableStrings = AstInfo#ast_info.translatable_strings,
-                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:revert_forms(
-                erl_syntax:arity_qualifier(erl_syntax:atom(render), erl_syntax:integer(2)),
+      lists:flatten(
-                erl_syntax:arity_qualifier(erl_syntax:atom(render), erl_syntax:integer(3))
+        ?Q(["-module('@Module@').",
-                | lists:foldl(
+            "-export([source_dir/0, dependencies/0, translatable_strings/0,",
-                    fun({FunctionName, _}, Acc) ->
+            "         render/1, render/2, render/3]).",
-                            [erl_syntax:arity_qualifier(erl_syntax:atom(FunctionName), erl_syntax:integer(1)),
+            "-export(['@__export_functions'/0]).",
-                             erl_syntax:arity_qualifier(erl_syntax:atom(FunctionName), erl_syntax:integer(2))
+            "source_dir() -> _@Dir@.",
-                             |Acc]
+            "dependencies() -> _@Dependencies@.",
-                    end, [], Functions)
+            "translatable_strings() -> _@TranslatableStrings@.",
-               ],
+            "render(Tag) -> render(Tag, [], []).",
-    ModuleAst = ?Q("-module('@Module@')."),
+            "render(Tag, Vars) -> render(Tag, Vars, []).",
-    ExportAst = ?Q("-export(['@_Exported'/1])."),
+            "render(Tag, Vars, Opts) ->",
-
+            "  try '@Module@':Tag(Vars, Opts) of",
-    SourceFunctionAst = ?Q("source_dir() -> _@Dir@."),
+            "    Val -> {ok, Val}",
-
+            "  catch",
-    RenderAsts = ?Q(["render(Tag) -> render(Tag, [], []).",
+            "    Err -> {error, Err}",
-                     "render(Tag, Vars) -> render(Tag, Vars, []).",
+            "  end.",
-                     "render(Tag, Vars, Opts) ->",
+            "'@_functions'() -> _."
-                     "    try '@Module@':Tag(Vars, Opts) of",
+           ],
-                     "      Val -> {ok, Val}",
+           [{export_functions,
-                     "    catch",
+             erl_syntax:list(
-                     "      Err -> {error, Err}",
+               [erl_syntax:arity_qualifier(erl_syntax:atom(FName), erl_syntax:integer(Arity))
-                     "    end."]),
+                || {FName, _} <- Functions, Arity <- [1, 2]])},
-
+            {functions, [Ast || {_, Ast} <- Functions]}
-    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
-    ].
 
 
 stringify(BodyAst, #dtl_context{ binary_strings=BinaryStrings }) ->
 stringify(BodyAst, #dtl_context{ binary_strings=BinaryStrings }) ->
     [?Q("erlydtl_runtime:stringify_final(_@BodyAst, '@BinaryStrings@')")].
     [?Q("erlydtl_runtime:stringify_final(_@BodyAst, '@BinaryStrings@')")].
@@ -483,55 +467,37 @@ forms({BodyAst, BodyInfo}, {CustomTagsFunctionAst, CustomTagsInfo}, CheckSum,
         }=TreeWalker) ->
         }=TreeWalker) ->
     MergedInfo = merge_info(BodyInfo, CustomTagsInfo),
     MergedInfo = merge_info(BodyInfo, CustomTagsInfo),
 
 
-    Render0FunctionAst = ?Q("render() -> render([])."),
+    Dependencies = MergedInfo#ast_info.dependencies,
-    Render1FunctionAst = ?Q("render(Variables) -> render(Variables, [])."),
+    TranslatableStrings = MergedInfo#ast_info.translatable_strings,
-
+    TranslatedBlocks = MergedInfo#ast_info.translated_blocks,
-    Render2FunctionAst = ?Q(["render(Variables, RenderOptions) ->",
+    Variables = lists:usort(MergedInfo#ast_info.var_names),
-                             "  try render_internal(Variables, RenderOptions) of",
+    DefaultVariables = lists:usort(MergedInfo#ast_info.def_names),
-                             "    Val -> {ok, Val}",
+    Constants = lists:usort(MergedInfo#ast_info.const_names),
-                             "  catch",
+    FinalBodyAst = options_match_ast(TreeWalker) ++ stringify(BodyAst, Context),
-                             "    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))
-                     ])
-                  ]),
 
 
     erl_syntax:revert_forms(
     erl_syntax:revert_forms(
-      erl_syntax:form_list(
+      ?Q(["-module('@Module@').",
-        [ModuleAst, ExportAst, Render0FunctionAst, Render1FunctionAst, Render2FunctionAst,
+          "-export([render/0, render/1, render/2, source/0, dependencies/0,",
-         SourceFunctionAst, DependenciesFunctionAst, TranslatableStringsAst,
+          "         translatable_strings/0, translated_blocks/0, variables/0,",
-         TranslatedBlocksAst, VariablesAst, RenderInternalFunctionAst,
+          "         default_variables/0, constants/0]).",
-         CustomTagsFunctionAst
+          "source() -> {_@File@, _@CheckSum@}.",
-         |BodyInfo#ast_info.pre_render_asts
+          "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(#treewalker{ context=Context }=TreeWalker) ->
     options_match_ast(Context, TreeWalker);
     options_match_ast(Context, TreeWalker);
@@ -540,8 +506,9 @@ options_match_ast(Context) ->
 
 
 options_match_ast(Context, TreeWalker) ->
 options_match_ast(Context, TreeWalker) ->
     [
     [
-     ?Q("_TranslationFun = proplists:get_value(translation_fun, RenderOptions, none)"),
+     ?Q(["_TranslationFun = erlydtl_runtime:init_translation(",
-     ?Q("_CurrentLocale = proplists:get_value(locale, RenderOptions, none)"),
+         "  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)}])
      ?Q("_RecordInfo = _@info", [{info, merl:term(Context#dtl_context.record_info)}])
      | case call_extension(Context, setup_render_ast, [Context, TreeWalker]) of
      | case call_extension(Context, setup_render_ast, [Context, TreeWalker]) of
            undefined -> [];
            undefined -> [];
@@ -588,169 +555,149 @@ body_ast(DjangoParseTree, TreeWalker) ->
 
 
 body_ast(DjangoParseTree, BodyScope, TreeWalker) ->
 body_ast(DjangoParseTree, BodyScope, TreeWalker) ->
     {ScopeId, TreeWalkerScope} = begin_scope(BodyScope, TreeWalker),
     {ScopeId, TreeWalkerScope} = begin_scope(BodyScope, TreeWalker),
-    {AstInfoList, TreeWalker1} =
+    BodyFun =
-        lists:mapfoldl(
+        fun ({'autoescape', {identifier, _, OnOrOff}, Contents}, TW) ->
-          fun ({'autoescape', {identifier, _, OnOrOff}, Contents}, TW) ->
+                {Info, BodyTW} = body_ast(Contents, push_auto_escape(OnOrOff, TW)),
-                  {Info, BodyTW} = body_ast(Contents, push_auto_escape(OnOrOff, TW)),
+                {Info, pop_auto_escape(BodyTW)};
-                  {Info, pop_auto_escape(BodyTW)};
+            ({'block', {identifier, Pos, Name}, Contents}, #treewalker{ context=Context }=TW) ->
-              ({'block', {identifier, Pos, Name}, Contents}, #treewalker{ context=Context }=TW) ->
+                {Block, BlockScope} =
-                  {Block, BlockScope} =
+                    case dict:find(Name, Context#dtl_context.block_dict) of
-                      case dict:find(Name, Context#dtl_context.block_dict) of
+                        {ok, ChildBlock} ->
-                          {ok, ChildBlock} ->
+                            {{ContentsAst, _ContentsInfo}, _ContentsTW} = body_ast(Contents, TW),
-                              {{ContentsAst, _ContentsInfo}, _ContentsTW} = body_ast(Contents, TW),
+                            {ChildBlock,
-                              {ChildBlock,
+                             create_scope(
-                               create_scope(
+                               [{block, ?Q("[{super, _@ContentsAst}]")}],
-                                 [{block, ?Q("[{super, _@ContentsAst}]")}],
+                               Pos, TW)
-                                 Pos, TW)
+                            };
-                              };
+                        _ ->
-                          _ ->
+                            {Contents, empty_scope()}
-                              {Contents, empty_scope()}
+                    end,
-                      end,
+                body_ast(Block, BlockScope, TW);
-                  body_ast(Block, BlockScope, TW);
+            ({'blocktrans', Args, Contents, PluralContents}, TW) ->
-              ({'blocktrans', Args, Contents}, TW) ->
+                blocktrans_ast(Args, Contents, PluralContents, TW);
-                  blocktrans_ast(Args, Contents, TW);
+            ({'call', {identifier, _, Name}}, TW) ->
-              ({'call', {identifier, _, Name}}, TW) ->
+                call_ast(Name, TW);
-                  call_ast(Name, TW);
+            ({'call', {identifier, _, Name}, With}, TW) ->
-              ({'call', {identifier, _, Name}, With}, TW) ->
+                call_with_ast(Name, With, TW);
-                  call_with_ast(Name, With, TW);
+            ({'comment', _Contents}, TW) ->
-              ({'comment', _Contents}, TW) ->
+                empty_ast(TW);
-                  empty_ast(TW);
+            ({'comment_tag', _, _}, TW) ->
-              ({'cycle', Names}, TW) ->
+                empty_ast(TW);
-                  cycle_ast(Names, TW);
+            ({'cycle', Names}, TW) ->
-              ({'cycle_compat', Names}, TW) ->
+                cycle_ast(Names, TW);
-                  cycle_compat_ast(Names, TW);
+            ({'cycle_compat', Names}, TW) ->
-              ({'date', 'now', {string_literal, _Pos, FormatString}}, TW) ->
+                cycle_compat_ast(Names, TW);
-                  now_ast(FormatString, TW);
+            ({'date', 'now', {string_literal, _Pos, FormatString}}, TW) ->
-              ({'filter', FilterList, Contents}, TW) ->
+                now_ast(FormatString, TW);
-                  filter_tag_ast(FilterList, Contents, TW);
+            ({'filter', FilterList, Contents}, TW) ->
-              ({'firstof', Vars}, TW) ->
+                filter_tag_ast(FilterList, Contents, TW);
-                  firstof_ast(Vars, TW);
+            ({'firstof', Vars}, TW) ->
-              ({'for', {'in', IteratorList, Variable, Reversed}, Contents}, TW) ->
+                firstof_ast(Vars, TW);
-                  {EmptyAstInfo, TW1} = empty_ast(TW),
+            ({'for', {'in', IteratorList, Variable, Reversed}, Contents}, TW) ->
-                  for_loop_ast(IteratorList, Variable, Reversed, Contents, EmptyAstInfo, TW1);
+                {EmptyAstInfo, TW1} = empty_ast(TW),
-              ({'for', {'in', IteratorList, Variable, Reversed}, Contents, EmptyPartContents}, TW) ->
+                for_loop_ast(IteratorList, Variable, Reversed, Contents, EmptyAstInfo, TW1);
-                  {EmptyAstInfo, TW1} = body_ast(EmptyPartContents, TW),
+            ({'for', {'in', IteratorList, Variable, Reversed}, Contents, EmptyPartContents}, TW) ->
-                  for_loop_ast(IteratorList, Variable, Reversed, Contents, EmptyAstInfo, TW1);
+                {EmptyAstInfo, TW1} = body_ast(EmptyPartContents, TW),
-              ({'if', Expression, Contents, Elif}, TW) ->
+                for_loop_ast(IteratorList, Variable, Reversed, Contents, EmptyAstInfo, TW1);
-                  {IfAstInfo, TW1} = body_ast(Contents, TW),
+            ({'if', Expression, Contents, Elif}, TW) ->
-                  {ElifAstInfo, TW2} = body_ast(Elif, TW1),
+                {IfAstInfo, TW1} = body_ast(Contents, TW),
-                  ifelse_ast(Expression, IfAstInfo, ElifAstInfo, TW2);
+                {ElifAstInfo, TW2} = body_ast(Elif, TW1),
-              ({'if', Expression, Contents}, TW) ->
+                ifelse_ast(Expression, IfAstInfo, ElifAstInfo, TW2);
-                  {IfAstInfo, TW1} = body_ast(Contents, TW),
+            ({'if', Expression, Contents}, TW) ->
-                  {ElseAstInfo, TW2} = empty_ast(TW1),
+                {IfAstInfo, TW1} = body_ast(Contents, TW),
-                  ifelse_ast(Expression, IfAstInfo, ElseAstInfo, TW2);
+                {ElseAstInfo, TW2} = empty_ast(TW1),
-              ({'ifchanged', '$undefined', Contents}, TW) ->
+                ifelse_ast(Expression, IfAstInfo, ElseAstInfo, TW2);
-                  {IfAstInfo, TW1} = body_ast(Contents, TW),
+            ({'ifchanged', '$undefined', Contents}, TW) ->
-                  {ElseAstInfo, TW2} = empty_ast(TW1),
+                {IfAstInfo, TW1} = body_ast(Contents, TW),
-                  ifchanged_contents_ast(Contents, IfAstInfo, ElseAstInfo, TW2);
+                {ElseAstInfo, TW2} = empty_ast(TW1),
-              ({'ifchanged', Values, Contents}, TW) ->
+                ifchanged_contents_ast(Contents, IfAstInfo, ElseAstInfo, TW2);
-                  {IfAstInfo, TW1} = body_ast(Contents, TW),
+            ({'ifchanged', Values, Contents}, TW) ->
-                  {ElseAstInfo, TW2} = empty_ast(TW1),
+                {IfAstInfo, TW1} = body_ast(Contents, TW),
-                  ifchanged_values_ast(Values, IfAstInfo, ElseAstInfo, TW2);
+                {ElseAstInfo, TW2} = empty_ast(TW1),
-              ({'ifchangedelse', '$undefined', IfContents, ElseContents}, TW) ->
+                ifchanged_values_ast(Values, IfAstInfo, ElseAstInfo, TW2);
-                  {IfAstInfo, TW1} = body_ast(IfContents, TW),
+            ({'ifchangedelse', '$undefined', IfContents, ElseContents}, TW) ->
-                  {ElseAstInfo, TW2} = body_ast(ElseContents, TW1),
+                {IfAstInfo, TW1} = body_ast(IfContents, TW),
-                  ifchanged_contents_ast(IfContents, IfAstInfo, ElseAstInfo, TW2);
+                {ElseAstInfo, TW2} = body_ast(ElseContents, TW1),
-              ({'ifchangedelse', Values, IfContents, ElseContents}, TW) ->
+                ifchanged_contents_ast(IfContents, IfAstInfo, ElseAstInfo, TW2);
-                  {IfAstInfo, TW1} = body_ast(IfContents, TW),
+            ({'ifchangedelse', Values, IfContents, ElseContents}, TW) ->
-                  {ElseAstInfo, TW2} = body_ast(ElseContents, TW1),
+                {IfAstInfo, TW1} = body_ast(IfContents, TW),
-                  ifchanged_values_ast(Values, IfAstInfo, ElseAstInfo, TW2);
+                {ElseAstInfo, TW2} = body_ast(ElseContents, TW1),
-              ({'ifelse', Expression, IfContents, ElseContents}, TW) ->
+                ifchanged_values_ast(Values, IfAstInfo, ElseAstInfo, TW2);
-                  {IfAstInfo, TW1} = body_ast(IfContents, TW),
+            ({'ifelse', Expression, IfContents, ElseContents}, TW) ->
-                  {ElseAstInfo, TW2} = body_ast(ElseContents, TW1),
+                {IfAstInfo, TW1} = body_ast(IfContents, TW),
-                  ifelse_ast(Expression, IfAstInfo, ElseAstInfo, TW2);
+                {ElseAstInfo, TW2} = body_ast(ElseContents, TW1),
-              ({'ifequal', [Arg1, Arg2], Contents}, TW) ->
+                ifelse_ast(Expression, IfAstInfo, ElseAstInfo, TW2);
-                  {IfAstInfo, TW1} = body_ast(Contents, TW),
+            ({'ifequal', [Arg1, Arg2], Contents}, TW) ->
-                  {ElseAstInfo, TW2} = empty_ast(TW1),
+                {IfAstInfo, TW1} = body_ast(Contents, TW),
-                  ifelse_ast({'expr', "eq", Arg1, Arg2}, IfAstInfo, ElseAstInfo, TW2);
+                {ElseAstInfo, TW2} = empty_ast(TW1),
-              ({'ifequalelse', [Arg1, Arg2], IfContents, ElseContents}, TW) ->
+                ifelse_ast({'expr', "eq", Arg1, Arg2}, IfAstInfo, ElseAstInfo, TW2);
-                  {IfAstInfo, TW1} = body_ast(IfContents, TW),
+            ({'ifequalelse', [Arg1, Arg2], IfContents, ElseContents}, TW) ->
-                  {ElseAstInfo, TW2} = body_ast(ElseContents,TW1),
+                {IfAstInfo, TW1} = body_ast(IfContents, TW),
-                  ifelse_ast({'expr', "eq", Arg1, Arg2}, IfAstInfo, ElseAstInfo, TW2);
+                {ElseAstInfo, TW2} = body_ast(ElseContents,TW1),
-              ({'ifnotequal', [Arg1, Arg2], Contents}, TW) ->
+                ifelse_ast({'expr', "eq", Arg1, Arg2}, IfAstInfo, ElseAstInfo, TW2);
-                  {IfAstInfo, TW1} = body_ast(Contents, TW),
+            ({'ifnotequal', [Arg1, Arg2], Contents}, TW) ->
-                  {ElseAstInfo, TW2} = empty_ast(TW1),
+                {IfAstInfo, TW1} = body_ast(Contents, TW),
-                  ifelse_ast({'expr', "ne", Arg1, Arg2}, IfAstInfo, ElseAstInfo, TW2);
+                {ElseAstInfo, TW2} = empty_ast(TW1),
-              ({'ifnotequalelse', [Arg1, Arg2], IfContents, ElseContents}, TW) ->
+                ifelse_ast({'expr', "ne", Arg1, Arg2}, IfAstInfo, ElseAstInfo, TW2);
-                  {IfAstInfo, TW1} = body_ast(IfContents, TW),
+            ({'ifnotequalelse', [Arg1, Arg2], IfContents, ElseContents}, TW) ->
-                  {ElseAstInfo, TW2} = body_ast(ElseContents, TW1),
+                {IfAstInfo, TW1} = body_ast(IfContents, TW),
-                  ifelse_ast({'expr', "ne", Arg1, Arg2}, IfAstInfo, ElseAstInfo, TW2);
+                {ElseAstInfo, TW2} = body_ast(ElseContents, TW1),
-              ({'include', {string_literal, _, File}, Args}, #treewalker{ context=Context }=TW) ->
+                ifelse_ast({'expr', "ne", Arg1, Arg2}, IfAstInfo, ElseAstInfo, TW2);
-                  include_ast(unescape_string_literal(File), Args, Context#dtl_context.local_scopes, TW);
+            ({'include', {string_literal, _, File}, Args}, #treewalker{ context=Context }=TW) ->
-              ({'include_only', {string_literal, _, File}, Args}, TW) ->
+                include_ast(unescape_string_literal(File), Args, Context#dtl_context.local_scopes, TW);
-                  {Info, IncTW} = include_ast(unescape_string_literal(File), Args, [], TW),
+            ({'include_only', {string_literal, _, File}, Args}, TW) ->
-                  {Info, restore_scope(TW, IncTW)};
+                {Info, IncTW} = include_ast(unescape_string_literal(File), Args, [], TW),
-              ({'load_libs', Libs}, TW) ->
+                {Info, restore_scope(TW, IncTW)};
-                  load_libs_ast(Libs, TW);
+            ({'load_libs', Libs}, TW) ->
-              ({'load_from_lib', What, Lib}, TW) ->
+                load_libs_ast(Libs, TW);
-                  load_from_lib_ast(What, Lib, TW);
+            ({'load_from_lib', What, Lib}, TW) ->
-              ({'regroup', {ListVariable, Grouper, {identifier, _, NewVariable}}}, TW) ->
+                load_from_lib_ast(What, Lib, TW);
-                  regroup_ast(ListVariable, Grouper, NewVariable, TW);
+            ({'regroup', {ListVariable, Grouper, {identifier, _, NewVariable}}}, TW) ->
-              ('end_regroup', TW) ->
+                regroup_ast(ListVariable, Grouper, NewVariable, TW);
-                  {{end_scope, #ast_info{}}, TW};
+            ('end_regroup', TW) ->
-              ({'spaceless', Contents}, TW) ->
+                {{end_scope, #ast_info{}}, TW};
-                  spaceless_ast(Contents, TW);
+            ({'spaceless', Contents}, TW) ->
-              ({'ssi', Arg}, TW) ->
+                spaceless_ast(Contents, TW);
-                  ssi_ast(Arg, TW);
+            ({'ssi', Arg}, TW) ->
-              ({'ssi_parsed', {string_literal, _, FileName}}, #treewalker{ context=Context }=TW) ->
+                ssi_ast(Arg, TW);
-                  include_ast(unescape_string_literal(FileName), [], Context#dtl_context.local_scopes, TW);
+            ({'ssi_parsed', {string_literal, _, FileName}}, #treewalker{ context=Context }=TW) ->
-              ({'string', _Pos, String}, TW) ->
+                include_ast(unescape_string_literal(FileName), [], Context#dtl_context.local_scopes, TW);
-                  string_ast(String, TW);
+            ({'string', _Pos, String}, TW) ->
-              ({'tag', Name, Args}, TW) ->
+                string_ast(String, TW);
-                  tag_ast(Name, Args, TW);
+            ({'tag', Name, Args}, TW) ->
-              ({'templatetag', {_, _, TagName}}, TW) ->
+                tag_ast(Name, Args, TW);
-                  templatetag_ast(TagName, TW);
+            ({'templatetag', {_, _, TagName}}, TW) ->
-              ({'trans', Value}, TW) ->
+                templatetag_ast(TagName, TW);
-                  translated_ast(Value, TW);
+            ({'trans', Value}, TW) ->
-              ({'widthratio', Numerator, Denominator, Scale}, TW) ->
+                translated_ast(Value, TW);
-                  widthratio_ast(Numerator, Denominator, Scale, TW);
+            ({'trans', Value, Context}, TW) ->
-              ({'with', Args, Contents}, TW) ->
+                translated_ast(Value, Context, TW);
-                  with_ast(Args, Contents, TW);
+            ({'widthratio', Numerator, Denominator, Scale}, TW) ->
-              ({'scope_as', {identifier, _, Name}, Contents}, TW) ->
+                widthratio_ast(Numerator, Denominator, Scale, TW);
-                  scope_as(Name, Contents, TW);
+            ({'with', Args, Contents}, TW) ->
-              ({'extension', Tag}, TW) ->
+                with_ast(Args, Contents, TW);
-                  extension_ast(Tag, TW);
+            ({'scope_as', {identifier, _, Name}, Contents}, TW) ->
-              ({'extends', _}, TW) ->
+                scope_as(Name, Contents, TW);
-                  empty_ast(?ERR(unexpected_extends_tag, TW));
+            ({'extension', Tag}, TW) ->
-              (ValueToken, TW) ->
+                extension_ast(Tag, TW);
-                  format(value_ast(ValueToken, true, true, TW))
+            ({'extends', _}, TW) ->
-          end,
+                empty_ast(?ERR(unexpected_extends_tag, TW));
-          TreeWalkerScope,
+            (ValueToken, TW) ->
-          DjangoParseTree),
+                format(value_ast(ValueToken, true, true, TW))
+        end,
+
+    {AstInfoList, TreeWalker1} = lists:mapfoldl(BodyFun, TreeWalkerScope, DjangoParseTree),
 
 
-    Vars = TreeWalker1#treewalker.context#dtl_context.vars,
+    {AstList, Info} =
-    {AstList, {Info, TreeWalker2}} =
         lists:mapfoldl(
         lists:mapfoldl(
-          fun ({Ast, Info}, {InfoAcc, TreeWalkerAcc}) ->
+          fun ({Ast, Info}, InfoAcc) ->
-                  PresetVars = lists:foldl(
+                  {Ast, merge_info(Info, InfoAcc)}
-                                 fun (X, Acc) ->
+          end, #ast_info{}, AstInfoList),
-                                         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),
 
 
-    {Ast, TreeWalker3} = end_scope(
+    {Ast, TreeWalker2} = end_scope(
                            fun ([ScopeVars|ScopeBody]) -> [?Q("begin _@ScopeVars, [_@ScopeBody] end")] end,
                            fun ([ScopeVars|ScopeBody]) -> [?Q("begin _@ScopeVars, [_@ScopeBody] end")] end,
-                           ScopeId, AstList, TreeWalker2),
+                           ScopeId, AstList, TreeWalker1),
-    {{erl_syntax:list(Ast), Info}, TreeWalker3}.
+    {{erl_syntax:list(Ast), Info}, TreeWalker2}.
 
 
 
 
 value_ast(ValueToken, AsString, EmptyIfUndefined, TreeWalker) ->
 value_ast(ValueToken, AsString, EmptyIfUndefined, TreeWalker) ->
@@ -804,38 +751,58 @@ with_dependency(FilePath, {{Ast, Info}, TreeWalker}) ->
 empty_ast(TreeWalker) ->
 empty_ast(TreeWalker) ->
     {{erl_syntax:list([]), #ast_info{}}, 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
     %% add new scope using 'with' values
     {NewScope, {ArgInfo, TreeWalker1}} =
     {NewScope, {ArgInfo, TreeWalker1}} =
         lists:mapfoldl(
         lists:mapfoldl(
-          fun ({{identifier, _, LocalVarName}, Value}, {AstInfoAcc, TreeWalkerAcc}) ->
+          fun ({LocalVarName, Value}, {AstInfoAcc, TreeWalkerAcc}) ->
                   {{Ast, Info}, TW} = value_ast(Value, false, false, TreeWalkerAcc),
                   {{Ast, Info}, TW} = value_ast(Value, false, false, TreeWalkerAcc),
                   {{LocalVarName, Ast}, {merge_info(AstInfoAcc, Info), TW}}
                   {{LocalVarName, Ast}, {merge_info(AstInfoAcc, Info), TW}}
           end,
           end,
           {#ast_info{}, TreeWalker},
           {#ast_info{}, TreeWalker},
-          ArgList),
+          case Count of
+              {{identifier, _, Name}, Value} ->
+                  [{Name, Value}|ArgList];
+              _ ->
+                  ArgList
+          end),
 
 
     TreeWalker2 = push_scope(NewScope, TreeWalker1),
     TreeWalker2 = push_scope(NewScope, TreeWalker1),
 
 
     %% key for translation lookup
     %% key for translation lookup
-    SourceText = lists:flatten(erlydtl_unparser:unparse(Contents)),
+    SourceText = erlydtl_unparser:unparse(Contents),
     {{DefaultAst, AstInfo}, TreeWalker3} = body_ast(Contents, TreeWalker2),
     {{DefaultAst, AstInfo}, TreeWalker3} = body_ast(Contents, TreeWalker2),
     MergedInfo = merge_info(AstInfo, ArgInfo),
     MergedInfo = merge_info(AstInfo, ArgInfo),
 
 
-    Context = TreeWalker3#treewalker.context,
+    #dtl_context{
-    case Context#dtl_context.trans_fun of
+      trans_fun = TFun,
-        none ->
+      trans_locales = TLocales } = TreeWalker3#treewalker.context,
+    if TFun =:= none; PluralContents =/= undefined ->
             %% translate in runtime
             %% translate in runtime
             {FinalAst, FinalTW} = blocktrans_runtime_ast(
             {FinalAst, FinalTW} = blocktrans_runtime_ast(
-                                    {DefaultAst, MergedInfo},
+                                    {DefaultAst, MergedInfo}, SourceText, Contents, Context,
-                                    SourceText, Contents, TreeWalker3),
+                                    plural_contents(PluralContents, Count, TreeWalker3)),
             {FinalAst, restore_scope(TreeWalker1, FinalTW)};
             {FinalAst, restore_scope(TreeWalker1, FinalTW)};
-        BlockTransFun when is_function(BlockTransFun) ->
+       is_function(TFun, 2) ->
             %% translate in compile-time
             %% translate in compile-time
-            {FinalAstInfo, FinalTreeWalker, Clauses} = 
+            {FinalAstInfo, FinalTreeWalker, Clauses} =
                 lists:foldr(
                 lists:foldr(
                   fun (Locale, {AstInfoAcc, TreeWalkerAcc, ClauseAcc}) ->
                   fun (Locale, {AstInfoAcc, TreeWalkerAcc, ClauseAcc}) ->
-                          case BlockTransFun(SourceText, Locale) of
+                          case TFun(SourceText, phrase_locale(Locale, Context)) of
                               default ->
                               default ->
                                   {AstInfoAcc, TreeWalkerAcc, ClauseAcc};
                                   {AstInfoAcc, TreeWalkerAcc, ClauseAcc};
                               Body ->
                               Body ->
@@ -845,74 +812,131 @@ blocktrans_ast(ArgList, Contents, TreeWalker) ->
                                    [?Q("_@Locale@ -> _@BodyAst")|ClauseAcc]}
                                    [?Q("_@Locale@ -> _@BodyAst")|ClauseAcc]}
                           end
                           end
                   end,
                   end,
-                  {MergedInfo, TreeWalker2, []},
+                  {MergedInfo, TreeWalker3, []}, TLocales),
-                  Context#dtl_context.trans_locales),
             FinalAst = ?Q("case _CurrentLocale of _@_Clauses -> _; _ -> _@DefaultAst end"),
             FinalAst = ?Q("case _CurrentLocale of _@_Clauses -> _; _ -> _@DefaultAst end"),
             {{FinalAst, FinalAstInfo#ast_info{ translated_blocks = [SourceText] }},
             {{FinalAst, FinalAstInfo#ast_info{ translated_blocks = [SourceText] }},
-             restore_scope(TreeWalker1, FinalTreeWalker)}
+             restore_scope(TreeWalker1, FinalTreeWalker)};
+       true ->
+            empty_ast(?ERR({translation_fun, TFun}, TreeWalker3))
     end.
     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.
     %% Contents is flat - only strings and '{{var}}' allowed.
     %% build sorted list (orddict) of pre-resolved variables to pass to runtime translation function
     %% build sorted list (orddict) of pre-resolved variables to pass to runtime translation function
     USortedVariables = lists:usort(fun({variable, {identifier, _, A}},
     USortedVariables = lists:usort(fun({variable, {identifier, _, A}},
                                        {variable, {identifier, _, B}}) ->
                                        {variable, {identifier, _, B}}) ->
                                            A =< B
                                            A =< B
-                                   end, [Var || {variable, _}=Var <- Contents]),
+                                   end, [Var || {variable, _}=Var
+                                                    <- Contents ++ maybe_plural_contents(Plural)]),
     VarBuilder = fun({variable, {identifier, _, Name}}=Var, TW) ->
     VarBuilder = fun({variable, {identifier, _, Name}}=Var, TW) ->
                          {{VarAst, _VarInfo}, VarTW}  = resolve_variable_ast(Var, false, TW),
                          {{VarAst, _VarInfo}, VarTW}  = resolve_variable_ast(Var, false, TW),
                          {?Q("{_@name, _@VarAst}", [{name, merl:term(atom_to_list(Name))}]), VarTW}
                          {?Q("{_@name, _@VarAst}", [{name, merl:term(atom_to_list(Name))}]), VarTW}
                  end,
                  end,
     {VarAsts, TreeWalker1} = lists:mapfoldl(VarBuilder, TreeWalker, USortedVariables),
     {VarAsts, TreeWalker1} = lists:mapfoldl(VarBuilder, TreeWalker, USortedVariables),
     VarListAst = erl_syntax:list(VarAsts),
     VarListAst = erl_syntax:list(VarAsts),
-    BlockTransAst = ?Q(["if _TranslationFun =:= none -> _@DefaultAst;",
+    BlockTransAst = ?Q(["begin",
-                        "  true -> erlydtl_runtime:translate_block(",
+                        "  case erlydtl_runtime:translate_block(",
-                        "    _@SourceText@, _TranslationFun, _@VarListAst)",
+                        "         _@phrase, _@locale,",
-                        "end"]),
+                        "         _@VarListAst, _TranslationFun) of",
-    {{BlockTransAst, Info}, TreeWalker1}.
+                        "    default -> _@DefaultAst;",
-
+                        "    Text -> Text",
-translated_ast({string_literal, _, String}, TreeWalker) ->
+                        "  end",
-    UnescapedStr = unescape_string_literal(String),
+                        "end"],
-    case call_extension(TreeWalker, translate_ast, [UnescapedStr, TreeWalker]) of
+                       [{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 ->
         undefined ->
-            AstInfo = #ast_info{translatable_strings = [UnescapedStr]},
             case TreeWalker#treewalker.context#dtl_context.trans_fun of
             case TreeWalker#treewalker.context#dtl_context.trans_fun of
-                none -> runtime_trans_ast({{erl_syntax:string(UnescapedStr), AstInfo}, TreeWalker});
+                none -> runtime_trans_ast(Text, Context, TreeWalker);
-                _ -> compiletime_trans_ast(UnescapedStr, AstInfo, 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;
             end;
-        Translated ->
+        TranslatedAst ->
-            Translated
+            TranslatedAst
     end;
     end;
-translated_ast(ValueToken, TreeWalker) ->
+translated_ast(Value, Context, TreeWalker) ->
-    runtime_trans_ast(value_ast(ValueToken, true, false, TreeWalker)).
+    runtime_trans_ast(value_ast(Value, true, false, TreeWalker), Context).
-
+
-runtime_trans_ast({{ValueAst, AstInfo}, TreeWalker}) ->
+runtime_trans_ast(Text, Context, TreeWalker) ->
-    {{?Q("erlydtl_runtime:translate(_@ValueAst, _TranslationFun)"),
+    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}.
       AstInfo}, TreeWalker}.
 
 
-compiletime_trans_ast(String, AstInfo,
+compiletime_trans_ast(TFun, Text, LContext,
                       #treewalker{
                       #treewalker{
                          context=#dtl_context{
                          context=#dtl_context{
-                                    trans_fun=TFun,
                                     trans_locales=TLocales
                                     trans_locales=TLocales
                                    }=Context
                                    }=Context
                         }=TreeWalker) ->
                         }=TreeWalker) ->
     ClAst = lists:foldl(
     ClAst = lists:foldl(
               fun(Locale, ClausesAcc) ->
               fun(Locale, ClausesAcc) ->
                       [?Q("_@Locale@ -> _@translated",
                       [?Q("_@Locale@ -> _@translated",
-                          [{translated, case TFun(String, Locale) of
+                          [{translated, case TFun(Text, phrase_locale(Locale, LContext)) of
-                                            default -> string_ast(String, Context);
+                                            default -> string_ast(Text, Context);
                                             Translated -> string_ast(Translated, Context)
                                             Translated -> string_ast(Translated, Context)
                                         end}])
                                         end}])
                        |ClausesAcc]
                        |ClausesAcc]
               end,
               end,
               [], TLocales),
               [], TLocales),
-    CaseAst = ?Q(["case _CurrentLocale of",
+    {{?Q(["case _CurrentLocale of",
-                  "  _@_ClAst -> _;",
+          "  _@_ClAst -> _;",
-                  " _ -> _@string",
+          " _ -> _@string",
-                  "end"],
+          "end"],
-                 [{string, string_ast(String, Context)}]),
+         [{string, string_ast(Text, Context)}]),
-    {{CaseAst, AstInfo}, TreeWalker}.
+      #ast_info{ translatable_strings = [Text] }},
+     TreeWalker}.
+
+%%% end of context being translation context
+
 
 
 %% Completely unnecessary in ErlyDTL (use {{ "{%" }} etc), but implemented for compatibility.
 %% Completely unnecessary in ErlyDTL (use {{ "{%" }} etc), but implemented for compatibility.
 templatetag_ast("openblock", TreeWalker) ->
 templatetag_ast("openblock", TreeWalker) ->
@@ -1127,8 +1151,7 @@ resolve_variable_ast1({attribute, {{_, Pos, Attr}, Variable}}, {Runtime, Finder}
     FileName = get_current_file(TreeWalker1),
     FileName = get_current_file(TreeWalker1),
     {{?Q(["'@Runtime@':'@Finder@'(",
     {{?Q(["'@Runtime@':'@Finder@'(",
           "  _@Attr@, _@VarAst,",
           "  _@Attr@, _@VarAst,",
-          "  [",
+          "  [{lists_0_based, _@Lists0Based@},",
-          "   {lists_0_based, _@Lists0Based@},",
           "   {tuples_0_based, _@Tuples0Based@},",
           "   {tuples_0_based, _@Tuples0Based@},",
           "   {render_options, RenderOptions},",
           "   {render_options, RenderOptions},",
           "   {record_info, _RecordInfo},",
           "   {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) ->
 resolve_variable_ast1({variable, {identifier, Pos, VarName}}, {Runtime, Finder}, TreeWalker) ->
     Ast = case resolve_variable(VarName, TreeWalker) of
     Ast = case resolve_variable(VarName, TreeWalker) of
-              undefined ->
+              {_, undefined} ->
                   FileName = get_current_file(TreeWalker),
                   FileName = get_current_file(TreeWalker),
                   {?Q(["'@Runtime@':'@Finder@'(",
                   {?Q(["'@Runtime@':'@Finder@'(",
                        "  _@VarName@, _Variables,",
                        "  _@VarName@, _Variables,",
                        "  [{filename, _@FileName@},",
                        "  [{filename, _@FileName@},",
                        "   {pos, _@Pos@},",
                        "   {pos, _@Pos@},",
                        "   {record_info, _RecordInfo},",
                        "   {record_info, _RecordInfo},",
-                       "   {render_options, RenderOptions}])"]),
+                       "   {render_options, RenderOptions}])"
+                      ]),
                    #ast_info{ var_names=[VarName] }};
                    #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{}}
                   {Val, #ast_info{}}
           end,
           end,
     {Ast, TreeWalker}.
     {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}) ->
 format({{Ast, Info}, TreeWalker}) ->
     auto_escape({{format_number_ast(Ast), Info}, TreeWalker}).
     auto_escape({{format_number_ast(Ast), Info}, TreeWalker}).
 
 
@@ -1207,7 +1257,7 @@ with_ast(ArgList, Contents, TreeWalker) ->
 
 
     NewScope = lists:map(
     NewScope = lists:map(
                  fun ({{identifier, _, LocalVarName}, _Value}) ->
                  fun ({{identifier, _, LocalVarName}, _Value}) ->
-                         {LocalVarName, merl:var(lists:concat(["Var_", LocalVarName]))}
+                         {LocalVarName, varname_ast(LocalVarName)}
                  end,
                  end,
                  ArgList),
                  ArgList),
 
 
@@ -1223,7 +1273,7 @@ with_ast(ArgList, Contents, TreeWalker) ->
 
 
 scope_as(VarName, Contents, TreeWalker) ->
 scope_as(VarName, Contents, TreeWalker) ->
     {{ContentsAst, ContentsInfo}, TreeWalker1} = body_ast(Contents, TreeWalker),
     {{ContentsAst, ContentsInfo}, TreeWalker1} = body_ast(Contents, TreeWalker),
-    VarAst = merl:var(lists:concat(["Var_", VarName])),
+    VarAst = varname_ast(VarName),
     {Id, TreeWalker2} = begin_scope(
     {Id, TreeWalker2} = begin_scope(
                           {[{VarName, VarAst}],
                           {[{VarName, VarAst}],
                            [?Q("_@VarAst = _@ContentsAst")]},
                            [?Q("_@VarAst = _@ContentsAst")]},
@@ -1232,7 +1282,7 @@ scope_as(VarName, Contents, TreeWalker) ->
 
 
 regroup_ast(ListVariable, GrouperVariable, LocalVarName, TreeWalker) ->
 regroup_ast(ListVariable, GrouperVariable, LocalVarName, TreeWalker) ->
     {{ListAst, ListInfo}, TreeWalker1} = value_ast(ListVariable, false, true, 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(
     {Id, TreeWalker2} = begin_scope(
                           {[{LocalVarName, LocalVarAst}],
                           {[{LocalVarName, LocalVarAst}],
@@ -1271,10 +1321,8 @@ for_loop_ast(IteratorList, LoopValue, IsReversed, Contents,
     %% setup
     %% setup
     VarScope = lists:map(
     VarScope = lists:map(
                  fun({identifier, {R, C}, Iterator}) ->
                  fun({identifier, {R, C}, Iterator}) ->
-                         {Iterator, merl:var(
+                         {Iterator, varname_ast(lists:concat([
-                                      lists:concat(["Var_", Iterator,
+                                    Iterator,"/", Level, "_", R, ":", C]))}
-                                                    "/", Level, "_", R, ":", C
-                                                   ]))}
                  end, IteratorList),
                  end, IteratorList),
     {Iterators, IteratorVars} = lists:unzip(VarScope),
     {Iterators, IteratorVars} = lists:unzip(VarScope),
     IteratorCount = length(IteratorVars),
     IteratorCount = length(IteratorVars),
@@ -1290,7 +1338,7 @@ for_loop_ast(IteratorList, LoopValue, IsReversed, Contents,
 
 
     LoopValueAst0 = to_list_ast(LoopValueAst, merl:term(IsReversed), TreeWalker2),
     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
     %% call for loop
     {{?Q(["case erlydtl_runtime:forloop(",
     {{?Q(["case erlydtl_runtime:forloop(",
@@ -1313,7 +1361,7 @@ for_loop_ast(IteratorList, LoopValue, IsReversed, Contents,
                               ?Q("() when true -> {_@Vars}")
                               ?Q("() when true -> {_@Vars}")
                       end}]),
                       end}]),
       merge_info(merge_info(Info, EmptyContentsInfo), LoopValueInfo)},
       merge_info(merge_info(Info, EmptyContentsInfo), LoopValueInfo)},
-     TreeWalker2}.
+     TreeWalker3}.
 
 
 ifchanged_values_ast(Values, {IfContentsAst, IfContentsInfo}, {ElseContentsAst, ElseContentsInfo}, TreeWalker) ->
 ifchanged_values_ast(Values, {IfContentsAst, IfContentsInfo}, {ElseContentsAst, ElseContentsInfo}, TreeWalker) ->
     Info = merge_info(IfContentsInfo, ElseContentsInfo),
     Info = merge_info(IfContentsInfo, ElseContentsInfo),
@@ -1355,10 +1403,10 @@ cycle_ast(Names, #treewalker{ context=Context }=TreeWalker) ->
                 (_, VarNamesAcc) ->
                 (_, VarNamesAcc) ->
                     {[], VarNamesAcc}
                     {[], VarNamesAcc}
             end, [], Names),
             end, [], Names),
-    {{?Q("erlydtl_runtime:cycle({_@NamesTuple}, _@forloop)",
+    {ForLoop, TreeWalker1} = resolve_reserved_variable('forloop', TreeWalker),
-        [{forloop, resolve_variable('forloop', TreeWalker)}]),
+    {{?Q("erlydtl_runtime:cycle({_@NamesTuple}, _@ForLoop)"),
       #ast_info{ var_names = VarNames }},
       #ast_info{ var_names = VarNames }},
-     TreeWalker}.
+     TreeWalker1}.
 
 
 %% Older Django templates treat cycle with comma-delimited elements as strings
 %% Older Django templates treat cycle with comma-delimited elements as strings
 cycle_compat_ast(Names, #treewalker{ context=Context }=TreeWalker) ->
 cycle_compat_ast(Names, #treewalker{ context=Context }=TreeWalker) ->
@@ -1366,10 +1414,10 @@ cycle_compat_ast(Names, #treewalker{ context=Context }=TreeWalker) ->
                    fun ({identifier, _, X}) ->
                    fun ({identifier, _, X}) ->
                            string_ast(X, Context)
                            string_ast(X, Context)
                    end, Names),
                    end, Names),
-    {{?Q("erlydtl_runtime:cycle({_@NamesTuple}, _@forloop)",
+    {ForLoop, TreeWalker1} = resolve_reserved_variable('forloop', TreeWalker),
-        [{forloop, resolve_variable('forloop', TreeWalker)}]),
+    {{?Q("erlydtl_runtime:cycle({_@NamesTuple}, _@ForLoop)"),
       #ast_info{}},
       #ast_info{}},
-     TreeWalker}.
+     TreeWalker1}.
 
 
 now_ast(FormatString, TreeWalker) ->
 now_ast(FormatString, TreeWalker) ->
     %% Note: we can't use unescape_string_literal here
     %% Note: we can't use unescape_string_literal here
@@ -1473,7 +1521,7 @@ create_scope(Vars, VarScope) ->
     {Scope, Values} =
     {Scope, Values} =
         lists:foldl(
         lists:foldl(
           fun ({Name, Value}, {VarAcc, ValueAcc}) ->
           fun ({Name, Value}, {VarAcc, ValueAcc}) ->
-                  NameAst = merl:var(lists:concat(["_Var_", Name, VarScope])),
+                  NameAst = varname_ast(lists:concat(["_", Name, VarScope])),
                   {[{Name, NameAst}|VarAcc],
                   {[{Name, NameAst}|VarAcc],
                    [?Q("_@NameAst = _@Value")|ValueAcc]
                    [?Q("_@NameAst = _@Value")|ValueAcc]
                   }
                   }
@@ -1485,3 +1533,8 @@ create_scope(Vars, VarScope) ->
 create_scope(Vars, {Row, Col}, #treewalker{ context=Context }) ->
 create_scope(Vars, {Row, Col}, #treewalker{ context=Context }) ->
     Level = length(Context#dtl_context.local_scopes),
     Level = length(Context#dtl_context.local_scopes),
     create_scope(Vars, lists:concat(["/", Level, "_", Row, ":", Col])).
     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(
     io_lib:format(
       "Failed to include file ~s: ~s",
       "Failed to include file ~s: ~s",
       [File, file:format_error(Error)]);
       [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) ->
 format_error(Error) ->
     erlydtl_compiler_utils:format_error(Error).
     erlydtl_compiler_utils:format_error(Error).
 
 
@@ -103,7 +107,7 @@ format_error(Error) ->
 process_opts(File, Module, Options0) ->
 process_opts(File, Module, Options0) ->
     Options1 = proplists:normalize(
     Options1 = proplists:normalize(
                  update_defaults(Options0),
                  update_defaults(Options0),
-                 [{aliases, [{outdir, out_dir}]}
+                 [{aliases, deprecated_opts()}
                  ]),
                  ]),
     Source0 = filename:absname(
     Source0 = filename:absname(
                 case File of
                 case File of
@@ -122,12 +126,32 @@ process_opts(File, Module, Options0) ->
     Source = shorten_filename(Source0),
     Source = shorten_filename(Source0),
     Options = [{compiler_options, [{source, Source}]}
     Options = [{compiler_options, [{source, Source}]}
                |compiler_opts(Options1, [])],
                |compiler_opts(Options1, [])],
-    case File of
+    Context =
-        {dir, _} ->
+        case File of
-            init_context([], Source, Module, Options);
+            {dir, _} ->
-        _ ->
+                init_context([], Source, Module, Options);
-            init_context([Source], filename:dirname(Source), Module, Options)
+            _ ->
-    end.
+                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)
 compiler_opts([CompilerOption|Os], Acc)
   when
   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, list_to_atom(Module), Options);
 init_context(ParseTrail, DefDir, Module, Options) ->
 init_context(ParseTrail, DefDir, Module, Options) ->
     Ctx = #dtl_context{},
     Ctx = #dtl_context{},
-    Locale = proplists:get_value(locale, Options),
+    Locales = lists:usort(
-    BlocktransLocales = proplists:get_value(blocktrans_locales, Options),
+                lists:concat(
-    TransLocales = case {Locale, BlocktransLocales} of
+                  [proplists:get_all_values(locale, Options),
-                       {undefined, undefined} -> Ctx#dtl_context.trans_locales;
+                   proplists:get_value(locales, Options, Ctx#dtl_context.trans_locales)]
-                       {undefined, Val} -> Val;
+                 )),
-                       {Val, undefined} -> [Val];
-                       _ -> lists:usort([Locale | BlocktransLocales])
-                   end,
     Context0 =
     Context0 =
         #dtl_context{
         #dtl_context{
            all_options = Options,
            all_options = Options,
@@ -239,9 +260,11 @@ init_context(ParseTrail, DefDir, Module, Options) ->
            custom_tags_dir = proplists:get_value(
            custom_tags_dir = proplists:get_value(
                                custom_tags_dir, Options,
                                custom_tags_dir, Options,
                                filename:join([erlydtl_deps:get_base_dir(), "priv", "custom_tags"])),
                                filename:join([erlydtl_deps:get_base_dir(), "priv", "custom_tags"])),
-           trans_fun = proplists:get_value(blocktrans_fun, Options, Ctx#dtl_context.trans_fun),
+           trans_fun = erlydtl_runtime:init_translation(
-           trans_locales = TransLocales,
+                         proplists:get_value(translation_fun, Options, Ctx#dtl_context.trans_fun)),
-           vars = proplists:get_value(vars, Options, Ctx#dtl_context.vars),
+           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),
            reader = proplists:get_value(reader, Options, Ctx#dtl_context.reader),
            compiler_options = proplists:append_values(compiler_options, Options),
            compiler_options = proplists:append_values(compiler_options, Options),
            binary_strings = proplists:get_value(binary_strings, Options, Ctx#dtl_context.binary_strings),
            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
             undefined
     end.
     end.
 
 
-merge_info(Info1, Info2) ->
+merge_info(Info1, Info2) when is_record(Info1, ast_info), is_record(Info2, ast_info) ->
-    #ast_info{
+    merge_info1(record_info(size, ast_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)}.
 
 
 resolve_variable(VarName, TreeWalker) ->
 resolve_variable(VarName, TreeWalker) ->
     resolve_variable(VarName, undefined, TreeWalker).
     resolve_variable(VarName, undefined, TreeWalker).
 
 
 resolve_variable(VarName, Default, #treewalker{ context=Context }) ->
 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) ->
 push_scope(Scope, #treewalker{ context=Context }=TreeWalker) ->
     TreeWalker#treewalker{ context=push_scope(Scope, Context) };
     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) ->
 pos_info({Line, Col}) when is_integer(Line), is_integer(Col) ->
     io_lib:format("~b:~b ", [Line, Col]).
     io_lib:format("~b:~b ", [Line, Col]).
 
 
-resolve_variable1([], _VarName, Default) -> Default;
+resolve_variable1([], _VarName) -> undefined;
-resolve_variable1([Scope|Scopes], VarName, Default) ->
+resolve_variable1([Scope|Scopes], VarName) ->
-    case proplists:get_value(VarName, get_scope(Scope), Default) of
+    case proplists:get_value(VarName, get_scope(Scope)) of
-        Default ->
+        undefined ->
-            resolve_variable1(Scopes, VarName, Default);
+            resolve_variable1(Scopes, VarName);
         Value -> Value
         Value -> Value
     end.
     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({_Id, Scope, _Values}) -> Scope;
 get_scope(Scope) -> Scope.
 get_scope(Scope) -> Scope.
 
 

+ 47 - 22
src/erlydtl_parser.yrl

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

+ 69 - 27
src/erlydtl_runtime.erl

@@ -2,7 +2,19 @@
 
 
 -compile(export_all).
 -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).
 -define(IFCHANGED_CONTEXT_VARIABLE, erlydtl_ifchanged_context).
 
 
@@ -98,8 +110,11 @@ find_value(Key, Tuple) when is_tuple(Tuple) ->
     end.
     end.
 
 
 fetch_value(Key, Data, Options) ->
 fetch_value(Key, Data, Options) ->
+    fetch_value(Key, Data, Options, []).
+
+fetch_value(Key, Data, Options, Default) ->
     case find_value(Key, Data, Options) of
     case find_value(Key, Data, Options) of
-        undefined -> [];
+        undefined -> Default;
         Val -> Val
         Val -> Val
     end.
     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])
             regroup(Rest, Attribute, [[{grouper, Value}, {list, [Item]}], [{grouper, PrevGrouper}, {list, lists:reverse(PrevList)}]|Acc])
     end.
     end.
 
 
--spec translate(Str, none | translate_fun()) -> Str when
+-spec init_translation(init_translation()) -> none | translate_fun().
-      Str :: string() | binary().
+init_translation(none) -> none;
-translate(String, none) -> String;
+init_translation(Fun) when is_function(Fun, 0) ->
-translate(String, TranslationFun)
+    init_translation(Fun());
-  when is_function(TranslationFun) ->
+init_translation({M, F}) ->
-    case TranslationFun(String) of
+    init_translation({M, F, []});
-        undefined -> String;
+init_translation({M, F, A}) ->
-        <<"">> -> String;
+    init_translation(apply(M, F, A));
-        "" -> String;
+init_translation(Fun)
-        Str -> Str
+  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.
     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.
 %% @doc Translate and interpolate 'blocktrans' content.
 %% Pre-requisites:
 %% Pre-requisites:
 %%  * `Variables' should be sorted
 %%  * `Variables' should be sorted
 %%  * Each interpolation variable should exist
 %%  * Each interpolation variable should exist
 %%    (String="{{a}}", Variables=[{"b", "b-val"}] will fall)
 %%    (String="{{a}}", Variables=[{"b", "b-val"}] will fall)
 %%  * Orddict keys should be string(), not binary()
 %%  * Orddict keys should be string(), not binary()
--spec translate_block(string() | binary(), translate_fun(), orddict:orddict()) -> iodata().
+-spec translate_block(phrase(), locale(), orddict:orddict(), none | translate_fun()) -> iodata().
-translate_block(String, TranslationFun, Variables) ->
+translate_block(Phrase, Locale, Variables, TranslationFun) ->
-    TransString = case TranslationFun(String) of
+    case translate(Phrase, Locale, TranslationFun, default) of
-                      No when (undefined == No)
+        default -> default;
-                              orelse (<<"">> == No)
+        Translated ->
-                              orelse ("" == No) -> String;
+            try interpolate_variables(Translated, Variables)
-                      Str -> Str
+            catch
-                  end,
+                {no_close_var, T} ->
-    try interpolate_variables(TransString, Variables)
+                    io:format(standard_error, "Warning: template translation: variable not closed: \"~s\"~n", [T]),
-    catch _:_ ->
+                    default;
-            %% Fallback to default language in case of errors (like Djando does)
+                _:_ -> default
-            interpolate_variables(String, Variables)
+            end
     end.
     end.
 
 
 interpolate_variables(Tpl, []) ->
 interpolate_variables(Tpl, []) ->
@@ -168,11 +211,10 @@ interpolate_variables(Tpl, Variables) ->
 interpolate_variables1(Tpl, Vars) ->
 interpolate_variables1(Tpl, Vars) ->
     %% pre-compile binary patterns?
     %% pre-compile binary patterns?
     case binary:split(Tpl, <<"{{">>) of
     case binary:split(Tpl, <<"{{">>) of
-        [NotFound] ->
+        [Tpl]=NoVars -> NoVars; %% need to enclose in list due to list tail call below..
-            [NotFound];
         [Pre, Post] ->
         [Pre, Post] ->
             case binary:split(Post, <<"}}">>) of
             case binary:split(Post, <<"}}">>) of
-                [_] -> throw({no_close_var, Post});
+                [_] -> throw({no_close_var, Tpl});
                 [Var, Post1] ->
                 [Var, Post1] ->
                     Var1 = string:strip(binary_to_list(Var)),
                     Var1 = string:strip(binary_to_list(Var)),
                     Value = orddict:fetch(Var1, Vars),
                     Value = orddict:fetch(Var1, Vars),

+ 8 - 6
src/erlydtl_scanner.erl

@@ -36,7 +36,7 @@
 %%%-------------------------------------------------------------------
 %%%-------------------------------------------------------------------
 -module(erlydtl_scanner).
 -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
 %% http://github.com/erlydtl/slex
 -slex_source(["src/erlydtl_scanner.slex"]).
 -slex_source(["src/erlydtl_scanner.slex"]).
 
 
@@ -87,9 +87,11 @@ is_keyword(any, "as") -> true;
 is_keyword(any, "by") -> true;
 is_keyword(any, "by") -> true;
 is_keyword(any, "with") -> true;
 is_keyword(any, "with") -> true;
 is_keyword(any, "from") -> 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, "only") -> true;
 is_keyword(close, "parsed") -> true;
 is_keyword(close, "parsed") -> true;
-is_keyword(close, "noop") -> true;
 is_keyword(close, "reversed") -> true;
 is_keyword(close, "reversed") -> true;
 is_keyword(close, "openblock") -> true;
 is_keyword(close, "openblock") -> true;
 is_keyword(close, "closeblock") -> true;
 is_keyword(close, "closeblock") -> true;
@@ -138,6 +140,7 @@ is_keyword(open, "trans") -> true;
 is_keyword(open, "blocktrans") -> true;
 is_keyword(open, "blocktrans") -> true;
 is_keyword(open, "endblocktrans") -> true;
 is_keyword(open, "endblocktrans") -> true;
 is_keyword(open, "load") -> true;
 is_keyword(open, "load") -> true;
+is_keyword(open, "plural") -> true;
 is_keyword(_, _) -> false.
 is_keyword(_, _) -> false.
 
 
 format_error({illegal_char, C}) ->
 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([H | T], S, {R, C} = P, {in_comment, E} = St) ->
     scan(T,
     scan(T,
 	 case S of
 	 case S of
-	   [{comment_inline, _, L} = M | Ss] ->
+	   [{comment_tag, _, L} = M | Ss] ->
 	       [setelement(3, M, [H | L]) | Ss];
 	       [setelement(3, M, [H | L]) | Ss];
 	   _ ->
 	   _ ->
-	       [{comment_inline, P, [H]} | post_process(S,
+	       [{comment_tag, P, [H]} | post_process(S, comment_tag)]
-							comment_inline)]
 	 end,
 	 end,
 	 case H of
 	 case H of
 	   $\n -> {R + 1, 1};
 	   $\n -> {R + 1, 1};
@@ -540,7 +542,7 @@ post_process(_, {string, _, L} = T, _) ->
     setelement(3, T, begin L1 = lists:reverse(L), L1 end);
     setelement(3, T, begin L1 = lists:reverse(L), L1 end);
 post_process(_, {string_literal, _, L} = T, _) ->
 post_process(_, {string_literal, _, L} = T, _) ->
     setelement(3, T, begin L1 = lists:reverse(L), L1 end);
     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);
     setelement(3, T, begin L1 = lists:reverse(L), L1 end);
 post_process(_, {number_literal, _, L} = T, _) ->
 post_process(_, {number_literal, _, L} = T, _) ->
     setelement(3, T,
     setelement(3, T,

+ 7 - 4
src/erlydtl_scanner.slex

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

+ 26 - 5
src/erlydtl_unparser.erl

@@ -5,23 +5,29 @@ unparse(DjangoParseTree) ->
     unparse(DjangoParseTree, []).
     unparse(DjangoParseTree, []).
 
 
 unparse([], Acc) ->
 unparse([], Acc) ->
-    lists:reverse(Acc);
+    lists:flatten(lists:reverse(Acc));
 unparse([{'extends', Value}|Rest], Acc) ->
 unparse([{'extends', Value}|Rest], Acc) ->
     unparse(Rest, [["{% extends ", unparse_value(Value), " %}"]|Acc]);
     unparse(Rest, [["{% extends ", unparse_value(Value), " %}"]|Acc]);
 unparse([{'autoescape', OnOrOff, Contents}|Rest], Acc) ->
 unparse([{'autoescape', OnOrOff, Contents}|Rest], Acc) ->
     unparse(Rest, [["{% autoescape ", unparse_identifier(OnOrOff), " %}", unparse(Contents), "{% endautoescape %}"]|Acc]);
     unparse(Rest, [["{% autoescape ", unparse_identifier(OnOrOff), " %}", unparse(Contents), "{% endautoescape %}"]|Acc]);
 unparse([{'block', Identifier, Contents}|Rest], Acc) ->
 unparse([{'block', Identifier, Contents}|Rest], Acc) ->
     unparse(Rest, [["{% block ", unparse_identifier(Identifier), " %}", unparse(Contents), "{% endblock %}"]|Acc]);
     unparse(Rest, [["{% block ", unparse_identifier(Identifier), " %}", unparse(Contents), "{% endblock %}"]|Acc]);
-unparse([{'blocktrans', [], Contents}|Rest], Acc) ->
+unparse([{'blocktrans', Args, Contents, undefined}|Rest], Acc) ->
-    unparse(Rest, [["{% blocktrans %}", unparse(Contents), "{% endblocktrans %}"]|Acc]);
+    unparse(Rest, [["{% blocktrans ", unparse_blocktrans_args(Args), "%}", unparse(Contents), "{% endblocktrans %}"]|Acc]);
-unparse([{'blocktrans', Args, Contents}|Rest], Acc) ->
+unparse([{'blocktrans', Args, Contents, PluralContents}|Rest], Acc) ->
-    unparse(Rest, [["{% blocktrans ", unparse_args(Args), " %}", unparse(Contents), "{% endblocktrans %}"]|Acc]);
+    unparse(Rest, [["{% blocktrans ", unparse_args(Args), " %}",
+                    unparse(Contents),
+                    "{% plural %}",
+                    unparse(PluralContents),
+                    "{% endblocktrans %}"]|Acc]);
 unparse([{'call', Identifier}|Rest], Acc) ->
 unparse([{'call', Identifier}|Rest], Acc) ->
     unparse(Rest, [["{% call ", unparse_identifier(Identifier), " %}"]|Acc]);
     unparse(Rest, [["{% call ", unparse_identifier(Identifier), " %}"]|Acc]);
 unparse([{'call', Identifier, With}|Rest], Acc) ->
 unparse([{'call', Identifier, With}|Rest], Acc) ->
     unparse(Rest, [["{% call ", unparse_identifier(Identifier), " with ", unparse_args(With), " %}"]|Acc]);
     unparse(Rest, [["{% call ", unparse_identifier(Identifier), " with ", unparse_args(With), " %}"]|Acc]);
 unparse([{'comment', Contents}|Rest], Acc) ->
 unparse([{'comment', Contents}|Rest], Acc) ->
     unparse(Rest, [["{% comment %}", unparse(Contents), "{% endcomment %}"]|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([{'cycle', Names}|Rest], Acc) ->
     unparse(Rest, [["{% cycle ", unparse(Names), " %}"]|Acc]);
     unparse(Rest, [["{% cycle ", unparse(Names), " %}"]|Acc]);
 unparse([{'cycle_compat', Names}|Rest], 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([], [atom_to_list(Name)|Acc]);
 unparse_cycle_compat_names([{identifier, _, Name}|Rest], Acc) ->
 unparse_cycle_compat_names([{identifier, _, Name}|Rest], Acc) ->
     unparse_cycle_compat_names(Rest, lists:reverse([atom_to_list(Name), ", "], 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").
 -include("include/erlydtl_ext.hrl").
 
 
 -record(phrase, {msgid :: string(),
 -record(phrase, {msgid :: string(),
-                 msgid_plural :: string() | undefined, %TODO
+                 msgid_plural :: string() | undefined,
-                 context :: string() | undefined,      %TODO
+                 context :: string() | undefined,
                  comment :: string() | undefined,
                  comment :: string() | undefined,
                  file :: string(),
                  file :: string(),
                  line :: non_neg_integer(),
                  line :: non_neg_integer(),
@@ -53,7 +53,7 @@
 -define(bail(Fmt, Args),
 -define(bail(Fmt, Args),
         throw(lists:flatten(io_lib:format(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
 %% API Functions
@@ -73,19 +73,17 @@ process_content(Path, Content) ->
 %% @doc convert new API output to old one.
 %% @doc convert new API output to old one.
 -spec to_compat([phrase()]) -> [compat_phrase()].
 -spec to_compat([phrase()]) -> [compat_phrase()].
 to_compat(Phrases) ->
 to_compat(Phrases) ->
-    Convert = fun(#phrase{msgid=Str, file=File, line=Line, col=Col}) ->
+    [{Str, {File, Line, Col}}
-                      {Str, {File, Line, Col}}
+     || #phrase{msgid=Str, file=File, line=Line, col=Col}
-              end,
+            <- Phrases].
-    lists:map(Convert, Phrases).
 
 
 %% New API
 %% New API
 
 
 %% @doc extract info about phrase.
 %% @doc extract info about phrase.
 %% See `field()' type for list of available info field names.
 %% See `field()' type for list of available info field names.
--spec phrase_info([field()] | field(), phrase()) -> [Info] | Info
+-spec phrase_info([field()] | field(), phrase()) -> [Info] | Info when
-                                                        when
       Info :: non_neg_integer() | string() | undefined.
       Info :: non_neg_integer() | string() | undefined.
-?GET_FIELD(msgid);                                  %little magick
+?GET_FIELD(msgid);
 ?GET_FIELD(msgid_plural);
 ?GET_FIELD(msgid_plural);
 ?GET_FIELD(context);
 ?GET_FIELD(context);
 ?GET_FIELD(comment);
 ?GET_FIELD(comment);
@@ -94,16 +92,16 @@ to_compat(Phrases) ->
 ?GET_FIELD(col);
 ?GET_FIELD(col);
 phrase_info(Fields, Phrase) when is_list(Fields) ->
 phrase_info(Fields, Phrase) when is_list(Fields) ->
     %% you may pass list of 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
 %% @doc list files, using wildcard and extract phrases from them
 -spec parse_pattern([string()]) -> [phrase()].
 -spec parse_pattern([string()]) -> [phrase()].
 parse_pattern(Pattern) ->
 parse_pattern(Pattern) ->
     %%We assume a basedir
     %%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),
     Files = lists:foldl(GetFiles,[],Pattern),
     io:format("Parsing files ~p~n",[Files]),
     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).
     lists:flatten(ParsedFiles).
 
 
 %% @doc extract phrases from single file
 %% @doc extract phrases from single file
@@ -120,8 +118,13 @@ parse_file(Path) ->
 parse_content(Path,Content)->
 parse_content(Path,Content)->
     case erlydtl_compiler:do_parse_template(Content, #dtl_context{}) of
     case erlydtl_compiler:do_parse_template(Content, #dtl_context{}) of
         {ok, Data} ->
         {ok, Data} ->
-            {ok, Result} = process_ast(Path, Data),
+            try process_ast(Path, Data) of
-            Result;
+                {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 ->
         Error ->
             ?bail("Template parsing failed for template ~s, cause ~p~n", [Path, Error])
             ?bail("Template parsing failed for template ~s, cause ~p~n", [Path, Error])
     end.
     end.
@@ -132,26 +135,48 @@ parse_content(Path,Content)->
 %%
 %%
 
 
 process_ast(Fname, Tokens) ->
 process_ast(Fname, Tokens) ->
-    {ok, (process_ast(Fname, Tokens, #state{}))#state.acc }.
+    State = process_ast(Fname, Tokens, #state{}),
-process_ast(_Fname, [], St) -> St;
+    {ok, State#state.acc}.
-process_ast(Fname,[Head|Tail], St) ->
+
-    NewSt = process_token(Fname,Head,St),
+process_ast(Fname, Tokens, State) when is_list(Tokens) ->
-    process_ast(Fname, Tail, NewSt).
+    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
 %%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, {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),
     Phrase = #phrase{msgid=unescape(String),
                      comment=Comment,
                      comment=Comment,
                      file=Fname,
                      file=Fname,
                      line=Line,
                      line=Line,
                      col=Col},
                      col=Col},
     St#state{acc=[Phrase | Acc], translators_comment=undefined};
     St#state{acc=[Phrase | Acc], translators_comment=undefined};
-process_token(_Fname, {apply_filter, _Value, _Filter}, St) -> St;
+process_token(Fname,
-process_token(_Fname, {date, now, _Filter}, St) -> St;
+              {trans,Text,{string_literal, _, Context}},
-process_token(Fname, {blocktrans, Args, Contents}, #state{acc=Acc, translators_comment=Comment}=St) ->
+              #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),
     {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,
                      comment=Comment,
                      file=Fname,
                      file=Fname,
                      line=Line,
                      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};
     St#state{acc=[Phrase | Acc], translators_comment=undefined};
 process_token(_, {comment, Comment}, St) ->
 process_token(_, {comment, Comment}, St) ->
     St#state{translators_comment=maybe_translators_comment(Comment)};
     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}, St) -> process_ast(Fname, Children, St);
 process_token(Fname, {_Instr, _Cond, Children, Children2}, St) ->
 process_token(Fname, {_Instr, _Cond, Children, Children2}, St) ->
     StModified = process_ast(Fname, Children, St),
     StModified = process_ast(Fname, Children, St),
     process_ast(Fname, Children2, StModified);
     process_ast(Fname, Children2, StModified);
 process_token(_,_AST,St) -> St.
 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
 %% hack to guess ~position of blocktrans
 guess_blocktrans_lc(Fname, [{{identifier, {L, C}, _}, _} | _], _) ->
 guess_blocktrans_lc(Fname, [{{identifier, {L, C}, _}, _} | _], _) ->
@@ -182,19 +216,15 @@ guess_blocktrans_lc(Fname, _, _) ->
 
 
 
 
 maybe_translators_comment([{string, _Pos, S}]) ->
 maybe_translators_comment([{string, _Pos, S}]) ->
-    %% fast path
+    translators_comment_text(S);
-    case is_translators(S) of
-        true -> S;
-        false -> undefined
-    end;
 maybe_translators_comment(Other) ->
 maybe_translators_comment(Other) ->
     %% smth like "{%comment%}Translators: Hey, {{var}} is variable substitution{%endcomment%}"
     %% smth like "{%comment%}Translators: Hey, {{var}} is variable substitution{%endcomment%}"
-    Unparsed = lists:flatten(erlydtl_unparser:unparse(Other)),
+    Unparsed = erlydtl_unparser:unparse(Other),
-    case is_translators(Unparsed) of
+    translators_comment_text(Unparsed).
-        true -> Unparsed;
-        false -> undefined
-    end.
 
 
-is_translators(S) ->
+translators_comment_text(S) ->
     Stripped = string:strip(S, left),
     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
         error_ok -> ok
     end.
     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) ->
 run_compile(T) ->
     case erlydtl:compile(
     case erlydtl:compile(
-           T#test.source, T#test.module,
+           T#test.source,
-           [{vars, T#test.compile_vars}|T#test.compile_opts]) of
+           T#test.module,
+           compile_opts(T))
+    of
         {ok, M, W} ->
         {ok, M, W} ->
             ?assertEqual(T#test.module, M),
             ?assertEqual(T#test.module, M),
             ?assertEqual(T#test.warnings, W);
             ?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 %}">>,
         <<"{% for outer in list %}{% for inner in outer %}{{ inner }}\n{% endfor %}{% endfor %}">>,
         [{'list', [["Al", "Albert"], ["Jo", "Joseph"]]}],
         [{'list', [["Al", "Albert"], ["Jo", "Joseph"]]}],
         <<"Al\nAlbert\nJo\nJoseph\n">>},
         <<"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",
        {"Access parent loop counters",
         <<"{% for outer in list %}{% for inner in outer %}({{ forloop.parentloop.counter0 }}, {{ forloop.counter0 }})\n{% endfor %}{% endfor %}">>,
         <<"{% 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">>,
         [{'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",
        {"trans functional reverse locale",
         <<"Hello {% trans \"Hi\" %}">>, [], [{locale, "reverse"}],
         <<"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">>
         <<"Hello iH">>
        },
        },
        {"trans literal at run-time",
        {"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 {% trans var1 noop %}">>, [{var1, <<"Hi">>}], [{translation_fun, fun(<<"Hi">>) -> <<"Konichiwa">> end}], [],
         <<"Hello Hi">>},
         <<"Hello Hi">>},
        {"trans as",
        {"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",
       [{"blocktrans default locale",
       [{"blocktrans default locale",
         <<"{% blocktrans %}Hello{% endblocktrans %}">>, [], <<"Hello">>},
         <<"{% blocktrans %}Hello{% endblocktrans %}">>, [], <<"Hello">>},
        {"blocktrans choose locale",
        {"blocktrans choose locale",
         <<"{% blocktrans %}Hello, {{ name }}{% endblocktrans %}">>, [{name, "Mr. President"}], [{locale, "de"}],
         <<"{% blocktrans %}Hello, {{ name }}{% endblocktrans %}">>, [{name, "Mr. President"}], [{locale, "de"}],
-        [{blocktrans_locales, ["de"]}, {blocktrans_fun, fun("Hello, {{ name }}", "de") -> <<"Guten tag, {{ name }}">> end}], <<"Guten tag, Mr. President">>},
+        [{locales, ["de"]}, {translation_fun, fun("Hello, {{ name }}", "de") -> <<"Guten tag, {{ name }}">> end}], <<"Guten tag, Mr. President">>},
        {"blocktrans with args",
        {"blocktrans with args",
         <<"{% blocktrans with var1=foo %}{{ var1 }}{% endblocktrans %}">>, [{foo, "Hello"}], <<"Hello">>},
         <<"{% blocktrans with var1=foo %}{{ var1 }}{% endblocktrans %}">>, [{foo, "Hello"}], <<"Hello">>},
        #test{
        #test{
@@ -1316,6 +1326,98 @@ all_test_defs() ->
         [{translation_fun, fun("Hello, {{ name }}! See {{ v1 }}.") -> <<"Guten tag, {{name}}! Sehen {{    v1   }}.">> end}],
         [{translation_fun, fun("Hello, {{ name }}! See {{ v1 }}.") -> <<"Guten tag, {{name}}! Sehen {{    v1   }}.">> end}],
         [], <<"Guten tag, Mr. President! Sehen rubber-duck.">>}
         [], <<"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",
      {"verbatim",
       [{"Plain verbatim",
       [{"Plain verbatim",
         <<"{% verbatim %}{{ oh no{% foobar %}{% endverbatim %}">>, [],
         <<"{% verbatim %}{{ oh no{% foobar %}{% endverbatim %}">>, [],
@@ -1459,6 +1561,15 @@ all_test_defs() ->
        {"ssi file not found",
        {"ssi file not found",
         <<"{% ssi 'foo' %}">>, [],
         <<"{% ssi 'foo' %}">>, [],
         {error, {read_file, <<"./foo">>, enoent}}
         {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",
      {"load",
@@ -1479,6 +1590,62 @@ all_test_defs() ->
         <<"ytrewQ">>
         <<"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",
       [functional_test(F)
       [functional_test(F)
        %% order is important.
        %% order is important.
@@ -1507,7 +1674,8 @@ all_test_defs() ->
                    renderer = fun(#test{ module=M, render_vars=V, render_opts=O }) ->
                    renderer = fun(#test{ module=M, render_vars=V, render_opts=O }) ->
                                       M:render(base1, V, O)
                                       M:render(base1, V, O)
                               end
                               end
-                  }]
+                  }
+               ]
       ]}
       ]}
     ].
     ].
 
 
@@ -1530,6 +1698,7 @@ def_to_test(Group, {Name, DTL, Vars, RenderOpts, CompilerOpts, Output, Warnings}
        source = {template, DTL},
        source = {template, DTL},
        render_vars = Vars,
        render_vars = Vars,
        render_opts = RenderOpts,
        render_opts = RenderOpts,
+       compile_vars = undefined,
        compile_opts = CompilerOpts ++ (#test{})#test.compile_opts,
        compile_opts = CompilerOpts ++ (#test{})#test.compile_opts,
        output = Output,
        output = Output,
        warnings = Warnings
        warnings = Warnings

+ 156 - 6
test/sources_parser_tests.erl

@@ -1,15 +1,12 @@
 -module(sources_parser_tests).
 -module(sources_parser_tests).
 
 
 -include_lib("eunit/include/eunit.hrl").
 -include_lib("eunit/include/eunit.hrl").
+-include("include/erlydtl_ext.hrl").
 
 
 all_sources_parser_test_() ->
 all_sources_parser_test_() ->
     [{Title, [test_fun(Test) || Test <- Tests]}
     [{Title, [test_fun(Test) || Test <- Tests]}
      || {Title, Tests} <- test_defs()].
      || {Title, Tests} <- test_defs()].
 
 
-all_sources_parser_ext_test_() ->
-    [test_ext_fun(Test) || Test <- ext_test_defs()].
-
-
 test_fun({Name, Content, Output}) ->
 test_fun({Name, Content, Output}) ->
     {Name, fun () ->
     {Name, fun () ->
                    Tokens = (catch sources_parser:process_content("dummy_path", Content)),
                    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}}]},
         [{"Hello inside an if inside a for",{"dummy_path",1,73}}]},
        {"if and else both with trans",
        {"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>">>,
         <<"<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}}) ->
 test_ext_fun({Name, Tpl, {Fields, Output}}) ->
     {Name, fun() ->
     {Name, fun() ->
                    Tokens = [sources_parser:phrase_info(Fields, P)
                    Tokens = [sources_parser:phrase_info(Fields, P)
@@ -56,5 +56,155 @@ ext_test_defs() ->
       {[msgid, comment], [["phrase", "translators: com{{ me }}nt"]]}},
       {[msgid, comment], [["phrase", "translators: com{{ me }}nt"]]}},
      {"blocktrans with comments",
      {"blocktrans with comments",
       <<"{%comment%}translators: comment{%endcomment%}{%blocktrans with a=b%}B={{b}}{%endblocktrans%}">>,
       <<"{%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)].