comparison src/tools/common/template.py @ 2249:e572482f6cbd

core (tools/common/template): i18n support - babel has been added as a new dependencies, and should replace gettext in core in the future - added i18n support in template rendered - current locale is available as babel Locale in templates through "locale" variable - locales can be changed before rendering using setLocale - for now, all locales translations are loaded on init, and stay in cache. A more complex cache system may be needed in the future (e.g. keeping only most used and load others from files when needed).
author Goffi <goffi@goffi.org>
date Sun, 21 May 2017 15:59:47 +0200
parents e09048cb7595
children 322694543225
comparison
equal deleted inserted replaced
2248:a81261cee29b 2249:e572482f6cbd
26 log = getLogger(__name__) 26 log = getLogger(__name__)
27 import os.path 27 import os.path
28 from collections import OrderedDict 28 from collections import OrderedDict
29 from xml.sax.saxutils import quoteattr 29 from xml.sax.saxutils import quoteattr
30 import time 30 import time
31 from babel import support
32 from babel import Locale
33 from babel.core import UnknownLocaleError
31 try: 34 try:
32 import sat_templates 35 import sat_templates
33 except ImportError: 36 except ImportError:
34 raise exceptions.MissingModule(u'sat_templates module is not available, please install it or check your path to use template engine') 37 raise exceptions.MissingModule(u'sat_templates module is not available, please install it or check your path to use template engine')
35 else: 38 else:
39 import jinja2 42 import jinja2
40 except: 43 except:
41 raise exceptions.MissingModule(u'Missing module jinja2, please install it from http://jinja.pocoo.org or with pip install jinja2') 44 raise exceptions.MissingModule(u'Missing module jinja2, please install it from http://jinja.pocoo.org or with pip install jinja2')
42 45
43 HTML_EXT = ('html', 'xhtml') 46 HTML_EXT = ('html', 'xhtml')
47 DEFAULT_LOCALE = u'en'
44 # TODO: handle external path (an additional search path for templates should be settable by user 48 # TODO: handle external path (an additional search path for templates should be settable by user
45 # TODO: handle absolute URL (should be used for trusted use cases) only (e.g. jp) for security reason 49 # TODO: handle absolute URL (should be used for trusted use cases) only (e.g. jp) for security reason
46 50
47 51
48 class TemplateLoader(jinja2.FileSystemLoader): 52 class TemplateLoader(jinja2.FileSystemLoader):
170 self.env = jinja2.Environment( 174 self.env = jinja2.Environment(
171 loader=TemplateLoader(), 175 loader=TemplateLoader(),
172 autoescape=jinja2.select_autoescape(['html', 'xhtml', 'xml']), 176 autoescape=jinja2.select_autoescape(['html', 'xhtml', 'xml']),
173 trim_blocks=True, 177 trim_blocks=True,
174 lstrip_blocks=True, 178 lstrip_blocks=True,
179 extensions=['jinja2.ext.i18n'],
175 ) 180 )
181 self._locale_str = DEFAULT_LOCALE
182 self._locale = Locale.parse(self._locale_str)
183 self.installTranslations()
176 # we want to have access to SàT constants in templates 184 # we want to have access to SàT constants in templates
177 self.env.globals[u'C'] = C 185 self.env.globals[u'C'] = C
178 # custom filters 186 # custom filters
179 self.env.filters['next_gidx'] = self._next_gidx 187 self.env.filters['next_gidx'] = self._next_gidx
180 self.env.filters['cur_gidx'] = self._cur_gidx 188 self.env.filters['cur_gidx'] = self._cur_gidx
181 self.env.filters['blog_date'] = self._blog_date 189 self.env.filters['blog_date'] = self._blog_date
190
191 def installTranslations(self):
192 i18n_dir = os.path.join(self.base_dir, 'i18n')
193 self.translations = {}
194 for lang_dir in os.listdir(i18n_dir):
195 lang_path = os.path.join(i18n_dir, lang_dir)
196 if not os.path.isdir(lang_path):
197 continue
198 po_path = os.path.join(lang_path, 'LC_MESSAGES/sat.mo')
199 try:
200 with open(po_path, 'rb') as f:
201 self.translations[Locale.parse(lang_dir)] = support.Translations(f, 'sat')
202 except EnvironmentError:
203 log.error(_(u"Can't find template translation at {path}").format(path = po_path))
204 except UnknownLocaleError as e:
205 log.error(_(u"Invalid locale name: {msg}").format(msg=e))
206 else:
207 log.info(_(u'loaded {lang} templates translations').format(lang=lang_dir))
208 self.env.install_null_translations(True)
209
210 def setLocale(self, locale_str):
211 if locale_str == self._locale_str:
212 return
213 locale = Locale.parse(locale_str)
214 locale_str = unicode(locale)
215 if locale_str != DEFAULT_LOCALE:
216 try:
217 translations = self.translations[locale]
218 except KeyError:
219 log.warning(_(u"Can't find locale {locale}".format(locale=locale)))
220 locale_str = DEFAULT_LOCALE
221 locale = Locale.parse(self._locale_str)
222 else:
223 self.env.install_gettext_translations(translations, True)
224 log.debug(_(u'Switched to {lang}').format(lang=locale.english_name))
225
226 if locale_str == DEFAULT_LOCALE:
227 self.env.install_null_translations(True)
228
229 self._locale = locale
230 self._locale_str = locale_str
182 231
183 def getThemeAndRoot(self, template): 232 def getThemeAndRoot(self, template):
184 """retrieve theme and root dir of a given tempalte 233 """retrieve theme and root dir of a given tempalte
185 234
186 @param template(unicode): template to parse 235 @param template(unicode): template to parse
263 312
264 def _blog_date(self, timestamp): 313 def _blog_date(self, timestamp):
265 # FIXME: Q&D, need to be done properly 314 # FIXME: Q&D, need to be done properly
266 return unicode(int(time.time() - int(timestamp))/(3600*24)) + u" days ago" 315 return unicode(int(time.time() - int(timestamp))/(3600*24)) + u" days ago"
267 316
268 def render(self, template, theme=None, root_path=u'', css_files=None, css_inline=False, **kwargs): 317 def render(self, template, theme=None, locale=DEFAULT_LOCALE, root_path=u'', css_files=None, css_inline=False, **kwargs):
269 """render a template 318 """render a template
270 319
271 @param template(unicode): template to render (e.g. blog/articles.html) 320 @param template(unicode): template to render (e.g. blog/articles.html)
272 @param theme(unicode): template theme 321 @param theme(unicode): template theme
273 @param root_path(unicode): prefix of the path/URL to use for template root 322 @param root_path(unicode): prefix of the path/URL to use for template root
307 css_contents.append(f.read()) 356 css_contents.append(f.read())
308 if css_contents: 357 if css_contents:
309 kwargs['css_content'] = '\n'.join(css_contents) 358 kwargs['css_content'] = '\n'.join(css_contents)
310 359
311 scripts_handler = ScriptsHandler(self, template_path, template_root_dir) 360 scripts_handler = ScriptsHandler(self, template_path, template_root_dir)
361 self.setLocale(locale)
312 # XXX: theme used in template arguments is the requested theme, which may differ from actual theme 362 # XXX: theme used in template arguments is the requested theme, which may differ from actual theme
313 # if the template doesn't exist in the requested theme. 363 # if the template doesn't exist in the requested theme.
314 return template_source.render(theme=theme, root_path=root_path, css_files=css_files, gidx=Indexer(), scripts_handler=scripts_handler, **kwargs) 364 return template_source.render(theme=theme, root_path=root_path, css_files=css_files, locale=locale, gidx=Indexer(), scripts_handler=scripts_handler, **kwargs)