Merge "Add support for API message localization"
This commit is contained in:
commit
9c92d27937
1
.gitignore
vendored
1
.gitignore
vendored
@ -27,3 +27,4 @@ etc/logging.conf
|
||||
keystone/tests/tmp/
|
||||
.project
|
||||
.pydevproject
|
||||
keystone/locale/*/LC_MESSAGES/*.mo
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
==========================
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user