# encoding=utf-8 import os, argparse, sys, time from threading import local from jinja2 import TemplateNotFound, FileSystemLoader, Environment # this is a workaround for a snow leopard bug that babel does not # work around :) if os.environ.get('LC_CTYPE', '').lower() == 'utf-8': os.environ['LC_CTYPE'] = 'en_US.utf-8' from datetime import datetime from babel import dates, numbers, support, Locale from babel.messages.frontend import parse_mapping from babel.util import pathmatch from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler _language = None _active = local() def activate_locale(locale): _active.value = locale def get_locale(): return _active.value def get_translations(): """Returns the correct gettext translations that should be used for this request. This will never fail and return a dummy translation object if used outside of the request or if a translation cannot be found. """ translations = support.Translations.load(cli.catalog, [get_locale()]) return translations def gettext(string, **variables): """Translates a string with the current locale and passes in the given keyword arguments as mapping to a string formatting string. :: gettext(u'Hello World!') gettext(u'Hello %(name)s!', name='World') """ t = get_translations() if t is None: return string % variables return t.ugettext(string) % variables _ = gettext def ngettext(singular, plural, num, **variables): """Translates a string with the current locale and passes in the given keyword arguments as mapping to a string formatting string. The `num` parameter is used to dispatch between singular and various plural forms of the message. It is available in the format string as ``%(num)d`` or ``%(num)s``. The source language should be English or a similar language which only has one plural form. :: ngettext(u'%(num)d Apple', u'%(num)d Apples', num=len(apples)) """ variables.setdefault('num', num) t = get_translations() if t is None: return (singular if num == 1 else plural) % variables return t.ungettext(singular, plural, num) % variables def pgettext(context, string, **variables): """Like :func:`gettext` but with a context. """ t = get_translations() if t is None: return string % variables return t.upgettext(context, string) % variables def npgettext(context, singular, plural, num, **variables): """Like :func:`ngettext` but with a context. """ variables.setdefault('num', num) t = get_translations() if t is None: return (singular if num == 1 else plural) % variables return t.unpgettext(context, singular, plural, num) % variables def get_locales(): """Returns a list of all the locales translations exist for. The list returned will be filled with actual locale objects and not just strings. A no-op translation is added for the `default_locale`. """ baselocale = Locale.parse(cli.baselocale) result = {baselocale.language: baselocale} if not os.path.isdir(cli.catalog): return result for folder in os.listdir(cli.catalog): locale_dir = os.path.join(cli.catalog, folder, 'LC_MESSAGES') if not os.path.isdir(locale_dir): continue if folder in result and cli.verbose: print "Warning: Translation found for the base locale [{}]".format(folder) if filter(lambda x: x.endswith('.mo'), os.listdir(locale_dir)): locale = Locale.parse(folder) result[locale.language] = locale return result.values() def write_template(name, folder=None, context={}): target = cli.output if folder: target = os.path.join(target, folder) if not os.path.isdir(target): os.makedirs(target) with open(os.path.join(target, name), 'w') as fp: template = env.get_template(name) fp.write(template.render(**context).encode('utf-8')) parser = argparse.ArgumentParser() parser.add_argument('--verbose', '-v', action='count') parser.add_argument('--output', '-o', default='public/') parser.add_argument('--catalog', '-c', default='translations/') parser.add_argument('--templates', '-t', default='app/assets/') parser.add_argument('--babelconf', '-b', default='babel.cfg') parser.add_argument('--baselocale', default='en') parser.add_argument('--watch', '-w', action='store_true') cli = parser.parse_args() def guess_autoescape(template_name): if template_name is None or '.' not in template_name: return False ext = template_name.rsplit('.', 1)[1] return ext in ('html', 'htm', 'xml') env = Environment(autoescape=guess_autoescape, loader=FileSystemLoader(cli.templates), extensions=['jinja2.ext.autoescape', 'jinja2.ext.i18n']) env.install_gettext_callables( lambda x: get_translations().ugettext(x), lambda s, p, n: get_translations().ungettext(s, p, n), newstyle=True ) def build(): try: mappings, _ = parse_mapping(open(cli.babelconf)) except IOError: sys.exit("Could not find Babel conf ({0})".format(cli.babelconf)) search_paths = [search_path for (search_path, _) in mappings] def is_template(name): full_name = os.path.join(cli.templates, name) for path in search_paths: if pathmatch(path, full_name): return True locales = get_locales() context = dict( locales=locales, ) for locale in locales: activate_locale(locale) if cli.verbose: print "Processing locale:", get_locale() for name in env.list_templates(): if not is_template(name): continue folder = get_locale().language if cli.verbose > 1: print "Writing template: ", name context = dict(context, now=datetime.now(), current_locale=get_locale() ) write_template(name, folder, context) def main(): class ChangeHandler(FileSystemEventHandler): def on_any_event(self, event): if event.is_directory: return print "Template update detected" build() build() if cli.watch: event_handler = ChangeHandler() observer = Observer() observer.schedule(event_handler, cli.templates, recursive=True) observer.start() try: while True: time.sleep(1) except KeyboardInterrupt: observer.stop() observer.join()