appengine/standard/i18n/i18n_utils.py (107 lines of code) (raw):

#!/usr/bin/env python # # Copyright 2013 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """A small module for i18n of webapp2 and jinja2 based apps. The idea of this example, especially for how to translate strings in Javascript is originally from an implementation of Django i18n. """ from __future__ import print_function import gettext import json import os import jinja2 import webapp2 from webob import Request try: basestring except NameError: basestring = str def _get_plural_forms(js_translations): """Extracts the parameters for what constitutes a plural. Args: js_translations: GNUTranslations object to be converted. Returns: A tuple of: A formula for what constitutes a plural How many plural forms there are """ plural = None n_plural = 2 if "" in js_translations._catalog: for l in js_translations._catalog[""].split("\n"): if l.startswith("Plural-Forms:"): plural = l.split(":", 1)[1].strip() print("plural is {}".format(plural)) if plural is not None: for raw_element in plural.split(";"): element = raw_element.strip() if element.startswith("nplurals="): n_plural = int(element.split("=", 1)[1]) elif element.startswith("plural="): plural = element.split("=", 1)[1] print("plural is now {}".format(plural)) else: n_plural = 2 plural = "(n == 1) ? 0 : 1" return plural, n_plural def convert_translations_to_dict(js_translations): """Convert a GNUTranslations object into a dict for jsonifying. Args: js_translations: GNUTranslations object to be converted. Returns: A dictionary representing the GNUTranslations object. """ plural, n_plural = _get_plural_forms(js_translations) translations_dict = {"plural": plural, "catalog": {}, "fallback": None} if js_translations._fallback is not None: translations_dict["fallback"] = convert_translations_to_dict( js_translations._fallback ) for key, value in js_translations._catalog.items(): if key == "": continue if isinstance(key, basestring): translations_dict["catalog"][key] = value elif isinstance(key, tuple): if key[0] not in translations_dict["catalog"]: translations_dict["catalog"][key[0]] = [""] * n_plural translations_dict["catalog"][key[0]][int(key[1])] = value return translations_dict class BaseHandler(webapp2.RequestHandler): """A base handler for installing i18n-aware Jinja2 environment.""" @webapp2.cached_property def jinja2_env(self): """Cached property for a Jinja2 environment. Returns: Jinja2 Environment object. """ jinja2_env = jinja2.Environment( loader=jinja2.FileSystemLoader( os.path.join(os.path.dirname(__file__), "templates") ), extensions=["jinja2.ext.i18n"], ) jinja2_env.install_gettext_translations( self.request.environ["i18n_utils.active_translation"] ) jinja2_env.globals["get_i18n_js_tag"] = self.get_i18n_js_tag return jinja2_env def get_i18n_js_tag(self): """Generates a Javascript tag for i18n in Javascript. This instance method is installed to the global namespace of the Jinja2 environment, so you can invoke this method just like `{{ get_i18n_js_tag() }}` from anywhere in your Jinja2 template. Returns: A 'javascript' HTML tag which contains functions and translation messages for i18n. """ template = self.jinja2_env.get_template("javascript_tag.jinja2") return template.render({"javascript_body": self.get_i18n_js()}) def get_i18n_js(self): """Generates a Javascript body for i18n in Javascript. If you want to load these javascript code from a static HTML file, you need to create another handler which just returns the code generated by this function. Returns: Actual javascript code for functions and translation messages for i18n. """ try: js_translations = gettext.translation( "jsmessages", "locales", fallback=False, languages=self.request.environ["i18n_utils.preferred_languages"], codeset="utf-8", ) except IOError: template = self.jinja2_env.get_template("null_i18n_js.jinja2") return template.render() translations_dict = convert_translations_to_dict(js_translations) template = self.jinja2_env.get_template("i18n_js.jinja2") return template.render( {"translations": json.dumps(translations_dict, indent=1)} ) class I18nMiddleware(object): """A WSGI middleware for i18n. This middleware determines users' preferred language, loads the translations files, and install it to the builtin namespace of the Python runtime. """ def __init__(self, app, default_language="en", locale_path=None): """A constructor for this middleware. Args: app: A WSGI app that you want to wrap with this middleware. default_language: fallback language; ex: 'en', 'ja', etc. locale_path: A directory containing the translations file. (defaults to 'locales' directory) """ self.app = app if locale_path is None: locale_path = os.path.join( os.path.abspath(os.path.dirname(__file__)), "locales" ) self.locale_path = locale_path self.default_language = default_language def __call__(self, environ, start_response): """Called by WSGI when a request comes in. Args: environ: A dict holding environment variables. start_response: A WSGI callable (PEP333). Returns: Application response data as an iterable. It just returns the return value of the inner WSGI app. """ req = Request(environ) preferred_languages = list(req.accept_language) if self.default_language not in preferred_languages: preferred_languages.append(self.default_language) translation = gettext.translation( "messages", self.locale_path, fallback=True, languages=preferred_languages, codeset="utf-8", ) translation.install(unicode=True, names=["gettext", "ngettext"]) environ["i18n_utils.active_translation"] = translation environ["i18n_utils.preferred_languages"] = preferred_languages return self.app(environ, start_response)