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

Merge pull request #219 from oxpa/module_trans

pass translation function into 'date' filter.
Andreas Stenius 9 лет назад
Родитель
Сommit
ce2ba828fc
5 измененных файлов с 319 добавлено и 103 удалено
  1. 40 4
      README.markdown
  2. 19 4
      src/erlydtl_beam_compiler.erl
  3. 16 1
      src/erlydtl_filters.erl
  4. 139 88
      src/filter_lib/erlydtl_dateformat.erl
  5. 105 6
      test/erlydtl_test_defs.erl

+ 40 - 4
README.markdown

@@ -281,8 +281,10 @@ Options is a proplist possibly containing:
     when Locale::string(), Context::string().
   ```
 
+  Please, keep in mind, that if your templates where not specially 
+  designed, you probably still need render time translations.
   See description of the `translation_fun` render option for more
-  details on the translation `context`.
+  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
@@ -412,9 +414,43 @@ Same as `render/1`, but with the following options:
   {% 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.
+  Render time translation function is also used to translate dates.
+  Date tokens mimics those used in django, so you may reuse django translations.
+  Tokens may appear in a date are:
+  1. Full months names, capitalized ("January" .. "December");
+  2. 3 letters months names ("jan" .. "dec");
+  3. Associated Press style months 
+     ("Jan." .. "Dec." with "abbrev.month" context);
+  4. Alternative month name, for "E" option
+     ("January" .. "December" with "alt. month" context);
+  5. Full week day names, capitalized ("Monday" .. "Sunday");
+  6. Abbreviated week day names, capitalized ("Mon" .. "Sun");
+  7. day time tokens ("AM", "PM", "a.m.", "p.m.", "noon", "midnight");
+
+  While date token values may be passed as lists, consider using binaries 
+  as a default string format.
+  Here is a simple but robust translation function stub:
+  ```erlang
+     translation_placeholder({Val,{Plural, Count}}, {L, C}) when is_list(Val) ->
+       translation_placeholder({list_to_binary(Val),{Plural, Count}}, LC);
+     translation_placeholder(Val, {L, C}) when is_list(Val) ->
+       translation_placeholder(list_to_binary(Val), LC);
+     translation_placeholder(Val, {L, C}) when is_list(C) ->
+       translation_placeholder(Val, list_to_binary(C));
+     translation_placeholder(Val, {L, C}) when is_list(C) ->
+       io:format("Translating ~p into ~p with context ~p~n", [Val, L, C]),
+       %% do nothing and return original value
+       Val.
+  ```
+
+  The translation fun can also be 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).
+
+  
 
 * `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,

+ 19 - 4
src/erlydtl_beam_compiler.erl

@@ -1120,13 +1120,28 @@ filter_ast1({{identifier, Pos, Name}, Args}, ValueAst, TreeWalker) ->
             empty_ast(?WARN({Pos, Error}, TreeWalker1))
     end.
 
