summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2013-08-18 22:30:05 +0000
committerGerrit Code Review <review@openstack.org>2013-08-18 22:30:05 +0000
commit9c92d27937f733645631eb43a1ad48bae78d630c (patch)
tree85dbaa61d42caf06246d90d97c0deeba943763ce
parent14cba15fcb9a03adb82bbe586f6431791077f1e8 (diff)
parent760856e9669b0af20909c56d4a5a082bd4d7e450 (diff)
downloadkeystone-9c92d27937f733645631eb43a1ad48bae78d630c.tar.gz
keystone-9c92d27937f733645631eb43a1ad48bae78d630c.tar.xz
keystone-9c92d27937f733645631eb43a1ad48bae78d630c.zip
Merge "Add support for API message localization"
-rw-r--r--.gitignore1
-rwxr-xr-xbin/keystone-all5
-rw-r--r--doc/source/developing.rst31
-rw-r--r--httpd/keystone.py5
-rw-r--r--keystone/common/wsgi.py37
-rw-r--r--keystone/tests/test_wsgi.py87
6 files changed, 153 insertions, 13 deletions
diff --git a/.gitignore b/.gitignore
index 1297ba42..26415afa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,3 +27,4 @@ etc/logging.conf
keystone/tests/tmp/
.project
.pydevproject
+keystone/locale/*/LC_MESSAGES/*.mo
diff --git a/bin/keystone-all b/bin/keystone-all
index bb755606..187a2ee1 100755
--- a/bin/keystone-all
+++ b/bin/keystone-all
@@ -67,7 +67,10 @@ def serve(*servers):
if __name__ == '__main__':
- gettextutils.install('keystone')
+ # NOTE(blk-u): Configure gettextutils for deferred translation of messages
+ # so that error messages in responses can be translated according to the
+ # Accept-Language in the request rather than the Keystone server locale.
+ gettextutils.install('keystone', lazy=True)
dev_conf = os.path.join(possible_topdir,
'etc',
diff --git a/doc/source/developing.rst b/doc/source/developing.rst
index 7029e1c8..312d7892 100644
--- a/doc/source/developing.rst
+++ b/doc/source/developing.rst
@@ -228,6 +228,37 @@ installed devstack with a different LDAP password, modify the file
``keystone/tests/backend_liveldap.conf`` to reflect your password.
+Translated responses
+--------------------
+
+The Keystone server can provide error responses translated into the language in
+the ``Accept-Language`` header of the request. In order to test this in your
+development environment, there's a couple of things you need to do.
+
+1. Build the message files. Run the following command in your keystone
+ directory::
+
+ $ python setup.py compile_catalog
+
+This will generate .mo files like keystone/locale/[lang]/LC_MESSAGES/[lang].mo
+
+2. When running Keystone, set the ``KEYSTONE_LOCALEDIR`` environment variable
+ to the keystone/locale directory. For example::
+
+ $ KEYSTONE_LOCALEDIR=/opt/stack/keystone/keystone/locale keystone-all
+
+Now you can get a translated error response::
+
+ $ curl -s -H "Accept-Language: zh" http://localhost:5000/notapath | python -mjson.tool
+ {
+ "error": {
+ "code": 404,
+ "message": "\u627e\u4e0d\u5230\u8cc7\u6e90\u3002",
+ "title": "Not Found"
+ }
+ }
+
+
Building the Documentation
==========================
diff --git a/httpd/keystone.py b/httpd/keystone.py
index c7373549..492d2519 100644
--- a/httpd/keystone.py
+++ b/httpd/keystone.py
@@ -7,7 +7,10 @@ from keystone.common import logging
from keystone import config
from keystone.openstack.common import gettextutils
-gettextutils.install('keystone')
+# NOTE(blk-u): Configure gettextutils for deferred translation of messages
+# so that error messages in responses can be translated according to the
+# Accept-Language in the request rather than the Keystone server locale.
+gettextutils.install('keystone', lazy=True)
LOG = logging.getLogger(__name__)
CONF = config.CONF
diff --git a/keystone/common/wsgi.py b/keystone/common/wsgi.py
index d515fde6..646bb4c4 100644
--- a/keystone/common/wsgi.py
+++ b/keystone/common/wsgi.py
@@ -29,6 +29,7 @@ import webob.exc
from keystone.common import config
from keystone.common import utils
from keystone import exception
+from keystone.openstack.common import gettextutils
from keystone.openstack.common import importutils
from keystone.openstack.common import jsonutils
from keystone.openstack.common import log as logging
@@ -123,7 +124,14 @@ def validate_token_bind(context, token_ref):
class Request(webob.Request):
- pass
+ def best_match_language(self):
+ """Determines the best available locale from the Accept-Language
+ HTTP header passed in the request.
+ """
+
+ return self.accept_language.best_match(
+ gettextutils.get_available_languages('keystone'),
+ default_match='en_US')
class BaseApplication(object):
@@ -231,16 +239,18 @@ class Application(BaseApplication):
LOG.warning(
_('Authorization failed. %(exception)s from %(remote_addr)s') %
{'exception': e, 'remote_addr': req.environ['REMOTE_ADDR']})
- return render_exception(e)
+ return render_exception(e, user_locale=req.best_match_language())
except exception.Error as e:
LOG.warning(e)
- return render_exception(e)
+ return render_exception(e, user_locale=req.best_match_language())
except TypeError as e:
LOG.exception(e)
- return render_exception(exception.ValidationError(e))
+ return render_exception(exception.ValidationError(e),
+ user_locale=req.best_match_language())
except Exception as e:
LOG.exception(e)
- return render_exception(exception.UnexpectedError(exception=e))
+ return render_exception(exception.UnexpectedError(exception=e),
+ user_locale=req.best_match_language())
if result is None:
return render_response(status=(204, 'No Content'))
@@ -364,13 +374,16 @@ class Middleware(Application):
return self.process_response(request, response)
except exception.Error as e:
LOG.warning(e)
- return render_exception(e)
+ return render_exception(e,
+ user_locale=request.best_match_language())
except TypeError as e:
LOG.exception(e)
- return render_exception(exception.ValidationError(e))
+ return render_exception(exception.ValidationError(e),
+ user_locale=request.best_match_language())
except Exception as e:
LOG.exception(e)
- return render_exception(exception.UnexpectedError(exception=e))
+ return render_exception(exception.UnexpectedError(exception=e),
+ user_locale=request.best_match_language())
class Debug(Middleware):
@@ -472,7 +485,8 @@ class Router(object):
match = req.environ['wsgiorg.routing_args'][1]
if not match:
return render_exception(
- exception.NotFound(_('The resource could not be found.')))
+ exception.NotFound(_('The resource could not be found.')),
+ user_locale=req.best_match_language())
app = match['controller']
return app
@@ -566,12 +580,13 @@ def render_response(body=None, status=None, headers=None):
headerlist=headers)
-def render_exception(error):
+def render_exception(error, user_locale=None):
"""Forms a WSGI response based on the current error."""
body = {'error': {
'code': error.code,
'title': error.title,
- 'message': unicode(error)
+ 'message': unicode(gettextutils.get_localized_message(error.args[0],
+ user_locale)),
}}
if isinstance(error, exception.AuthPluginException):
body['error']['identity'] = error.authentication
diff --git a/keystone/tests/test_wsgi.py b/keystone/tests/test_wsgi.py
index 781159e2..0dfa9467 100644
--- a/keystone/tests/test_wsgi.py
+++ b/keystone/tests/test_wsgi.py
@@ -14,8 +14,14 @@
# License for the specific language governing permissions and limitations
# under the License.
+import uuid
+
+from babel import localedata
+import gettext
+
from keystone.common import wsgi
from keystone import exception
+from keystone.openstack.common import gettextutils
from keystone.openstack.common import jsonutils
from keystone.tests import core as test
@@ -211,3 +217,84 @@ class WSGIFunctionTest(test.TestCase):
message = 'test = "param1" : "value"'
self.assertEqual(wsgi.mask_password(message),
'test = "param1" : "value"')
+
+
+class LocalizedResponseTest(test.TestCase):
+ def setUp(self):
+ super(LocalizedResponseTest, self).setUp()
+ gettextutils._AVAILABLE_LANGUAGES = []
+
+ def tearDown(self):
+ gettextutils._AVAILABLE_LANGUAGES = []
+ super(LocalizedResponseTest, self).tearDown()
+
+ def _set_expected_languages(self, all_locales=[], avail_locales=None):
+ # Override localedata.locale_identifiers to return some locales.
+ def returns_some_locales(*args, **kwargs):
+ return all_locales
+
+ self.stubs.Set(localedata, 'locale_identifiers', returns_some_locales)
+
+ # Override gettext.find to return other than None for some languages.
+ def fake_gettext_find(lang_id, *args, **kwargs):
+ found_ret = '/keystone/%s/LC_MESSAGES/keystone.mo' % lang_id
+ if avail_locales is None:
+ # All locales are available.
+ return found_ret
+ languages = kwargs['languages']
+ if languages[0] in avail_locales:
+ return found_ret
+ return None
+
+ self.stubs.Set(gettext, 'find', fake_gettext_find)
+
+ def test_request_match_default(self):
+ # The default language if no Accept-Language is provided is en_US
+ req = wsgi.Request.blank('/')
+ self.assertEquals(req.best_match_language(), 'en_US')
+
+ def test_request_match_language_expected(self):
+ # If Accept-Language is a supported language, best_match_language()
+ # returns it.
+
+ self._set_expected_languages(all_locales=['it'])
+
+ req = wsgi.Request.blank('/', headers={'Accept-Language': 'it'})
+ self.assertEquals(req.best_match_language(), 'it')
+
+ def test_request_match_language_unexpected(self):
+ # If Accept-Language is a language we do not support,
+ # best_match_language() returns the default.
+
+ self._set_expected_languages(all_locales=['it'])
+
+ req = wsgi.Request.blank('/', headers={'Accept-Language': 'zh'})
+ self.assertEquals(req.best_match_language(), 'en_US')
+
+ def test_localized_message(self):
+ # If the accept-language header is set on the request, the localized
+ # message is returned by calling get_localized_message.
+
+ LANG_ID = uuid.uuid4().hex
+ ORIGINAL_TEXT = uuid.uuid4().hex
+ TRANSLATED_TEXT = uuid.uuid4().hex
+
+ self._set_expected_languages(all_locales=[LANG_ID])
+
+ def fake_get_localized_message(message, user_locale):
+ if (user_locale == LANG_ID and
+ message == ORIGINAL_TEXT):
+ return TRANSLATED_TEXT
+
+ self.stubs.Set(gettextutils, 'get_localized_message',
+ fake_get_localized_message)
+
+ error = exception.NotFound(message=ORIGINAL_TEXT)
+ resp = wsgi.render_exception(error, user_locale=LANG_ID)
+ result = jsonutils.loads(resp.body)
+
+ exp = {'error': {'message': TRANSLATED_TEXT,
+ 'code': 404,
+ 'title': 'Not Found'}}
+
+ self.assertEqual(exp, result)