Skip to content

Commit 599496d

Browse files
committed
Merge pull request #119 from GoogleCloudPlatform/i18n
Move sample from appengine-i18n-sample-python
2 parents 3e9cc9f + 1a42d89 commit 599496d

26 files changed

+666
-1
lines changed

appengine/i18n/README.md

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# App Engine Internationalization sample in Python
2+
3+
A simple example app showing how to build an internationalized app
4+
with App Engine. The main purpose of this example is to provide the
5+
basic how-to.
6+
7+
## What to internationalize
8+
9+
There are lots of things to internationalize with your web
10+
applications.
11+
12+
1. Strings in Python code
13+
2. Strings in HTML template
14+
3. Strings in Javascript
15+
4. Common strings
16+
- Country Names, Language Names, etc.
17+
5. Formatting
18+
- Date/Time formatting
19+
- Number formatting
20+
- Currency
21+
6. Timezone conversion
22+
23+
This example only covers first 3 basic scenarios above. In order to
24+
cover other aspects, I recommend using
25+
[Babel](http://babel.edgewall.org/) and [pytz]
26+
(http://pypi.python.org/pypi/gaepytz). Also, you may want to use
27+
[webapp2_extras.i18n](http://webapp-improved.appspot.com/tutorials/i18n.html)
28+
module.
29+
30+
## Wait, so why not webapp2_extras.i18n?
31+
32+
webapp2_extras.i18n doesn't cover how to internationalize strings in
33+
Javascript code. Additionally it depends on babel and pytz, which
34+
means you need to deploy babel and pytz alongside with your code. I'd
35+
like to show a reasonably minimum example for string
36+
internationalization in Python code, jinja2 templates, as well as
37+
Javascript.
38+
39+
## How to run this example
40+
41+
First of all, please install babel in your local Python environment.
42+
43+
### Wait, you just said I don't need babel, are you crazy?
44+
45+
As I said before, you don't need to deploy babel with this
46+
application, but you need to locally use pybabel script which is
47+
provided by babel distribution in order to extract the strings, manage
48+
and compile the translations file.
49+
50+
### Extract strings in Python code and Jinja2 templates to translate
51+
52+
Move into this project directory and invoke the following command:
53+
54+
$ env PYTHONPATH=/google_appengine_sdk/lib/jinja2 \
55+
pybabel extract -o locales/messages.pot -F main.mapping .
56+
57+
This command creates a `locales/messages.pot` file in the `locales`
58+
directory which contains all the string found in your Python code and
59+
Jija2 tempaltes.
60+
61+
Since the babel configration file `main.mapping` contains a reference
62+
to `jinja2.ext.babel_extract` helper function which is provided by
63+
jinja2 distribution bundled with the App Engine SDK, you need to add a
64+
PYTHONPATH environment variable pointing to the jinja2 directory in
65+
the SDK.
66+
67+
### Manage and compile translations.
68+
69+
Create an initial translation source by the following command:
70+
71+
$ pybabel init -l ja -d locales -i locales/messages.pot
72+
73+
Open `locales/ja/LC_MESSAGES/messages.po` with any text editor and
74+
translate the strings, then compile the file by the following command:
75+
76+
$ pybabel compile -d locales
77+
78+
If any of the strings changes, you can extract the strings again, and
79+
update the translations by the following command:
80+
81+
$ pybabel update -l ja -d locales -i locales/messages.pot
82+
83+
Note: If you run `pybabel init` against an existant translations file,
84+
you will lose your translations.
85+
86+
87+
### Extract strings in Javascript code and compile translations
88+
89+
$ pybabel extract -o locales/jsmessages.pot -F js.mapping .
90+
$ pybabel init -l ja -d locales -i locales/jsmessages.pot -D jsmessages
91+
92+
Open `locales/ja/LC_MESSAGES/jsmessages.po` and translate it.
93+
94+
$ pybabel compile -d locales -D jsmessages
95+
96+
97+
## How it works
98+
99+
As you can see it in the `appengine_config.py` file, our
100+
`main.application` is wrapped by the `i18n_utils.I18nMiddleware` WSGI
101+
middleware. When a request comes in, this middleware parses the
102+
`HTTP_ACCEPT_LANGUAGE` HTTP header, loads available translation
103+
files(`messages.mo`) from the application directory, and install the
104+
`gettext` and `ngettext` functions to the `__builtin__` namespace in
105+
the Python runtime.
106+
107+
For strings in Jinja2 templates, there is the `i18n_utils.BaseHandler`
108+
class from which you can extend in order to have a handy property
109+
named `jinja2_env` that lazily initializes Jinja2 environment for you
110+
with the `jinja2.ext.i18n` extention, and similar to the
111+
`I18nMiddleware`, installs `gettext` and `ngettext` functions to the
112+
global namespace of the Jinja2 environment.
113+
114+
## What about Javascript?
115+
116+
The `BaseHandler` class also installs the `get_i18n_js_tag()` instance
117+
method to the Jinja2 global namespace. When you use this function in
118+
your Jinja2 template (like in the `index.jinja2` file), you will get a
119+
set of Javascript functions; `gettext`, `ngettext`, and `format` on
120+
the string type. The `format` function can be used with `ngettext`ed
121+
strings for number formatting. See this example:
122+
123+
window.alert(ngettext(
124+
'You need to provide at least {0} item.',
125+
'You need to provide at least {0} items.',
126+
n).format(n);

appengine/i18n/__init__.py

Whitespace-only changes.

appengine/i18n/app.yaml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
application: i18n-sample-python
2+
runtime: python27
3+
api_version: 1
4+
version: 1
5+
threadsafe: true
6+
7+
8+
handlers:
9+
- url: /favicon\.ico
10+
static_files: favicon.ico
11+
upload: favicon\.ico
12+
13+
- url: /static
14+
static_dir: static
15+
- url: /.*
16+
script: main.application
17+
18+
libraries:
19+
- name: webapp2
20+
version: latest
21+
- name: jinja2
22+
version: latest
23+
- name: webob
24+
version: 1.2.3

appengine/i18n/appengine_config.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#!/usr/bin/env python
2+
#
3+
# Copyright 2013 Google Inc.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
"""App Engine configuration file for applying the I18nMiddleware."""
18+
19+
20+
from i18n_utils import I18nMiddleware
21+
22+
23+
def webapp_add_wsgi_middleware(app):
24+
"""Applying the I18nMiddleware to our HelloWorld app.
25+
26+
Args:
27+
app: The WSGI application object that you want to wrap with the
28+
I18nMiddleware.
29+
30+
Returns:
31+
The wrapped WSGI application.
32+
"""
33+
34+
app = I18nMiddleware(app)
35+
return app

appengine/i18n/i18n_utils.py

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
#!/usr/bin/env python
2+
#
3+
# Copyright 2013 Google Inc.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
"""A small module for i18n of webapp2 and jinja2 based apps.
18+
19+
The idea of this example, especially for how to translate strings in
20+
Javascript is originally from an implementation of Django i18n.
21+
"""
22+
23+
24+
import gettext
25+
import json
26+
import os
27+
28+
import jinja2
29+
30+
import webapp2
31+
32+
from webob import Request
33+
34+
35+
def _get_plural_forms(js_translations):
36+
"""Extracts the parameters for what constitutes a plural.
37+
38+
Args:
39+
js_translations: GNUTranslations object to be converted.
40+
41+
Returns:
42+
A tuple of:
43+
A formula for what constitutes a plural
44+
How many plural forms there are
45+
"""
46+
plural = None
47+
n_plural = 2
48+
if '' in js_translations._catalog:
49+
for l in js_translations._catalog[''].split('\n'):
50+
if l.startswith('Plural-Forms:'):
51+
plural = l.split(':', 1)[1].strip()
52+
print "plural is %s" % plural
53+
if plural is not None:
54+
for raw_element in plural.split(';'):
55+
element = raw_element.strip()
56+
if element.startswith('nplurals='):
57+
n_plural = int(element.split('=', 1)[1])
58+
elif element.startswith('plural='):
59+
plural = element.split('=', 1)[1]
60+
print "plural is now %s" % plural
61+
else:
62+
n_plural = 2
63+
plural = '(n == 1) ? 0 : 1'
64+
return plural, n_plural
65+
66+
67+
def convert_translations_to_dict(js_translations):
68+
"""Convert a GNUTranslations object into a dict for jsonifying.
69+
70+
Args:
71+
js_translations: GNUTranslations object to be converted.
72+
73+
Returns:
74+
A dictionary representing the GNUTranslations object.
75+
"""
76+
plural, n_plural = _get_plural_forms(js_translations)
77+
78+
translations_dict = {'plural': plural, 'catalog': {}, 'fallback': None}
79+
if js_translations._fallback is not None:
80+
translations_dict['fallback'] = convert_translations_to_dict(
81+
js_translations._fallback
82+
)
83+
for key, value in js_translations._catalog.items():
84+
if key == '':
85+
continue
86+
if type(key) in (str, unicode):
87+
translations_dict['catalog'][key] = value
88+
elif type(key) == tuple:
89+
if key[0] not in translations_dict['catalog']:
90+
translations_dict['catalog'][key[0]] = [''] * n_plural
91+
translations_dict['catalog'][key[0]][int(key[1])] = value
92+
return translations_dict
93+
94+
95+
class BaseHandler(webapp2.RequestHandler):
96+
"""A base handler for installing i18n-aware Jinja2 environment."""
97+
98+
@webapp2.cached_property
99+
def jinja2_env(self):
100+
"""Cached property for a Jinja2 environment.
101+
102+
Returns:
103+
Jinja2 Environment object.
104+
"""
105+
106+
jinja2_env = jinja2.Environment(
107+
loader=jinja2.FileSystemLoader(
108+
os.path.join(os.path.dirname(__file__), 'templates')),
109+
extensions=['jinja2.ext.i18n'])
110+
jinja2_env.install_gettext_translations(
111+
self.request.environ['i18n_utils.active_translation'])
112+
jinja2_env.globals['get_i18n_js_tag'] = self.get_i18n_js_tag
113+
return jinja2_env
114+
115+
def get_i18n_js_tag(self):
116+
"""Generates a Javascript tag for i18n in Javascript.
117+
118+
This instance method is installed to the global namespace of
119+
the Jinja2 environment, so you can invoke this method just
120+
like `{{ get_i18n_js_tag() }}` from anywhere in your Jinja2
121+
template.
122+
123+
Returns:
124+
A 'javascript' HTML tag which contains functions and
125+
translation messages for i18n.
126+
"""
127+
128+
template = self.jinja2_env.get_template('javascript_tag.jinja2')
129+
return template.render({'javascript_body': self.get_i18n_js()})
130+
131+
def get_i18n_js(self):
132+
"""Generates a Javascript body for i18n in Javascript.
133+
134+
If you want to load these javascript code from a static HTML
135+
file, you need to create another handler which just returns
136+
the code generated by this function.
137+
138+
Returns:
139+
Actual javascript code for functions and translation
140+
messages for i18n.
141+
"""
142+
143+
try:
144+
js_translations = gettext.translation(
145+
'jsmessages', 'locales', fallback=False,
146+
languages=self.request.environ[
147+
'i18n_utils.preferred_languages'],
148+
codeset='utf-8')
149+
except IOError:
150+
template = self.jinja2_env.get_template('null_i18n_js.jinja2')
151+
return template.render()
152+
153+
translations_dict = convert_translations_to_dict(js_translations)
154+
template = self.jinja2_env.get_template('i18n_js.jinja2')
155+
return template.render(
156+
{'translations': json.dumps(translations_dict, indent=1)})
157+
158+
159+
class I18nMiddleware(object):
160+
"""A WSGI middleware for i18n.
161+
162+
This middleware determines users' preferred language, loads the
163+
translations files, and install it to the builtin namespace of the
164+
Python runtime.
165+
"""
166+
167+
def __init__(self, app, default_language='en', locale_path=None):
168+
"""A constructor for this middleware.
169+
170+
Args:
171+
app: A WSGI app that you want to wrap with this
172+
middleware.
173+
default_language: fallback language; ex: 'en', 'ja', etc.
174+
locale_path: A directory containing the translations
175+
file. (defaults to 'locales' directory)
176+
"""
177+
178+
self.app = app
179+
if locale_path is None:
180+
locale_path = os.path.join(
181+
os.path.abspath(os.path.dirname(__file__)), 'locales')
182+
self.locale_path = locale_path
183+
self.default_language = default_language
184+
185+
def __call__(self, environ, start_response):
186+
"""Called by WSGI when a request comes in.
187+
188+
Args:
189+
environ: A dict holding environment variables.
190+
start_response: A WSGI callable (PEP333).
191+
192+
Returns:
193+
Application response data as an iterable. It just returns
194+
the return value of the inner WSGI app.
195+
"""
196+
req = Request(environ)
197+
preferred_languages = list(req.accept_language)
198+
if self.default_language not in preferred_languages:
199+
preferred_languages.append(self.default_language)
200+
translation = gettext.translation(
201+
'messages', self.locale_path, fallback=True,
202+
languages=preferred_languages, codeset='utf-8')
203+
translation.install(unicode=True, names=['gettext', 'ngettext'])
204+
environ['i18n_utils.active_translation'] = translation
205+
environ['i18n_utils.preferred_languages'] = preferred_languages
206+
207+
return self.app(environ, start_response)

appengine/i18n/js.mapping

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[javascript: **.js]
526 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)