Permalink
Browse files

Support for i18n at run-time.

Pass a translation dictionary as the second argument to "render"
and it will be used to translate strings inside {% trans %} blocks.
Values in this dictionary will override compile-time translations
achieved with the 'locale' compile-time option.

Also generates a new function translatable_strings/0 which returns
a list of {% trans %} strings than can be converted with a dictionary.
  • Loading branch information...
1 parent 4fb4176 commit 550bbe4824990b376d49ab77edff68b45e257b20 @evanmiller evanmiller committed May 26, 2010
Showing with 81 additions and 25 deletions.
  1. +11 −0 README
  2. +1 −1 src/erlydtl/erlydtl.app
  3. +43 −17 src/erlydtl/erlydtl_compiler.erl
  4. +8 −0 src/erlydtl/erlydtl_runtime.erl
  5. +18 −7 src/tests/erlydtl_unittests.erl
View
11 README
@@ -67,6 +67,17 @@ Usage (of a compiled template)
IOList is the rendered template.
+ my_compiled_template:render(Variables, Dictionary) ->
+ {ok, IOList} | {error, Err}
+
+ Same as render/1, but Dictionary is a dict that will be used to
+ translate strings appearing inside {% trans %} tags.
+
+ my_compiled_template:translatable_strings() -> [String]
+
+ List of strings appearing in {% trans %} tags that can be overridden
+ with a dictionary passed to render/2.
+
my_compiled_template:source() -> {FileName, CheckSum}
Name and checksum of the original template file.
View
@@ -1,7 +1,7 @@
%% -*- mode: erlang -*-
{application, erlydtl,
[{description, "ErlyDTL implements most but not all of the Django Template Language"},
- {vsn, "0.6.0"},
+ {vsn, "0.6.1"},
{modules, [
erlydtl,
erlydtl_compiler,
@@ -52,10 +52,11 @@
module = [],
compiler_options = [verbose, report_errors],
force_recompile = false,
- locale}).
+ locale = none}).
-record(ast_info, {
dependencies = [],
+ translatable_strings = [],
var_names = [],
pre_render_asts = []}).
@@ -230,14 +231,22 @@ forms(File, Module, BodyAst, BodyInfo, CheckSum) ->
Render0FunctionAst = erl_syntax:function(erl_syntax:atom(render),
[erl_syntax:clause([], none, [erl_syntax:application(none,
erl_syntax:atom(render), [erl_syntax:list([])])])]),
- Function2 = erl_syntax:application(none, erl_syntax:atom(render2),
- [erl_syntax:variable("Variables")]),
+ Render1FunctionAst = erl_syntax:function(erl_syntax:atom(render),
+ [erl_syntax:clause([erl_syntax:variable("Variables")], none,
+ [erl_syntax:application(none,
+ erl_syntax:atom(render),
+ [erl_syntax:variable("Variables"),
+ erl_syntax:application(
+ erl_syntax:atom(dict), erl_syntax:atom(new), [])])])]),
+ Function2 = erl_syntax:application(none, erl_syntax:atom(render_internal),
+ [erl_syntax:variable("Variables"), erl_syntax:variable("Dictionary")]),
ClauseOk = erl_syntax:clause([erl_syntax:variable("Val")], none,
[erl_syntax:tuple([erl_syntax:atom(ok), erl_syntax:variable("Val")])]),
ClauseCatch = erl_syntax:clause([erl_syntax:variable("Err")], none,
[erl_syntax:tuple([erl_syntax:atom(error), erl_syntax:variable("Err")])]),
- Render1FunctionAst = erl_syntax:function(erl_syntax:atom(render),
- [erl_syntax:clause([erl_syntax:variable("Variables")], none,
+ Render2FunctionAst = erl_syntax:function(erl_syntax:atom(render),
+ [erl_syntax:clause([erl_syntax:variable("Variables"),
+ erl_syntax:variable("Dictionary")], none,
[erl_syntax:try_expr([Function2], [ClauseOk], [ClauseCatch])])]),
SourceFunctionTuple = erl_syntax:tuple(
@@ -253,27 +262,33 @@ forms(File, Module, BodyAst, BodyInfo, CheckSum) ->
erl_syntax:tuple([erl_syntax:string(XFile), erl_syntax:string(XCheckSum)])
end, BodyInfo#ast_info.dependencies))])]),
- BodyAstTmp = erl_syntax:application(
+ TranslatableStringsAst = erl_syntax:function(
+ erl_syntax:atom(translatable_strings), [erl_syntax:clause([], none,
+ [erl_syntax:list(lists:map(fun(String) -> erl_syntax:string(String) end,
+ BodyInfo#ast_info.translatable_strings))])]),
+
+ BodyAstTmp = erl_syntax:application(
erl_syntax:atom(erlydtl_runtime),
erl_syntax:atom(stringify_final),
- [BodyAst]
- ),
+ [BodyAst]),
RenderInternalFunctionAst = erl_syntax:function(
- erl_syntax:atom(render2),
- [erl_syntax:clause([erl_syntax:variable("Variables")], none,
+ erl_syntax:atom(render_internal),
+ [erl_syntax:clause([erl_syntax:variable("Variables"), erl_syntax:variable("Dictionary")], none,
[BodyAstTmp])]),
ModuleAst = erl_syntax:attribute(erl_syntax:atom(module), [erl_syntax:atom(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(dependencies), erl_syntax:integer(0)),
+ erl_syntax:arity_qualifier(erl_syntax:atom(translatable_strings), erl_syntax:integer(0))])]),
- [erl_syntax:revert(X) || X <- [ModuleAst, ExportAst, Render0FunctionAst,
- Render1FunctionAst, SourceFunctionAst, DependenciesFunctionAst, RenderInternalFunctionAst
+ [erl_syntax:revert(X) || X <- [ModuleAst, ExportAst, Render0FunctionAst, Render1FunctionAst, Render2FunctionAst,
+ SourceFunctionAst, DependenciesFunctionAst, TranslatableStringsAst, RenderInternalFunctionAst
| BodyInfo#ast_info.pre_render_asts]].
@@ -446,6 +461,10 @@ merge_info(Info1, Info2) ->
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)),
pre_render_asts =
lists:merge(
Info1#ast_info.pre_render_asts,
@@ -466,10 +485,17 @@ empty_ast(TreeWalker) ->
translated_ast(String,Context, TreeWalker) ->
- NewStr = string:sub_string(String, 2, string:len(String) -1),
- Locale = Context#dtl_context.locale,
- LocalizedString = erlydtl_i18n:translate(NewStr,Locale),
- {{erl_syntax:string(LocalizedString), #ast_info{}}, TreeWalker}.
+ NewStr = unescape_string_literal(String),
+ DefaultString = case Context#dtl_context.locale of
+ none -> NewStr;
+ Locale -> erlydtl_i18n:translate(NewStr,Locale)
+ end,
+ StringLookupAst = erl_syntax:application(
+ erl_syntax:atom(erlydtl_runtime),
+ erl_syntax:atom(translate),
+ [erl_syntax:string(NewStr), erl_syntax:variable("Dictionary"),
+ erl_syntax:string(DefaultString)]),
+ {{StringLookupAst, #ast_info{translatable_strings = [NewStr]}}, TreeWalker}.
string_ast(String, TreeWalker) ->
{{erl_syntax:string(String), #ast_info{}}, TreeWalker}. %% less verbose AST, better for development and debugging
@@ -50,6 +50,14 @@ fetch_value(Key, Data) ->
Val
end.
+translate(String, Dictionary, Default) ->
+ case dict:find(String, Dictionary) of
+ {ok, Val} ->
+ Val;
+ _ ->
+ Default
+ end.
+
are_equal(Arg1, Arg2) when Arg1 =:= Arg2 ->
true;
are_equal(Arg1, Arg2) when is_binary(Arg1) ->
@@ -90,8 +90,11 @@ tests() ->
<<"Hello {% trans \"Hi\" %}">>, [], <<"Hello Hi">>
},
{"trans functional reverse locale",
- <<"Hello {% trans \"Hi\" %}">>, [], [{locale, "reverse"}], <<"Hello iH">>
- }
+ <<"Hello {% trans \"Hi\" %}">>, [], dict:new(), [{locale, "reverse"}], <<"Hello iH">>
+ },
+ {"trans run-time lookup",
+ <<"Hello {% trans \"Hi\" %}">>, [], dict:from_list([{"Hi", "Konichiwa"}]), [],
+ <<"Hello Konichiwa">>}
]},
{"if", [
{"If/else",
@@ -550,18 +553,26 @@ run_tests() ->
Failures = lists:foldl(
fun({Group, Assertions}, GroupAcc) ->
io:format(" Test group ~p...~n", [Group]),
- lists:foldl(fun({Name, DTL, Vars, Output}, Acc) -> process_unit_test(erlydtl:compile(DTL, erlydtl_running_test, []),Vars, Output, Acc, Group, Name);
- ({Name, DTL, Vars, CompilerOpts, Output}, Acc) -> process_unit_test(erlydtl:compile(DTL, erlydtl_running_test, CompilerOpts),Vars, Output, Acc, Group, Name)
+ lists:foldl(fun
+ ({Name, DTL, Vars, Output}, Acc) ->
+ process_unit_test(erlydtl:compile(DTL, erlydtl_running_test, []),
+ Vars, dict:new(), Output, Acc, Group, Name);
+ ({Name, DTL, Vars, Dictionary, Output}, Acc) ->
+ process_unit_test(erlydtl:compile(DTL, erlydtl_running_test, []),
+ Vars, Dictionary, Output, Acc, Group, Name);
+ ({Name, DTL, Vars, Dictionary, CompilerOpts, Output}, Acc) ->
+ process_unit_test(erlydtl:compile(DTL, erlydtl_running_test, CompilerOpts),
+ Vars, Dictionary, Output, Acc, Group, Name)
end, GroupAcc, Assertions)
end, [], tests()),
io:format("Unit test failures: ~p~n", [lists:reverse(Failures)]).
-process_unit_test(CompiledTemplate, Vars, Output,Acc, Group, Name) ->
+process_unit_test(CompiledTemplate, Vars, Dictionary, Output,Acc, Group, Name) ->
case CompiledTemplate of
{ok, _} ->
- {ok, IOList} = erlydtl_running_test:render(Vars),
- {ok, IOListBin} = erlydtl_running_test:render(vars_to_binary(Vars)),
+ {ok, IOList} = erlydtl_running_test:render(Vars, Dictionary),
+ {ok, IOListBin} = erlydtl_running_test:render(vars_to_binary(Vars), Dictionary),
case {iolist_to_binary(IOList), iolist_to_binary(IOListBin)} of
{Output, Output} ->
Acc;

0 comments on commit 550bbe4

Please sign in to comment.