/modules/mod_translation/mod_translation.erl

https://code.google.com/p/zotonic/ · Erlang · 312 lines · 236 code · 39 blank · 37 comment · 6 complexity · cf8fd7b2fd94e69c1606f13d0e839afa MD5 · raw file

  1. %% @author Marc Worrell <marc@worrell.nl>
  2. %% @copyright 2010 Marc Worrell
  3. %% Date: 2010-05-19
  4. %% @doc Translation support for i18n. Generates .po files by scanning templates.
  5. %% Copyright 2010 Marc Worrell
  6. %%
  7. %% Licensed under the Apache License, Version 2.0 (the "License");
  8. %% you may not use this file except in compliance with the License.
  9. %% You may obtain a copy of the License at
  10. %%
  11. %% http://www.apache.org/licenses/LICENSE-2.0
  12. %%
  13. %% Unless required by applicable law or agreed to in writing, software
  14. %% distributed under the License is distributed on an "AS IS" BASIS,
  15. %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  16. %% See the License for the specific language governing permissions and
  17. %% limitations under the License.
  18. -module(mod_translation).
  19. -author("Marc Worrell <marc@worrell.nl>").
  20. -mod_title("Translation").
  21. -mod_description("Generate .po files containing translatable texts by scanning templates.").
  22. -mod_prio(500).
  23. -export([
  24. observe_session_init_fold/3,
  25. observe_session_context/3,
  26. observe_auth_logon/3,
  27. init/1,
  28. event/2,
  29. generate/1,
  30. do_choose/2
  31. ]).
  32. -include("zotonic.hrl").
  33. %% @doc Make sure that we have the i18n.language_list setting when the site starts up.
  34. init(Context) ->
  35. case Context#context.host of
  36. zotonic_status ->
  37. ok;
  38. _Other ->
  39. case m_config:get(i18n, language_list, Context) of
  40. undefined ->
  41. m_config:set_prop(i18n, language_list, list, [
  42. {en, [ {language, <<"English">>}, {is_enabled, true}]},
  43. {es, [ {language, <<"Espańol">>}, {is_enabled, true}]},
  44. {fr, [ {language, <<"Français">>}, {is_enabled, true}]},
  45. {nl, [ {language, <<"Nederlands">>}, {is_enabled, true}]},
  46. {tr, [ {language, <<"Türkçe">>}, {is_enabled, true}]}
  47. ], Context);
  48. _Exists ->
  49. ok
  50. end
  51. end.
  52. %% @doc Check if the user has a prefered language (in the user's persistent data). If not
  53. %% then check the accept-language header (if any) against the available languages.
  54. observe_session_init_fold(session_init_fold, Context, _Context) ->
  55. case z_context:get_persistent(language, Context) of
  56. undefined ->
  57. case z_context:get_req_header("accept-language", Context) of
  58. undefined ->
  59. Context;
  60. AcceptLanguage ->
  61. LanguagesAvailable = [ atom_to_list(Lang)
  62. || {Lang, Props} <- get_language_config(Context),
  63. proplists:get_value(is_enabled, Props) =:= true
  64. ],
  65. case catch do_choose(LanguagesAvailable, AcceptLanguage) of
  66. Lang when is_list(Lang) ->
  67. do_set_language(list_to_atom(Lang), Context);
  68. none ->
  69. Context;
  70. _Error ->
  71. % @todo log the error, might be a problem in the accept-language header
  72. Context
  73. end
  74. end;
  75. Language ->
  76. do_set_language(Language, Context)
  77. end.
  78. observe_session_context(session_context, Context, _Context) ->
  79. case z_context:get_session(language, Context) of
  80. undefined -> Context;
  81. Language -> Context#context{language=Language}
  82. end.
  83. observe_auth_logon(auth_logon, Context, _Context) ->
  84. UserId = z_acl:user(Context),
  85. case m_rsc:p_no_acl(UserId, pref_language, Context) of
  86. undefined ->
  87. % Ensure that the user has a default language
  88. catch m_rsc:update(UserId, [{pref_language, z_context:language(Context)}], Context),
  89. Context;
  90. Code ->
  91. % Switch the session to the default language of the user
  92. List = get_language_config(Context),
  93. Context1 = set_language(z_convert:to_list(Code), List, Context),
  94. z_context:set_persistent(language, z_context:language(Context1), Context1),
  95. Context1
  96. end.
  97. %% @doc Set the current session (and user) language, reload the user agent's page.
  98. event({postback, {set_language, Args}, _TriggerId, _TargetId}, Context) ->
  99. Code = case proplists:get_value(code, Args) of
  100. undefined -> z_context:get_q("triggervalue", Context);
  101. ArgCode -> ArgCode
  102. end,
  103. List = get_language_config(Context),
  104. Context1 = set_language(z_convert:to_list(Code), List, Context),
  105. z_context:set_persistent(language, z_context:language(Context1), Context1),
  106. case z_acl:user(Context1) of
  107. undefined ->
  108. nop;
  109. UserId ->
  110. case m_rsc:p_no_acl(UserId, pref_language, Context1) of
  111. Code -> nop;
  112. _ -> catch m_rsc:update(UserId, [{pref_language, Code}], Context1)
  113. end
  114. end,
  115. z_render:wire({reload, []}, Context1);
  116. %% @doc Set the default language.
  117. event({postback, {language_default, Args}, _TriggerId, _TargetId}, Context) ->
  118. case z_acl:is_allowed(use, ?MODULE, Context) of
  119. true ->
  120. {code, Code} = proplists:lookup(code, Args),
  121. m_config:set_value(i18n, language, z_convert:to_binary(Code), Context),
  122. Context;
  123. false ->
  124. z_render:growl_error("Sorry, you don't have permission to set the default language.", Context)
  125. end;
  126. %% @doc Start rescanning all templates for translation tags.
  127. event({postback, translation_generate, _TriggerId, _TargetId}, Context) ->
  128. case z_acl:is_allowed(use, ?MODULE, Context) of
  129. true ->
  130. spawn(fun() -> generate(Context) end),
  131. z_render:growl("Started building the .po templates. This may take a while.", Context);
  132. false ->
  133. z_render:growl_error("Sorry, you don't have permission to scan for translations.", Context)
  134. end;
  135. event({postback, translation_reload, _TriggerId, _TargetId}, Context) ->
  136. case z_acl:is_allowed(use, ?MODULE, Context) of
  137. true ->
  138. spawn(fun() -> z_trans_server:load_translations(Context) end),
  139. z_render:growl("Reloading all .po template in the background.", Context);
  140. false ->
  141. z_render:growl_error("Sorry, you don't have permission to reload translations.", Context)
  142. end;
  143. event({submit, {language_edit, Args}, _TriggerId, _TargetId}, Context) ->
  144. case z_acl:is_allowed(use, ?MODULE, Context) of
  145. true ->
  146. OldCode = proplists:get_value(code, Args, '$empty'),
  147. language_add(OldCode, z_context:get_q("code", Context), z_context:get_q("language", Context),
  148. z_context:get_q("is_enabled", Context), Context),
  149. Context1 = z_render:dialog_close(Context),
  150. z_render:wire({reload, []}, Context1);
  151. false ->
  152. z_render:growl_error("Sorry, you don't have permission to change the language list.", Context)
  153. end;
  154. event({postback, {language_delete, Args}, _TriggerId, _TargetId}, Context) ->
  155. case z_acl:is_allowed(use, ?MODULE, Context) of
  156. true ->
  157. {code, Code} = proplists:lookup(code, Args),
  158. language_delete(Code, Context),
  159. Context1 = z_render:dialog_close(Context),
  160. z_render:wire({reload, []}, Context1);
  161. false ->
  162. z_render:growl_error("Sorry, you don't have permission to change the language list.", Context)
  163. end;
  164. event({postback, {language_enable, Args}, _TriggerId, _TargetId}, Context) ->
  165. case z_acl:is_allowed(use, ?MODULE, Context) of
  166. true ->
  167. {code, Code} = proplists:lookup(code, Args),
  168. language_enable(Code, z_convert:to_bool(z_context:get_q("triggervalue", Context)), Context),
  169. Context;
  170. false ->
  171. z_render:growl_error("Sorry, you don't have permission to change the language list.", Context)
  172. end.
  173. %% @doc Set the language of the current user/session
  174. set_language(Code, [{CodeAtom, _Language}|Other], Context) ->
  175. case z_convert:to_list(CodeAtom) of
  176. Code -> do_set_language(CodeAtom, Context);
  177. _Other -> set_language(Code, Other, Context)
  178. end.
  179. do_set_language(Code, Context) when is_atom(Code) ->
  180. Context1 = z_context:set_language(Code, Context),
  181. z_context:set_session(language, Code, Context1),
  182. z_notifier:notify({language, Code}, Context1),
  183. Context1.
  184. %% @doc Add a language to the i18n configuration
  185. language_add(OldIsoCode, NewIsoCode, Language, IsEnabled, Context) ->
  186. IsoCodeNewAtom = z_convert:to_atom(z_string:to_name(z_string:trim(NewIsoCode))),
  187. Languages = get_language_config(Context),
  188. Languages1 = proplists:delete(OldIsoCode, Languages),
  189. Languages2 = lists:usort([{IsoCodeNewAtom,
  190. [{language, z_convert:to_binary(z_string:trim(z_html:escape(Language)))},
  191. {is_enabled, z_convert:to_bool(IsEnabled)}
  192. ]} | Languages1]),
  193. set_language_config(Languages2, Context).
  194. %% @doc Remove a language from the i18n configuration
  195. language_delete(IsoCode, Context) ->
  196. Languages = get_language_config(Context),
  197. Languages1 = proplists:delete(IsoCode, Languages),
  198. set_language_config(Languages1, Context).
  199. %% @doc Set/reset the is_enabled flag of a language.
  200. language_enable(Code, IsEnabled, Context) ->
  201. Languages = get_language_config(Context),
  202. Lang = proplists:get_value(Code, Languages),
  203. Lang1 = [{is_enabled, IsEnabled} | proplists:delete(is_enabled, Lang)],
  204. Languages1 = lists:usort([{Code, Lang1} | proplists:delete(Code, Languages)]),
  205. set_language_config(Languages1, Context).
  206. %% @doc Get the list of languages
  207. get_language_config(Context) ->
  208. case m_config:get(i18n, language_list, Context) of
  209. undefined -> [];
  210. LanguageConfig -> proplists:get_value(list, LanguageConfig, [])
  211. end.
  212. set_language_config(NewConfig, Context) ->
  213. m_config:set_prop(i18n, language_list, list, NewConfig, Context).
  214. % @doc Generate all .po templates for the given site
  215. generate(#context{} = Context) ->
  216. translation_po:generate(translation_scan:scan(Context));
  217. generate(Host) when is_atom(Host) ->
  218. translation_po:generate(translation_scan:scan(z_context:new(Host))).
  219. %% do_choose/2 is adapted from webmachine_util:do_choose/3
  220. %% Original code copyright 2007-2008 Basho Technologies
  221. do_choose(Choices, Header) ->
  222. Accepted = build_conneg_list(string:tokens(Header, ",")),
  223. StarPrio = [P || {P,C} <- Accepted, C =:= "*"],
  224. AnyOkay = case StarPrio of
  225. [] -> no;
  226. [0.0] -> no;
  227. _ -> yes
  228. end,
  229. do_choose(AnyOkay, Choices, Accepted).
  230. do_choose(_AnyOkay, [], []) ->
  231. none;
  232. do_choose(_AnyOkay, [], _Accepted) ->
  233. none;
  234. do_choose(yes, Choices, []) ->
  235. hd(Choices);
  236. do_choose(no, _Choices, []) ->
  237. none;
  238. do_choose(AnyOkay, Choices, [AccPair|AccRest]) ->
  239. {Prio, Acc} = AccPair,
  240. case Prio of
  241. 0.0 ->
  242. do_choose(AnyOkay, lists:delete(Acc, Choices), AccRest);
  243. _ ->
  244. LAcc = string:to_lower(Acc),
  245. LChoices = [string:to_lower(X) || X <- Choices],
  246. % doing this a little more work than needed in
  247. % order to be easily insensitive but preserving
  248. case lists:member(LAcc, LChoices) of
  249. true ->
  250. hd([X || X <- Choices,
  251. string:to_lower(X) =:= LAcc]);
  252. false -> do_choose(AnyOkay, Choices, AccRest)
  253. end
  254. end.
  255. build_conneg_list(AccList) ->
  256. build_conneg_list(AccList, []).
  257. build_conneg_list([], Result) -> lists:reverse(lists:sort(Result));
  258. build_conneg_list([Acc|AccRest], Result) ->
  259. XPair = list_to_tuple([string:strip(X) || X <- string:tokens(Acc, ";")]),
  260. Pair = case XPair of
  261. {Choice, "q=" ++ PrioStr} ->
  262. % Simplify "en-us" to "en"
  263. [Choice1|_] = string:tokens(Choice, "-"),
  264. case PrioStr of
  265. "0" -> {0.0, Choice1};
  266. "1" -> {1.0, Choice1};
  267. [$.|_] -> {list_to_float([$0|PrioStr]), Choice1};
  268. _ -> {list_to_float(PrioStr), Choice1}
  269. end;
  270. {Choice} ->
  271. {1.0, hd(string:tokens(Choice, "-"))}
  272. end,
  273. build_conneg_list(AccRest,[Pair|Result]).