diff --git a/appengine/i18n/README.md b/appengine/i18n/README.md new file mode 100644 index 000000000000..c1ae720e9604 --- /dev/null +++ b/appengine/i18n/README.md @@ -0,0 +1,126 @@ +# App Engine Internationalization sample in Python + +A simple example app showing how to build an internationalized app +with App Engine. The main purpose of this example is to provide the +basic how-to. + +## What to internationalize + +There are lots of things to internationalize with your web +applications. + +1. Strings in Python code +2. Strings in HTML template +3. Strings in Javascript +4. Common strings + - Country Names, Language Names, etc. +5. Formatting + - Date/Time formatting + - Number formatting + - Currency +6. Timezone conversion + +This example only covers first 3 basic scenarios above. In order to +cover other aspects, I recommend using +[Babel](http://babel.edgewall.org/) and [pytz] +(http://pypi.python.org/pypi/gaepytz). Also, you may want to use +[webapp2_extras.i18n](http://webapp-improved.appspot.com/tutorials/i18n.html) +module. + +## Wait, so why not webapp2_extras.i18n? + +webapp2_extras.i18n doesn't cover how to internationalize strings in +Javascript code. Additionally it depends on babel and pytz, which +means you need to deploy babel and pytz alongside with your code. I'd +like to show a reasonably minimum example for string +internationalization in Python code, jinja2 templates, as well as +Javascript. + +## How to run this example + +First of all, please install babel in your local Python environment. + +### Wait, you just said I don't need babel, are you crazy? + +As I said before, you don't need to deploy babel with this +application, but you need to locally use pybabel script which is +provided by babel distribution in order to extract the strings, manage +and compile the translations file. + +### Extract strings in Python code and Jinja2 templates to translate + +Move into this project directory and invoke the following command: + + $ env PYTHONPATH=/google_appengine_sdk/lib/jinja2 \ + pybabel extract -o locales/messages.pot -F main.mapping . + +This command creates a `locales/messages.pot` file in the `locales` +directory which contains all the string found in your Python code and +Jija2 tempaltes. + +Since the babel configration file `main.mapping` contains a reference +to `jinja2.ext.babel_extract` helper function which is provided by +jinja2 distribution bundled with the App Engine SDK, you need to add a +PYTHONPATH environment variable pointing to the jinja2 directory in +the SDK. + +### Manage and compile translations. + +Create an initial translation source by the following command: + + $ pybabel init -l ja -d locales -i locales/messages.pot + +Open `locales/ja/LC_MESSAGES/messages.po` with any text editor and +translate the strings, then compile the file by the following command: + + $ pybabel compile -d locales + +If any of the strings changes, you can extract the strings again, and +update the translations by the following command: + + $ pybabel update -l ja -d locales -i locales/messages.pot + +Note: If you run `pybabel init` against an existant translations file, +you will lose your translations. + + +### Extract strings in Javascript code and compile translations + + $ pybabel extract -o locales/jsmessages.pot -F js.mapping . + $ pybabel init -l ja -d locales -i locales/jsmessages.pot -D jsmessages + +Open `locales/ja/LC_MESSAGES/jsmessages.po` and translate it. + + $ pybabel compile -d locales -D jsmessages + + +## How it works + +As you can see it in the `appengine_config.py` file, our +`main.application` is wrapped by the `i18n_utils.I18nMiddleware` WSGI +middleware. When a request comes in, this middleware parses the +`HTTP_ACCEPT_LANGUAGE` HTTP header, loads available translation +files(`messages.mo`) from the application directory, and install the +`gettext` and `ngettext` functions to the `__builtin__` namespace in +the Python runtime. + +For strings in Jinja2 templates, there is the `i18n_utils.BaseHandler` +class from which you can extend in order to have a handy property +named `jinja2_env` that lazily initializes Jinja2 environment for you +with the `jinja2.ext.i18n` extention, and similar to the +`I18nMiddleware`, installs `gettext` and `ngettext` functions to the +global namespace of the Jinja2 environment. + +## What about Javascript? + +The `BaseHandler` class also installs the `get_i18n_js_tag()` instance +method to the Jinja2 global namespace. When you use this function in +your Jinja2 template (like in the `index.jinja2` file), you will get a +set of Javascript functions; `gettext`, `ngettext`, and `format` on +the string type. The `format` function can be used with `ngettext`ed +strings for number formatting. See this example: + + window.alert(ngettext( + 'You need to provide at least {0} item.', + 'You need to provide at least {0} items.', + n).format(n); diff --git a/appengine/i18n/__init__.py b/appengine/i18n/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/appengine/i18n/app.yaml b/appengine/i18n/app.yaml new file mode 100644 index 000000000000..c37918a6de70 --- /dev/null +++ b/appengine/i18n/app.yaml @@ -0,0 +1,24 @@ +application: i18n-sample-python +runtime: python27 +api_version: 1 +version: 1 +threadsafe: true + + +handlers: +- url: /favicon\.ico + static_files: favicon.ico + upload: favicon\.ico + +- url: /static + static_dir: static +- url: /.* + script: main.application + +libraries: +- name: webapp2 + version: latest +- name: jinja2 + version: latest +- name: webob + version: 1.2.3 diff --git a/appengine/i18n/appengine_config.py b/appengine/i18n/appengine_config.py new file mode 100644 index 000000000000..1076922e1626 --- /dev/null +++ b/appengine/i18n/appengine_config.py @@ -0,0 +1,35 @@ +#!/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. + +"""App Engine configuration file for applying the I18nMiddleware.""" + + +from i18n_utils import I18nMiddleware + + +def webapp_add_wsgi_middleware(app): + """Applying the I18nMiddleware to our HelloWorld app. + + Args: + app: The WSGI application object that you want to wrap with the + I18nMiddleware. + + Returns: + The wrapped WSGI application. + """ + + app = I18nMiddleware(app) + return app diff --git a/appengine/i18n/i18n_utils.py b/appengine/i18n/i18n_utils.py new file mode 100644 index 000000000000..12eb5e591794 --- /dev/null +++ b/appengine/i18n/i18n_utils.py @@ -0,0 +1,207 @@ +#!/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. +""" + + +import gettext +import json +import os + +import jinja2 + +import webapp2 + +from webob import Request + + +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 %s" % 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 %s" % 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 type(key) in (str, unicode): + translations_dict['catalog'][key] = value + elif type(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) diff --git a/appengine/i18n/js.mapping b/appengine/i18n/js.mapping new file mode 100644 index 000000000000..e5feb2058826 --- /dev/null +++ b/appengine/i18n/js.mapping @@ -0,0 +1 @@ +[javascript: **.js] diff --git a/appengine/i18n/locales/en/LC_MESSAGES/jsmessages.mo b/appengine/i18n/locales/en/LC_MESSAGES/jsmessages.mo new file mode 100644 index 000000000000..be32af390463 Binary files /dev/null and b/appengine/i18n/locales/en/LC_MESSAGES/jsmessages.mo differ diff --git a/appengine/i18n/locales/en/LC_MESSAGES/jsmessages.po b/appengine/i18n/locales/en/LC_MESSAGES/jsmessages.po new file mode 100644 index 000000000000..c2458e006ebf --- /dev/null +++ b/appengine/i18n/locales/en/LC_MESSAGES/jsmessages.po @@ -0,0 +1,18 @@ +msgid "" +msgstr "" +"Project-Id-Version: App Engine i18n sample\n" +"Report-Msgid-Bugs-To: tmatsuo@google.com\n" +"POT-Creation-Date: 2013-01-11 12:50-0800\n" +"PO-Revision-Date: 2013-01-11 12:53-0800\n" +"Last-Translator: Takashi Matsuo \n" +"Language-Team: en \n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.6\n" + +#: static/js/main.js:5 +msgid "Hello World from javascript!" +msgstr "" + diff --git a/appengine/i18n/locales/en/LC_MESSAGES/messages.mo b/appengine/i18n/locales/en/LC_MESSAGES/messages.mo new file mode 100644 index 000000000000..8f82b9c84d58 Binary files /dev/null and b/appengine/i18n/locales/en/LC_MESSAGES/messages.mo differ diff --git a/appengine/i18n/locales/en/LC_MESSAGES/messages.po b/appengine/i18n/locales/en/LC_MESSAGES/messages.po new file mode 100644 index 000000000000..3e546590ac3b --- /dev/null +++ b/appengine/i18n/locales/en/LC_MESSAGES/messages.po @@ -0,0 +1,23 @@ + +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2013-01-11 12:46-0800\n" +"PO-Revision-Date: 2013-03-18 16:36-0700\n" +"Last-Translator: FULL NAME \n" +"Language-Team: en \n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.6\n" + +#: main.py:10 +msgid "Hello World from Python code!" +msgstr "" + +#: templates/index.jinja2:6 +msgid "Hello World from Jinja2 template!" +msgstr "" + diff --git a/appengine/i18n/locales/ja/LC_MESSAGES/jsmessages.mo b/appengine/i18n/locales/ja/LC_MESSAGES/jsmessages.mo new file mode 100644 index 000000000000..3464c12b9c06 Binary files /dev/null and b/appengine/i18n/locales/ja/LC_MESSAGES/jsmessages.mo differ diff --git a/appengine/i18n/locales/ja/LC_MESSAGES/jsmessages.po b/appengine/i18n/locales/ja/LC_MESSAGES/jsmessages.po new file mode 100644 index 000000000000..e24c23e01070 --- /dev/null +++ b/appengine/i18n/locales/ja/LC_MESSAGES/jsmessages.po @@ -0,0 +1,18 @@ +msgid "" +msgstr "" +"Project-Id-Version: App Engine i18n sample\n" +"Report-Msgid-Bugs-To: tmatsuo@google.com\n" +"POT-Creation-Date: 2013-01-11 12:50-0800\n" +"PO-Revision-Date: 2013-01-11 12:53-0800\n" +"Last-Translator: Takashi Matsuo \n" +"Language-Team: ja \n" +"Plural-Forms: nplurals=1; plural=0\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.6\n" + +#: static/js/main.js:5 +msgid "Hello World from javascript!" +msgstr "Javascript から、こんにちは、世界!" + diff --git a/appengine/i18n/locales/ja/LC_MESSAGES/messages.mo b/appengine/i18n/locales/ja/LC_MESSAGES/messages.mo new file mode 100644 index 000000000000..683a0359ca3c Binary files /dev/null and b/appengine/i18n/locales/ja/LC_MESSAGES/messages.mo differ diff --git a/appengine/i18n/locales/ja/LC_MESSAGES/messages.po b/appengine/i18n/locales/ja/LC_MESSAGES/messages.po new file mode 100644 index 000000000000..9b0713c01d0c --- /dev/null +++ b/appengine/i18n/locales/ja/LC_MESSAGES/messages.po @@ -0,0 +1,23 @@ + +msgid "" +msgstr "" +"Project-Id-Version: App Engine i18n sample\n" +"Report-Msgid-Bugs-To: tmatsuo@google.com\n" +"POT-Creation-Date: 2013-01-11 12:46-0800\n" +"PO-Revision-Date: 2013-01-11 12:53-0800\n" +"Last-Translator: Takashi Matsuo \n" +"Language-Team: ja \n" +"Plural-Forms: nplurals=1; plural=0\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.6\n" + +#: main.py:10 +msgid "Hello World from Python code!" +msgstr "Python のコードから、こんにちは、世界!" + +#: templates/index.jinja2:6 +msgid "Hello World from Jinja2 template!" +msgstr "Jinja2 template から、こんにちは、世界!" + diff --git a/appengine/i18n/locales/jsmessages.pot b/appengine/i18n/locales/jsmessages.pot new file mode 100644 index 000000000000..113e91b08f6d --- /dev/null +++ b/appengine/i18n/locales/jsmessages.pot @@ -0,0 +1,18 @@ +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2013-01-11 12:50-0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.6\n" + +#: static/js/main.js:5 +msgid "Hello World from javascript!" +msgstr "" + diff --git a/appengine/i18n/locales/messages.pot b/appengine/i18n/locales/messages.pot new file mode 100644 index 000000000000..10048d75de63 --- /dev/null +++ b/appengine/i18n/locales/messages.pot @@ -0,0 +1,22 @@ +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2013-01-11 12:46-0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.6\n" + +#: main.py:10 +msgid "Hello World from Python code!" +msgstr "" + +#: templates/index.jinja2:6 +msgid "Hello World from Jinja2 template!" +msgstr "" + diff --git a/appengine/i18n/locales/pl/LC_MESSAGES/jsmessages.mo b/appengine/i18n/locales/pl/LC_MESSAGES/jsmessages.mo new file mode 100644 index 000000000000..c4aa6b078c8d Binary files /dev/null and b/appengine/i18n/locales/pl/LC_MESSAGES/jsmessages.mo differ diff --git a/appengine/i18n/locales/pl/LC_MESSAGES/jsmessages.po b/appengine/i18n/locales/pl/LC_MESSAGES/jsmessages.po new file mode 100644 index 000000000000..062ad846634d --- /dev/null +++ b/appengine/i18n/locales/pl/LC_MESSAGES/jsmessages.po @@ -0,0 +1,19 @@ +msgid "" +msgstr "" +"Project-Id-Version: App Engine i18n sample\n" +"Report-Msgid-Bugs-To: tmatsuo@google.com\n" +"POT-Creation-Date: 2013-01-11 12:50-0800\n" +"PO-Revision-Date: 2013-01-11 12:54-0800\n" +"Last-Translator: Takashi Matsuo \n" +"Language-Team: pl \n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && " +"(n%100<10 || n%100>=20) ? 1 : 2)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.6\n" + +#: static/js/main.js:5 +msgid "Hello World from javascript!" +msgstr "" + diff --git a/appengine/i18n/main.mapping b/appengine/i18n/main.mapping new file mode 100644 index 000000000000..190f5a275eb8 --- /dev/null +++ b/appengine/i18n/main.mapping @@ -0,0 +1,8 @@ +# Extraction from Python source files +[python: **.py] +# Extraction from jinja2 templates +[jinja2: templates/**.jinja2] +encoding = utf-8 +[extractors] +jinja2 = jinja2.ext:babel_extract + diff --git a/appengine/i18n/main.py b/appengine/i18n/main.py new file mode 100644 index 000000000000..f36ed404e6ac --- /dev/null +++ b/appengine/i18n/main.py @@ -0,0 +1,45 @@ +#!/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 example showing how to build an i18n app with App Engine.""" + +from i18n_utils import BaseHandler + +import webapp2 + + +class MainHandler(BaseHandler): + """A simple handler with internationalized strings. + + This handler demonstrates how to internationalize strings in + Python, Jinja2 template and Javascript. + """ + + def get(self): + """A get handler for this sample. + + It just shows internationalized strings in Python, Jinja2 + template and Javascript. + """ + + context = dict(message=gettext('Hello World from Python code!')) + template = self.jinja2_env.get_template('index.jinja2') + self.response.out.write(template.render(context)) + + +application = webapp2.WSGIApplication([ + ('/', MainHandler), +], debug=True) diff --git a/appengine/i18n/static/js/main.js b/appengine/i18n/static/js/main.js new file mode 100644 index 000000000000..81986d124b02 --- /dev/null +++ b/appengine/i18n/static/js/main.js @@ -0,0 +1,9 @@ + +function init() { + + document.getElementById('js-message').innerHTML = + gettext('Hello World from javascript!') +} + + +window.onload = init; diff --git a/appengine/i18n/templates/i18n_js.jinja2 b/appengine/i18n/templates/i18n_js.jinja2 new file mode 100644 index 000000000000..07f66febac59 --- /dev/null +++ b/appengine/i18n/templates/i18n_js.jinja2 @@ -0,0 +1,38 @@ +{% extends "null_i18n_js.jinja2" %} +{% block i18n_functions -%} +var translations = {{ translations|safe }} +function get_value_from_translations(translations, msgid) { + var ret = translations['catalog'][msgid] + if (typeof(ret) == 'undefined' && + translations['fallback'] != null) { + ret = get_value_from_translations(translations['fallback'], msgid); + } + return ret; +} + +function plural_index(count, translations) { + var s = 'var n = ' + count + '; var v = ' + translations['plural']; + eval(s); + return v; +} + +function gettext(msgid) { + var value = get_value_from_translations(translations, msgid); + + if (typeof(value) == 'undefined') { + return msgid; + } else { + return (typeof(value) == 'string') ? value : value[0]; + } +} + +function ngettext(singular, plural, count) { + var value = get_value_from_translations(translations, singular); + + if (typeof(value) == 'undefined') { + return (count == 1) ? singular : plural; + } else { + return value[plural_index(count, translations)]; + } +} +{%- endblock %} \ No newline at end of file diff --git a/appengine/i18n/templates/index.jinja2 b/appengine/i18n/templates/index.jinja2 new file mode 100644 index 000000000000..dd9687d590c5 --- /dev/null +++ b/appengine/i18n/templates/index.jinja2 @@ -0,0 +1,13 @@ + + +{{ get_i18n_js_tag() }} +An internationalized message generated in the Python code: {{ message|e }}
+An internationalized message generated in jinja2 template: +{{ gettext('Hello World from Jinja2 template!')|e }}
+ +An internationalized message generated in javascript: +
+ + + diff --git a/appengine/i18n/templates/javascript_tag.jinja2 b/appengine/i18n/templates/javascript_tag.jinja2 new file mode 100644 index 000000000000..2f7952359dc5 --- /dev/null +++ b/appengine/i18n/templates/javascript_tag.jinja2 @@ -0,0 +1,3 @@ + diff --git a/appengine/i18n/templates/null_i18n_js.jinja2 b/appengine/i18n/templates/null_i18n_js.jinja2 new file mode 100644 index 000000000000..4c7837320bdc --- /dev/null +++ b/appengine/i18n/templates/null_i18n_js.jinja2 @@ -0,0 +1,15 @@ +{% block i18n_functions -%} +function gettext(s) { return s; }; +function ngettext(singular, plural, count) { + return (count == 1) ? singular :plural; +} +{%- endblock %} +String.prototype.format = function() { + var args = arguments; + return this.replace(/{(\d+)}/g, function(match, number) { + return typeof args[number] != 'undefined' + ? args[number] + : match + ; + }); +}; diff --git a/tox.ini b/tox.ini index 4df2a11487d9..2f109bdd67ea 100644 --- a/tox.ini +++ b/tox.ini @@ -75,4 +75,4 @@ deps = flake8 flake8-import-order commands = - flake8 --max-complexity=10 --import-order-style=google {posargs} + flake8 --builtin=gettext --max-complexity=10 --import-order-style=google {posargs}