-filter_ast2(Name, Args, #dtl_context{ filters = Filters }) ->
+% special case for date, which reqires localisation
+% may be replaced later with a query to a list 
+% of functions which require translation
+filter_ast2('date' = Name, Args, #dtl_context{ filters = Filters } = Ctx) ->
+    case proplists:get_value(Name, Filters) of
+        {Mod, Fun} -> 
+            case erlang:function_exported(Mod, Fun, length(Args) + 2) of
+                true -> {ok, ?Q("'@Mod@':'@Fun@'(_@Args, _TranslationFun, _CurrentLocale )")};
+                false -> filter_ast3(Name, Args, Ctx) % redefined 'date'?
+            end;
+        % should never happen
+        undefined -> {unknown_filter, Name, length(Args)}
+    end;
+filter_ast2(Name, Args, Ctx) ->
+    filter_ast3(Name, Args, Ctx).
+
+filter_ast3(Name, Args, #dtl_context{ filters = Filters }) ->
     case proplists:get_value(Name, Filters) of
         {Mod, Fun}=Filter ->
             case erlang:function_exported(Mod, Fun, length(Args)) of
                 true -> {ok, ?Q("'@Mod@':'@Fun@'(_@Args)")};
-                false ->
-                    {filter_args, Name, Filter, length(Args)}
+                false -> {filter_args, Name, Filter, length(Args)}
             end;
         undefined ->
             {unknown_filter, Name, length(Args)}
@@ -1483,7 +1498,7 @@ now_ast(FormatString, TreeWalker) ->
     %% i.e. \"foo\" becomes "foo"
     UnescapeOuter = string:strip(FormatString, both, 34),
     {{StringAst, Info}, TreeWalker1} = string_ast(UnescapeOuter, TreeWalker),
-    {{?Q("erlydtl_dateformat:format(_@StringAst)"), Info}, TreeWalker1}.
+    {{?Q("erlydtl_dateformat:format(_@StringAst, _TranslationFun, _CurrentLocale)"), Info}, TreeWalker1}.
 
 spaceless_ast(Contents, TreeWalker) ->
     {{Ast, Info}, TreeWalker1} = body_ast(Contents, TreeWalker),

+ 16 - 1
src/erlydtl_filters.erl

@@ -56,6 +56,8 @@
         cut/2,
         date/1,
         date/2,
+        date/3,
+        date/4,
         default/2,
         default_if_none/2,
         dictsort/2,
@@ -234,7 +236,7 @@ cut(Input, Arg) when is_binary(Input) ->
 cut(Input, [Char]) when is_list(Input) ->
     cut(Input, Char, []).
  
-%% @doc Formats a date according to the default format.
+%% @doc Formats a date according to the default format. 
 date(Input) ->
     date(Input, "F j, Y").
 
@@ -247,6 +249,19 @@ date(Input, _FormatStr) ->
     io:format("Unexpected date parameter: ~p~n", [Input]),
     "".
 
+%% @doc Formats a date according to the default format 
+%%      localizing it with provided translation function.
+date(Input, TransFun, Locale) ->
+    date(Input, "F j, Y", TransFun, Locale).
+date(Input, FormatStr, TransFun, Locale)
+  when is_tuple(Input)
+       andalso (size(Input) == 2 orelse size(Input) == 3) ->
+    erlydtl_dateformat:format(Input, FormatStr, TransFun, Locale);
+date(Input, _FormatStr, _TransFun, _Locale) ->
+    io:format("Unexpected date parameter: ~p~n", [Input]),
+    "".
+
+
 %% @doc If value evaluates to `false', use given default. Otherwise, use the value.
 default(Input, Default) ->
     case erlydtl_runtime:is_false(Input) of

+ 139 - 88
src/filter_lib/erlydtl_dateformat.erl

@@ -1,5 +1,5 @@
 -module(erlydtl_dateformat).
--export([format/1, format/2]).
+-export([format/1, format/2, format/3, format/4]).
 
 -define(TAG_SUPPORTED(C),
         C =:= $a orelse
@@ -9,6 +9,7 @@
         C =:= $c orelse
         C =:= $d orelse
         C =:= $D orelse
+        C =:= $E orelse
         C =:= $f orelse
         C =:= $F orelse
         C =:= $g orelse
@@ -45,41 +46,74 @@
 %% Format the current date/time
 %%
 format(FormatString) when is_binary(FormatString) ->
-    format(binary_to_list(FormatString));
+    format(binary_to_list(FormatString), fun stub_tran/2, <<>>);
 format(FormatString) ->
     {Date, Time} = erlang:localtime(),
-    replace_tags(Date, Time, FormatString).
+    replace_tags(Date, Time, FormatString, fun stub_tran/2, <<>>).
+
 %%
 %% Format a tuple of the form {{Y,M,D},{H,M,S}}
 %% This is the format returned by erlang:localtime()
 %% and other standard date/time BIFs
 %%
 format(DateTime, FormatString) when is_binary(FormatString) ->
-    format(DateTime, binary_to_list(FormatString));
+    format(DateTime, binary_to_list(FormatString),fun stub_tran/2, <<>>);
 format({{_,_,_} = Date,{_,_,_} = Time}, FormatString) ->
-    replace_tags(Date, Time, FormatString);
+    replace_tags(Date, Time, FormatString, fun stub_tran/2, <<>> );
 %%
 %% Format a tuple of the form {Y,M,D}
 %%
 format({_,_,_} = Date, FormatString) ->
-    replace_tags(Date, {0,0,0}, FormatString);
+    replace_tags(Date, {0,0,0}, FormatString, fun stub_tran/2, <<>> );
 format(DateTime, FormatString) ->
     io:format("Unrecognised date paramater : ~p~n", [DateTime]),
     FormatString.
 
-replace_tags(Date, Time, Input) ->
-    replace_tags(Date, Time, Input, [], noslash).
-replace_tags(_Date, _Time, [], Out, _State) ->
+%% The same set of functions with TranslationFunction and Locale args
+%% Translation function may be 'none' atom - handle this separately
+%% replacing atom with a stub function (it's easier to do it this way)
+format(FormatString, TransFun, Locale) when is_binary(FormatString) ->
+    format(binary_to_list(FormatString), TransFun, Locale);
+format(FormatString, none, _Locale) -> 
+    format(FormatString, fun stub_tran/2, <<>>);
+format(FormatString, TransFun, Locale) ->
+    {Date, Time} = erlang:localtime(),
+    replace_tags(Date, Time, FormatString, TransFun, Locale).
+
+format(DateTime, FormatString, TransFun, Locale) when is_binary(FormatString) ->
+    format(DateTime, binary_to_list(FormatString), TransFun, Locale);
+format(DateTime, FormatString, none, _Locale) ->
+    format(DateTime, FormatString, fun stub_tran/2, <<>>);
+format({{_,_,_} = Date,{_,_,_} = Time}, FormatString, TransFun, Locale) ->
+    replace_tags(Date, Time, FormatString, TransFun, Locale );
+
+format({_,_,_} = Date, FormatString, none, _Locale) ->
+    replace_tags(Date, {0,0,0}, FormatString, fun stub_tran/2, <<>>);
+format({_,_,_} = Date, FormatString, TransFun, Locale) ->
+    replace_tags(Date, {0,0,0}, FormatString, TransFun, Locale);
+format(DateTime, FormatString, _TransFun, _Locale) ->
+    io:format("Unrecognised date paramater : ~p~n", [DateTime]),
+    FormatString.
+
+replace_tags(Date, Time, Input, TransFun, Locale) ->
+    replace_tags(Date, Time, Input, [], noslash, TransFun, Locale).
+replace_tags(_Date, _Time, [], Out, _State, _TransFun, _Locale) ->
     lists:reverse(Out);
-replace_tags(Date, Time, [C|Rest], Out, noslash) when ?TAG_SUPPORTED(C) ->
-    replace_tags(Date, Time, Rest,
-                 lists:reverse(tag_to_value(C, Date, Time)) ++ Out, noslash);
-replace_tags(Date, Time, [$\\|Rest], Out, noslash) ->
-    replace_tags(Date, Time, Rest, Out, slash);
-replace_tags(Date, Time, [C|Rest], Out, slash) ->
-    replace_tags(Date, Time, Rest, [C|Out], noslash);
-replace_tags(Date, Time, [C|Rest], Out, _State) ->
-    replace_tags(Date, Time, Rest, [C|Out], noslash).
+replace_tags(Date, Time, [C|Rest], Out, noslash, TransFun, Locale) when ?TAG_SUPPORTED(C) ->
+    case tag_to_value(C, Date, Time, TransFun, Locale) of
+        V when is_binary(V) -> replace_tags(Date, Time, Rest, 
+                                            [V] ++ Out, noslash, 
+                                            TransFun, Locale);
+        V when is_list(V) ->   replace_tags(Date, Time, Rest, 
+                                            lists:reverse(V) ++ Out, 
+                                            noslash, TransFun, Locale)
+    end;
+replace_tags(Date, Time, [$\\|Rest], Out, noslash, TransFun, Locale) ->
+    replace_tags(Date, Time, Rest, Out, slash, TransFun, Locale);
+replace_tags(Date, Time, [C|Rest], Out, slash, TransFun, Locale) ->
+    replace_tags(Date, Time, Rest, [C|Out], noslash, TransFun, Locale);
+replace_tags(Date, Time, [C|Rest], Out, _State, TransFun, Locale) ->
+    replace_tags(Date, Time, Rest, [C|Out], noslash, TransFun, Locale).
 
 
 %%-----------------------------------------------------------
@@ -87,25 +121,29 @@ replace_tags(Date, Time, [C|Rest], Out, _State) ->
 %%-----------------------------------------------------------
 
 %% 'a.m.' or 'p.m.'
-tag_to_value($a, _, {H, _, _}) when H > 11 -> "p.m.";
-tag_to_value($a, _, _) -> "a.m.";
+tag_to_value($a, _, {H, _, _}, TransFun, Locale) when H > 11 -> 
+    TransFun("p.m.", Locale);
+tag_to_value($a, _, _, TransFun, Locale) -> 
+    TransFun("a.m.", Locale);
 
 %% 'AM' or 'PM'
-tag_to_value($A, _, {H, _, _}) when H > 11 -> "PM";
-tag_to_value($A, _, _) -> "AM";
+tag_to_value($A, _, {H, _, _}, TransFun, Locale) when H > 11 -> 
+    TransFun("PM", Locale);
+tag_to_value($A, _, _, TransFun, Locale) -> 
+    TransFun("AM", Locale);
 
 %% Swatch Internet time
-tag_to_value($B, _, _) ->
+tag_to_value($B, _, _, _TransFun, _Locale) ->
     ""; %% NotImplementedError
 
 %% ISO 8601 Format.
-tag_to_value($c, Date, Time) ->
-    tag_to_value($Y, Date, Time) ++
-        "-" ++ tag_to_value($m, Date, Time) ++
-        "-" ++ tag_to_value($d, Date, Time) ++
-        "T" ++ tag_to_value($H, Date, Time) ++
-        ":" ++ tag_to_value($i, Date, Time) ++
-        ":" ++ tag_to_value($s, Date, Time);
+tag_to_value($c, Date, Time, TransFun, Locale) ->
+    tag_to_value($Y, Date, Time, TransFun, Locale) ++
+        "-" ++ tag_to_value($m, Date, Time, TransFun, Locale) ++
+        "-" ++ tag_to_value($d, Date, Time, TransFun, Locale) ++
+        "T" ++ tag_to_value($H, Date, Time, TransFun, Locale) ++
+        ":" ++ tag_to_value($i, Date, Time, TransFun, Locale) ++
+        ":" ++ tag_to_value($s, Date, Time, TransFun, Locale);
 
 %%
 %% Time, in 12-hour hours and minutes, with minutes
@@ -115,46 +153,48 @@ tag_to_value($c, Date, Time) ->
 %%
 %% Proprietary extension.
 %%
-tag_to_value($f, Date, {H, 0, S}) ->
+tag_to_value($f, Date, {H, 0, S}, TransFun, Locale) ->
     %% If min is zero then return the hour only
-    tag_to_value($g, Date, {H, 0, S});
-tag_to_value($f, Date, Time) ->
+    tag_to_value($g, Date, {H, 0, S}, TransFun, Locale);
+tag_to_value($f, Date, Time, TransFun, Locale) ->
     %% Otherwise return hours and mins
-    tag_to_value($g, Date, Time)
-        ++ ":" ++ tag_to_value($i, Date, Time);
+    tag_to_value($g, Date, Time, TransFun, Locale)
+        ++ ":" ++ tag_to_value($i, Date, Time, TransFun, Locale);
 
 %% Hour, 12-hour format without leading zeros; i.e. '1' to '12'
-tag_to_value($g, _, {H,_,_}) ->
+tag_to_value($g, _, {H,_,_}, _TransFun, _Locale) ->
     integer_to_list(hour_24to12(H));
 
 %% Hour, 24-hour format without leading zeros; i.e. '0' to '23'
-tag_to_value($G, _, {H,_,_}) ->
+tag_to_value($G, _, {H,_,_}, _TransFun, _Locale) ->
     integer_to_list(H);
 
 %% Hour, 12-hour format; i.e. '01' to '12'
-tag_to_value($h, _, {H,_,_}) ->
+tag_to_value($h, _, {H,_,_}, _TransFun, _Locale) ->
     integer_to_list_zerofill(hour_24to12(H));
 
 %% Hour, 24-hour format; i.e. '00' to '23'
-tag_to_value($H, _, {H,_,_}) ->
+tag_to_value($H, _, {H,_,_}, _TransFun, _Locale) ->
     integer_to_list_zerofill(H);
 
 %% Minutes; i.e. '00' to '59'
-tag_to_value($i, _, {_,M,_}) ->
+tag_to_value($i, _, {_,M,_}, _TransFun, _Locale) ->
     integer_to_list_zerofill(M);
 
 %% Time, in 12-hour hours, minutes and 'a.m.'/'p.m.', with minutes left off
 %% if they're zero and the strings 'midnight' and 'noon' if appropriate.
 %% Examples: '1 a.m.', '1:30 p.m.', 'midnight', 'noon', '12:30 p.m.'
 %% Proprietary extension.
-tag_to_value($P, _, {0,  0, _}) -> "midnight";
-tag_to_value($P, _, {12, 0, _}) -> "noon";
-tag_to_value($P, Date, Time) ->
-    tag_to_value($f, Date, Time)
-        ++ " " ++ tag_to_value($a, Date, Time);
+tag_to_value($P, _, {0,  0, _}, TransFun, Locale) -> 
+    TransFun("midnight", Locale);
+tag_to_value($P, _, {12, 0, _}, TransFun, Locale) -> 
+    TransFun("noon", Locale);
+tag_to_value($P, Date, Time, TransFun, Locale) ->
+    tag_to_value($f, Date, Time, TransFun, Locale)
+        ++ " " ++ tag_to_value($a, Date, Time, TransFun, Locale);
 
 %% Seconds; i.e. '00' to '59'
-tag_to_value($s, _, {_,_,S}) ->
+tag_to_value($s, _, {_,_,S}, _TransFun, _Locale) ->
     integer_to_list_zerofill(S);
 
 %%-----------------------------------------------------------
@@ -162,67 +202,74 @@ tag_to_value($s, _, {_,_,S}) ->
 %%-----------------------------------------------------------
 
 %% Month, textual, 3 letters, lowercase; e.g. 'jan'
-tag_to_value($b, {_,M,_}, _) ->
-    string:sub_string(monthname(M), 1, 3);
+tag_to_value($b, {_,M,_}, _, TransFun, Locale) ->
+    TransFun(string:sub_string(monthname(M), 1, 3), Locale);
 
 %% Day of the month, 2 digits with leading zeros; i.e. '01' to '31'
-tag_to_value($d, {_, _, D}, _) ->
+tag_to_value($d, {_, _, D}, _, _TransFun, _Locale) ->
     integer_to_list_zerofill(D);
 
 %% Day of the week, textual, 3 letters; e.g. 'Fri'
-tag_to_value($D, Date, _) ->
+tag_to_value($D, Date, _, TransFun, Locale) ->
     Dow = calendar:day_of_the_week(Date),
-    ucfirst(string:sub_string(dayname(Dow), 1, 3));
+    TransFun(ucfirst(string:sub_string(dayname(Dow), 1, 3)), Locale);
+
+%% Month, textual, long, alternative; e.g. 'Listopada'
+tag_to_value($E, {_,M,_}, _, TransFun, Locale) ->
+    TransFun(ucfirst(monthname(M)), {Locale, <<"alt. month">>});
+
 
 %% Month, textual, long; e.g. 'January'
-tag_to_value($F, {_,M,_}, _) ->
-    ucfirst(monthname(M));
+tag_to_value($F, {_,M,_}, _, TransFun, Locale) ->
+    TransFun(ucfirst(monthname(M)), Locale);
 
 %% '1' if Daylight Savings Time, '0' otherwise.
-tag_to_value($I, _, _) ->
+tag_to_value($I, _, _, _TransFun, _Locale) ->
     "TODO";
 
 %% Day of the month without leading zeros; i.e. '1' to '31'
-tag_to_value($j, {_, _, D}, _) ->
+tag_to_value($j, {_, _, D}, _, _TransFun, _Locale) ->
     integer_to_list(D);
 
 %% Day of the week, textual, long; e.g. 'Friday'
-tag_to_value($l, Date, _) ->
-    ucfirst(dayname(calendar:day_of_the_week(Date)));
+tag_to_value($l, Date, _, TransFun, Locale) ->
+    TransFun(ucfirst(dayname(calendar:day_of_the_week(Date))), Locale);
 
 %% Boolean for whether it is a leap year; i.e. True or False
-tag_to_value($L, {Y,_,_}, _) ->
+tag_to_value($L, {Y,_,_}, _, _TransFun, _Locale) ->
     case calendar:is_leap_year(Y) of
         true -> "True";
         _ -> "False"
     end;
 
 %% Month; i.e. '01' to '12'
-tag_to_value($m, {_, M, _}, _) ->
+tag_to_value($m, {_, M, _}, _, _TransFun, _Locale) ->
     integer_to_list_zerofill(M);
 
 %% Month, textual, 3 letters; e.g. 'Jan'
-tag_to_value($M, {_,M,_}, _) ->
-    ucfirst(string:sub_string(monthname(M), 1, 3));
+tag_to_value($M, {_,M,_}, _, TransFun, Locale) ->
+    TransFun(ucfirst(string:sub_string(monthname(M), 1, 3)), Locale);
 
 %% Month without leading zeros; i.e. '1' to '12'
-tag_to_value($n, {_, M, _}, _) ->
+tag_to_value($n, {_, M, _}, _, _TransFun, _Locale) ->
     integer_to_list(M);
 
 %% Month abbreviation in Associated Press style. Proprietary extension.
-tag_to_value($N, {_,M,_}, _) when M =:= 9 ->
+tag_to_value($N, {_,M,_}, _, TransFun, Locale) when M =:= 9 ->
     %% Special case - "Sept."
-    ucfirst(string:sub_string(monthname(M), 1, 4)) ++ ".";
-tag_to_value($N, {_,M,_}, _) when M < 3 orelse M > 7 ->
+    TransFun(ucfirst(string:sub_string(monthname(M), 1, 4)) ++ ".",
+             {Locale, <<"abbrev. month">>});
+tag_to_value($N, {_,M,_}, _, TransFun, Locale) when M < 3 orelse M > 7 ->
     %% Jan, Feb, Aug, Oct, Nov, Dec are all
     %% abbreviated with a full-stop appended.
-    ucfirst(string:sub_string(monthname(M), 1, 3)) ++ ".";
-tag_to_value($N, {_,M,_}, _) ->
+    TransFun(ucfirst(string:sub_string(monthname(M), 1, 3)) ++ ".",
+                 {Locale, <<"abbrev. month">>});
+tag_to_value($N, {_,M,_}, _, TransFun, Locale) ->
     %% The rest are the fullname.
-    ucfirst(monthname(M));
+    TransFun(ucfirst(monthname(M)), {Locale, <<"abbrev. month">>});
 
 %% Difference to Greenwich time in hours; e.g. '+0200'
-tag_to_value($O, Date, Time) ->
+tag_to_value($O, Date, Time, _TransFun, _Locale) ->
     Diff = utc_diff(Date, Time),
     Offset = if
                  Diff < 0 ->
@@ -233,67 +280,68 @@ tag_to_value($O, Date, Time) ->
     lists:flatten(Offset);
 
 %% RFC 2822 formatted date; e.g. 'Thu, 21 Dec 2000 16:01:07 +0200'
-tag_to_value($r, Date, Time) ->
-    replace_tags(Date, Time, "D, j M Y H:i:s O");
+tag_to_value($r, Date, Time, _TransFun, _Locale) ->
+    % afaik, date should not be translated in case RFC format is specified.
+    replace_tags(Date, Time, "D, j M Y H:i:s O", fun stub_tran/2, <<>> );
 
 %% English ordinal suffix for the day of the month, 2 characters;
 %% i.e. 'st', 'nd', 'rd' or 'th'
-tag_to_value($S, {_, _, D}, _) when
+tag_to_value($S, {_, _, D}, _, _TransFun, _Locale) when
       D rem 100 =:= 11 orelse
       D rem 100 =:= 12 orelse
       D rem 100 =:= 13 -> "th";
-tag_to_value($S, {_, _, D}, _) when D rem 10 =:= 1 -> "st";
-tag_to_value($S, {_, _, D}, _) when D rem 10 =:= 2 -> "nd";
-tag_to_value($S, {_, _, D}, _) when D rem 10 =:= 3 -> "rd";
-tag_to_value($S, _, _) -> "th";
+tag_to_value($S, {_, _, D}, _, _TransFun, _Locale) when D rem 10 =:= 1 -> "st";
+tag_to_value($S, {_, _, D}, _, _TransFun, _Locale) when D rem 10 =:= 2 -> "nd";
+tag_to_value($S, {_, _, D}, _, _TransFun, _Locale) when D rem 10 =:= 3 -> "rd";
+tag_to_value($S, _, _, _TransFun, _Locale) -> "th";
 
 %% Number of days in the given month; i.e. '28' to '31'
-tag_to_value($t, {Y,M,_}, _) ->
+tag_to_value($t, {Y,M,_}, _, _TransFun, _Locale) ->
     integer_to_list(calendar:last_day_of_the_month(Y,M));
 
 %% Time zone of this machine; e.g. 'EST' or 'MDT'
-tag_to_value($T, _, _) ->
+tag_to_value($T, _, _, _TransFun, _Locale) ->
     "TODO";
 
 %% Seconds since the Unix epoch (January 1 1970 00:00:00 GMT)
-tag_to_value($U, Date, Time) ->
+tag_to_value($U, Date, Time, _TransFun, _Locale) ->
     EpochSecs = calendar:datetime_to_gregorian_seconds({Date, Time})
         - calendar:datetime_to_gregorian_seconds({{1970,1,1},{0,0,0}}),
     integer_to_list(EpochSecs);
 
 %% Day of the week, numeric, i.e. '0' (Sunday) to '6' (Saturday)
-tag_to_value($w, Date, _) ->
+tag_to_value($w, Date, _, _TransFun, _Locale) ->
     %% Note: calendar:day_of_the_week returns
     %%   1 | .. | 7. Monday = 1, Tuesday = 2, ..., Sunday = 7
     integer_to_list(calendar:day_of_the_week(Date) rem 7);
 
 %% ISO-8601 week number of year, weeks starting on Monday
-tag_to_value($W, {Y,M,D}, _) ->
+tag_to_value($W, {Y,M,D}, _, _TransFun, _Locale) ->
     integer_to_list(year_weeknum(Y,M,D));
 
 %% Year, 2 digits; e.g. '99'
-tag_to_value($y, {Y, _, _}, _) ->
+tag_to_value($y, {Y, _, _}, _, _TransFun, _Locale) ->
     string:sub_string(integer_to_list(Y), 3);
 
 %% Year, 4 digits; e.g. '1999'
-tag_to_value($Y, {Y, _, _}, _) ->
+tag_to_value($Y, {Y, _, _}, _, _TransFun, _Locale) ->
     integer_to_list(Y);
 
 %% Day of the year; i.e. '0' to '365'
-tag_to_value($z, {Y,M,D}, _) ->
+tag_to_value($z, {Y,M,D}, _, _TransFun, _Locale) ->
     integer_to_list(day_of_year(Y,M,D));
 
 %% Time zone offset in seconds (i.e. '-43200' to '43200'). The offset for
 %% timezones west of UTC is always negative, and for those east of UTC is
 %% always positive.
-tag_to_value($Z, _, _) ->
+tag_to_value($Z, _, _, _TransFun, _Locale) ->
     "TODO";
 
 %% o – the ISO 8601 year number
-tag_to_value($o, {Y,M,D}, _) ->
+tag_to_value($o, {Y,M,D}, _, _TransFun, _Locale) ->
     integer_to_list(weeknum_year(Y,M,D));
 
-tag_to_value(C, Date, Time) ->
+tag_to_value(C, Date, Time, _TransFun, _Locale) ->
     io:format("Unimplemented tag : ~p [Date : ~p] [Time : ~p]",
               [C, Date, Time]),
     "".
@@ -384,4 +432,7 @@ ucfirst([First | Rest]) when First >= $a, First =< $z ->
 ucfirst(Other) ->
     Other.
 
-
+stub_tran(A,_) -> 
+    % userful for test debuggging
+    % io:format("calling stub translation!!!",[]),
+    A.

+ 105 - 6
test/erlydtl_test_defs.erl

@@ -1,3 +1,4 @@
+%% -*- coding: utf-8 -*-
 -module(erlydtl_test_defs).
 
 -export([tests/0, extra_reader/2]).
@@ -205,7 +206,11 @@ all_test_defs() ->
      {"now",
       [{"now functional",
         <<"It is the {% now \"jS \\o\\f F Y\" %}.">>, [{var1, ""}], generate_test_date()}
-      ]},
+     ]},
+      {"now",
+      [{"now function with translation", % notice, that only date output is traslated. While you might want to transle the whole format string ('F'->'E')
+        <<"It is the {% now \"jS \\o\\f F Y\" %}.">>, [{var1, ""}], [{locale, <<"ru">>}, {translation_fun, fun date_translation/2}], generate_test_date(russian)}
+     ]},
      {"if",
       [{"If/else",
         <<"{% if var1 %}boo{% else %}yay{% endif %}">>, [{var1, ""}], <<"yay">>},
@@ -562,6 +567,48 @@ all_test_defs() ->
         <<"{{ var1|date }}">>,
         [{var1, {{1975,7,24}, {7,13,1}}}],
         <<"July 24, 1975">>},
+        % I doubt someone need first two, but test we support it
+        {"|date a translation",
+        <<"{{ var1|date:\"a\" }}">>,
+        [{var1, {{1975,7,24},{12,00,00}}}],[{translation_fun, fun date_translation/2},{locale, <<"ru">>}],
+        <<"п.п."/utf8>>},
+        {"|date A translation",
+        <<"{{ var1|date:\"A\" }}">>,
+        [{var1, {{1975,7,24},{12,00,00}}}],[{translation_fun, fun date_translation/2},{locale, <<"ru">>}],
+        <<"ПП"/utf8>>},
+        {"|date b translation", 
+        <<"{{ var1|date:\"b\" }}">>,
+        [{var1, {1975,7,24}}],[{translation_fun, fun date_translation/2},{locale, <<"ru">>}],
+        <<"июл"/utf8>>},
+        {"|date D translation",
+        <<"{{ var1|date:\"D\" }}">>,
+        [{var1, {1975,7,24}}],[{translation_fun, fun date_translation/2},{locale, <<"ru">>}],
+        <<"Чтв"/utf8>>},
+        {"|date E translation",
+        <<"{{ var1|date:\"E\" }}">>,
+        [{var1, {1975,7,24}}],[{translation_fun, fun date_translation/2},{locale, <<"ru">>}],
+        <<"Июля"/utf8>>},
+        {"|date F translation",
+        <<"{{ var1|date:\"F\" }}">>,
+        [{var1, {1975,7,24}}],[{translation_fun, fun date_translation/2},{locale, <<"ru">>}],
+        <<"Июль"/utf8>>},
+        {"|date l translation",
+        <<"{{ var1|date:\"l\" }}">>,
+        [{var1, {1975,7,24}}],[{translation_fun, fun date_translation/2},{locale, <<"ru">>}],
+        <<"Четверг"/utf8>>},
+        {"|date M translation",
+        <<"{{ var1|date:\"M\" }}">>,
+        [{var1, {1986,9,24}}],[{translation_fun, fun date_translation/2},{locale, <<"ru">>}],
+        <<"Сен"/utf8>>},
+        {"|date N translation",
+        <<"{{ var1|date:\"N\" }}">>,
+        [{var1, {1986,9,24}}],[{translation_fun, fun date_translation/2},{locale, <<"ru">>}],
+        <<"Сен."/utf8>>},
+        {"|date P translation",
+        <<"{{ var1|date:\"P\" }}">>,
+        [{var1, {{1986,9,24},{12,0,0}}}],[{translation_fun, fun date_translation/2},{locale, <<"ru">>}],
+        <<"полдень"/utf8>>},
+
        {"|default:\"foo\" 1",
         <<"{{ var1|default:\"foo\" }}">>, [], <<"foo">>},
        {"|default:\"foo\" 2",
@@ -1799,13 +1846,65 @@ def_to_test(Group, {Name, DTL, Vars, RenderOpts, CompilerOpts, Output, Warnings}
       }.
 
 
+date_translation(Val, LC) when is_list(Val) ->
+    io:format("Translating ~p~n", [Val]),
+    date_translation(list_to_binary(Val),LC);
+% date a
+date_translation(<<"p.m.">>, <<"ru">>) ->
+    <<"п.п."/utf8>>;
+% date A
+date_translation(<<"PM">>, <<"ru">>) ->
+    <<"ПП"/utf8>>;
+% date b
+date_translation(<<"jul">>, <<"ru">>) ->
+    <<"июл"/utf8>>;
+% date D
+date_translation(<<"Thu">>, <<"ru">>) ->
+    <<"Чтв"/utf8>>;
+% date E
+date_translation(<<"July">>, {<<"ru">>, <<"alt. month">>}) ->
+    <<"Июля"/utf8>>;
+% date F
+date_translation(<<"July">>, <<"ru">>) ->
+    <<"Июль"/utf8>>;
+% date l
+date_translation(<<"Thursday">>, <<"ru">>) ->
+    <<"Четверг"/utf8>>;
+% date M
+date_translation(<<"Sep">>, <<"ru">>) ->
+    <<"Сен"/utf8>>;
+% date N
+date_translation(<<"Sept.">>, {<<"ru">>, <<"abbrev. month">>}) ->
+    <<"Сен."/utf8>>;
+% date P
+date_translation(<<"noon">>, <<"ru">>) ->
+    <<"полдень"/utf8>>;
+date_translation(Text, <<"ru">>) ->
+    proplists:get_value(Text,
+                        lists:zip(
+                              lists:map(fun list_to_binary/1, en_months()),
+                              ru_months()),
+                        Text);
+date_translation(Text, _) ->
+    Text.
+
+ru_months() -> [ <<"Январь"/utf8>>, <<"Февраль"/utf8>>, <<"Март"/utf8>>, <<"Апрель"/utf8>>,
+             <<"Май"/utf8>>, <<"Июнь"/utf8>>, <<"Июль"/utf8>>, <<"Август"/utf8>>, <<"Сентябрь"/utf8>>,
+             <<"Октябрь"/utf8>>, <<"Ноябрь"/utf8>>, <<"Декабрь"/utf8>>].
+en_months() -> ["January", "February", "March", "April",
+             "May", "June", "July", "August", "September",
+             "October", "November", "December"].
+
+
+
 generate_test_date() ->
+    generate_test_date(false).
+generate_test_date(Translation) ->
     {{Y,M,D}, _} = erlang:localtime(),
-    MonthName = [
-                 "January", "February", "March", "April",
-                 "May", "June", "July", "August", "September",
-                 "October", "November", "December"
-                ],
+    MonthName = case Translation of
+                    russian -> ru_months();
+                    _ -> en_months()
+                end,
     OrdinalSuffix = [
                      "st","nd","rd","th","th","th","th","th","th","th", % 1-10
                      "th","th","th","th","th","th","th","th","th","th", % 10-20