-
Notifications
You must be signed in to change notification settings - Fork 4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
mattdrayer/xblock-translations: I18N/L10N for XBlocks #11575
Conversation
@@ -127,7 +127,7 @@ def enable_stanford_theme(): | |||
|
|||
# Include theme locale path for django translations lookup | |||
settings.LOCALE_PATHS = (theme_root / 'conf/locale',) + settings.LOCALE_PATHS | |||
|
|||
settings.LOCALE_PATHS = ('/edx/src/xblock-drag-and-drop-v2/conf/locale',) + settings.LOCALE_PATHS |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure that this line is necessary -- we can probably remove it.
Please don't ever use plain str() where the value could be a unicode string. If you do, then non-ascii characters will cause a UnicodeEncodeError to be thrown. |
@@ -924,6 +924,10 @@ | |||
|
|||
# Localization strings (e.g. django.po) are under this directory | |||
LOCALE_PATHS = (REPO_ROOT + '/conf/locale',) # edx-platform/conf/locale/ | |||
|
|||
# TODO: Figure out a way to discover locale paths for installed components | |||
LOCALE_PATHS = ('/edx/src/drag-and-drop-v2/conf/locale',) + LOCALE_PATHS |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this needed if we are using gettext directly to load XBlock translations? LOCALE_PATHS is only a Django thing, so if we aren't using Django machinery to get the XBlock translations, then we won't have to tweak it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@asadiqbal08 and I discussed this late last night and we're also thinking it's not needed.
25cf4e2
to
f9cce55
Compare
Okay, I've done some housecleaning on this PR to get rid of a lot of the extraneous stuff. @asadiqbal08 I noted that we could move some things into settings params, and we could also consider supporting multiple values for both domain and the xblock locale which would allow for some flexibility on the XBlock developer's side. However these are not required, and per a previous conversation between @nedbat and me, it might be easier to simply support a single locale/domain and document it as such, which would give people an easier way to provide translations with their XBlocks. |
@@ -243,9 +244,38 @@ class ModuleI18nService(object): | |||
i18n service. | |||
|
|||
""" | |||
|
|||
def __getattr__(self, name): | |||
return getattr(django.utils.translation, name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not entirely sure we want to remove getattr here -- are there other instances of ModuleI18nService calls which would require support for a clear pass-through to django.utils.translation? Otherwise an AttributeError would be raised and the operation would need to be added to the service at that point, right? Of course it could still be a simple pass-through to d.u.t., but we're moving from an implicit approach to an explicit approach here, no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok @mattdrayer , I think we should keep __getattr__
here. I am not sure but may be it is possible that some xBlocks trying to access some other attribute of django.utils.translation
instead of ugettext
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I checked all the XBlocks we know about: they use ugettext
, ungettext
, strftime
, and with an attribute check, get_language_bidi
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should document the methods we support on the i18n service in the XBlock docs.
I don't understand where in this code the XBlock's translations are found. The |
# TODO: Move these values into settings. We might allow for multiple possibilities | ||
# for these values through the use of lists or tuples. For example, instead of | ||
# 'django' an XBlock developer could specify 'xblock_name' for their PO file. | ||
xblock_domain = 'django' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we are skipping the Django logic, shouldn't we use a different domain name?
@nedbat |
# TODO: Move these values into settings. We might allow for multiple possibilities | ||
# for these values through the use of lists or tuples. For example, instead of | ||
# 'django' an XBlock developer could specify 'xblock_name' for their PO file. | ||
xblock_domain = kwargs.get('xblock_domain', 'django') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mattdrayer can you look here ? each xBlock can specify its own domain as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
hope build will be green now ! |
It doesn't make sense to me to change the signature of ugettext. What is xblock_root for, and why should XBlocks have to pass it in? Why would an XBlock want to specify its own domain? If it does want to, why is ugettext the right place to do it? I don't feel like we've considered the API we're presenting to the XBlock here. |
If there's a way for the runtime to determine the path/location of the XBlock's installation, then we could remove the need to specify the xblock_root from the XBlock's side of things. As far as domain and locale go, these are not required, but we were thinking it might be useful to allow an XBlock developer to specify an alternative location and domain (ie, the name of the PO/MO files) of the translations if they choose. We could certainly remove that flexibility, however, if you think it's unnecessary or confusing. |
I would like to provide this capability in an opinionated simple way. We don't need to provide for all possibilities, we need to make it possible to provide translations. But even if we want to provide those options, is ugettext the right place? Remember, we also need to implement a few other methods here. And there's a self.runtime.service call that can help with this stuff. |
Yes, I agree that ugettext is too specific of a place -- no reason to reimplement for every additional gettext operation we want to support. @asadiqbal08 I'm looking into the ResourceManager API from setuptools, which was a recommendation from @cpennington which may allow us to avoid forcing the XBlock to inform the runtime of its location (ie, xblock_root). If we can crack that, then we can drop the entire extra_info approach and document the conventions for locale and domain. See this link for info on setuptools.ResourceManager: https://pythonhosted.org/setuptools/pkg_resources.html#resourcemanager-api |
@cpennington @nedbat -- getting closer to a real implementation -- this changeset now utilizes the pkg_resources library on the runtime-side to determine the XBlock installation location. The XBlock runtime's service locator is overridden for LMS and Studio. Let us know what you think and if the approach is sound we can work on cleaning things up, proper test coverage, etc. |
from request_cache.middleware import RequestCache | ||
from xblock.exceptions import NoSuchServiceError | ||
import xblock.reference.plugins | ||
from xblock.runtime import NullI18nService |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can drop this import now
@mattdrayer I like where this is going :) We're keeping the XBlock API simple and easy to use. |
try: | ||
xblock_resource = self.block.__class__.unmixed_class.__module__ | ||
xblock_locale_dir = '/conf/locale' | ||
xblock_locale_path = resource_filename(xblock_resource, xblock_locale_dir) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'll need to write instructions for XBlocks, and the layout will look different than the way edx-platform and edx-ora2 both do it. The "conf/locale" directory won't be at the root of the project, but inside one of the package directories.
[to_locale(selected_language if selected_language else settings.LANGUAGE_CODE)] | ||
) | ||
except IOError: | ||
self.translator = django.utils.translation |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nedbat fyi. let me know about it.
fa3ca94
to
8d5f86e
Compare
jenkins run python |
translated_string = unicode(string) | ||
if translated_string: | ||
translated_string = self.translator.ugettext(translated_string) # pylint: disable=translation-of-non-string | ||
return translated_string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couldn't we just change __getattr__
to read from self.translator
(which is either xblock or django), and then not have to override ugettext
at all?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it appears we can -- just pushed a commit to remove the now-unnecessary override -- good catch!
fef8d9a
to
ebbdd37
Compare
Squashed a lot of the working PRs into the main commit -- will be working on an implementation of the "callable" suggestion today. |
@cpennington @nedbat I've pushed a commit that reflects the suggested 'callable' approach -- if you can take a look and let us know what you think, that would be great -- @asadiqbal08 FYI |
if callable(service): | ||
return service(block) | ||
else: | ||
return super(ModuleSystem, self).service(block=block, service_name=service_name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, this looks like I was expecting it to.
jenkins run bokchoy |
3a688a1
to
3c26d56
Compare
@bradenmacdonald @nedbat @cpennington -- I've done another round of rebasing+squashing, and @asadiqbal08 has changed the locale/domain values to be more along the lines of Flask-Babel -- if you can take another look at the PR and note any additional concerns that would be great -- and if you're good with the state of things we'd love to start seeing some +1's 😄 |
@mattdrayer I've only looked at the code and done some quick tests in Studio, but I like this how this approach has shaped up, so 👍 from me. I'd be happy to do a more in depth review of the Drag and Drop XBlock integration with this code once it's updated. |
@@ -713,7 +713,6 @@ def rebind_noauth_module_to_user(module, real_user): | |||
wrappers=block_wrappers, | |||
get_real_user=user_by_anonymous_id, | |||
services={ | |||
'i18n': ModuleI18nService(), | |||
'fs': FSService(), | |||
'field-data': field_data, | |||
'user': DjangoXBlockUserService(user, user_is_staff=user_is_staff), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this still have the ModuleI18nService
installed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@asadiqbal08 -- I caught up with @cpennington today to understand more about why this particular line is necessary. It seems there is an LTI use case we need to support, which has to do with when the LTI XBlock is loaded. The XBlock needs to re-bind to the user associated with the authentication information passed along with the XBlock handler contents. The good news is that the fix should be as easy as restoring the above line.
In addition to reverting the above change, however, we should ensure that the LTI use case is not impacted in the future by protecting it with a unit test. The test can utilize a simple XBlock that calls the re-bind method and confirms the proper services are both defined and the same before and after the re-binding process is complete.
@cpennington, if there's any more context/information to add here so @asadiqbal08 can write up the proper test, please feel free to share -- thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mattdrayer I was trying to get your point here but sorry I cannot get it, what is the dependency of LTI xblock on i18n service ? We are already initializing the ModuleI18nService
in init
method of LmsModuleSystem
.
here
what is meant by re-bind method ? are you pointing out to -- Bind a module to another student / user ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mattdrayer is that you required to test ?
def assert_services(self, module):
"""
assert possible services
"""
self.assertEqual(module.runtime._services.get('i18n'), ModuleI18nService)
self.assertTrue(module.runtime._services.get('fs', False))
self.assertTrue(module.runtime._services.get('user', False))
self.assertTrue(module.runtime._services.get('reverification', False))
self.assertTrue(module.runtime._services.get('proctoring', False))
self.assertTrue(module.runtime._services.get('credit', False))
self.assertTrue(module.runtime._services.get('bookmarks', False))
def test_services_before_and_after_module_rebinding(self):
"""
LTI XBlock that calls the re-bind method and confirms the proper services.
"""
module = self.get_module_for_user(self.anon_user)
# assert services before rebinding
self.assert_services(module)
user2 = UserFactory.create()
module.system.rebind_noauth_module_to_user(module, user2)
# assert services after rebinding
self.assert_services(self.get_module_for_user(user2))
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@cpennington, is this what you had in mind? @asadiqbal08, does this new
test fail if you remove the ModuleI18nService() call above?
On Mar 9, 2016 7:45 AM, "Asad Iqbal" [email protected] wrote:
In lms/djangoapps/courseware/module_render.py
https://github.com/edx/edx-platform/pull/11575#discussion_r55512583:@@ -713,7 +713,6 @@ def rebind_noauth_module_to_user(module, real_user):
wrappers=block_wrappers,
get_real_user=user_by_anonymous_id,
services={
'i18n': ModuleI18nService(), 'fs': FSService(), 'field-data': field_data, 'user': DjangoXBlockUserService(user, user_is_staff=user_is_staff),
@mattdrayer https://github.com/mattdrayer is that you required to test
?def assert_services(self, module):
"""
assert possible services
"""
self.assertTrue(module.runtime._services.get('i18n', False))
self.assertTrue(module.runtime._services.get('fs', False))
self.assertTrue(module.runtime._services.get('user', False))
self.assertTrue(module.runtime._services.get('reverification', False))
self.assertTrue(module.runtime._services.get('proctoring', False))
self.assertTrue(module.runtime._services.get('credit', False))
self.assertTrue(module.runtime._services.get('bookmarks', False))def test_services_before_and_after_module_rebinding(self):
"""
LTI XBlock that calls the re-bind method and confirms the proper services.
"""
module = self.get_module_for_user(self.anon_user)
# assert services before rebinding
self.assert_services(module)
user2 = UserFactory.create()
module.system.rebind_noauth_module_to_user(module, user2)
# assert services after rebinding
self.assert_services(self.get_module_for_user(user2))—
Reply to this email directly or view it on GitHub
https://github.com/edx/edx-platform/pull/11575/files#r55512583.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mattdrayer I cannot understand why is it required to revert the above change ? talking about
i18n': ModuleI18nService
because we are already declaring it in init
of LmsModuleSystem .
In this new test I am explicitly testing for ModuleI18nService
, I am sure if we will not declare the i18n: ModuleI18nService
then test would be failed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we're going to have to ask @cpennington to provide some more clarity here -- I unfortunately don't have all the context around this requirement, either.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, @asadiqbal08, I'm sorry, you're right. I misread this code, and didn't realize that this line was being removed and the line during __init__
was being added to replace it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AH, okay then :) So we are good to go from @cpennington's perspective at this point -- I'd like to get one last +1 from @nedbat and then we should be clear to merge.
👍 after my remaining comment is fixed |
Caught up with @nedbat and we're good to go on this PR with the caveat that we'll finish up the WIP RTD documentation in the very near future -- going ahead with the merge. |
…the runtime * mattdrayer: Latest proto code * mattdrayer: Add translation.py * asadiqbal08: Xblock translation ugettext update, remove translation.py * mattdrayer: Additional I18N work -- starting to see some translations! * asadiqbal08: Trying to make xBlock message catalog files path dynamic * mattdrayer: Remove unnecessary modifications * mattdrayer: Cleaned up implementation * mattdrayer: Moved import statement * asadiqbal08: update as suggested * asadiqbal08: xblock its own domain * asadiqbal08: translation: secure none object * asadiqbal08: pylint * asadiqbal08: get locale from xblock * asadiqbal08: update * mattdrayer: Determine XBlock locale path within runtime service * mattdrayer: Determine module location via the runtime * mattdrayer: Remove ModuleI18nService reference * asadiqbal08: override the service in studio * asadiqbal08: remove import * asadiqbal08: update the Modulei18nService * asadiqbal08: update the Modulei18nService * mattdrayer: Remove redundant __class__ reference * asadiqbal08: update the docstring * asadiqbal08: tests * mattdrayer: Remove specific ugettext override from ModuleI18nService * mattdrayer: Move service operation to base class * mattdrayer: Address quality violations * asadiqbal08: Investigating the test failure issue on jenkins and solved * asadiqbal08: First utilizing the parent class method * mattdrayer: Use recommended callable approach * asadiqbal08: remove unused code * asadiqbal08: Updated the test to use cms preview module system runtime in order to get i18n service. * asadiqbal08: Pylint quality * asadiqbal08: update the service call to check xblock declarations * asadiqbal08: update doc string * asadiqbal08: i18n callable test in studio * asadiqbal08: test lms runtime for modulei18n service * asadiqbal08: add doc strings * asadiqbal08: Rename locale and domain to Flask-Babel convention
3c26d56
to
3ed3fea
Compare
mattdrayer/xblock-translations: I18N/L10N for XBlocks
Add I18N/L10N support to XBlocks via the XBlock runtime (ModuleI18nService)
@asadiqbal08 @nedbat @cpennington @bradenmacdonald FYI