Skip to content

Commit

Permalink
i18n tooling and translation
Browse files Browse the repository at this point in the history
  • Loading branch information
OmarIthawi committed Sep 6, 2018
1 parent 1efbb08 commit db709c5
Show file tree
Hide file tree
Showing 22 changed files with 616 additions and 167 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ xblock_google_drive.egg-info
coverage.xml
htmlcov/

# Translations
*.pot

# IDEs and text editors
*~
*.swp
Expand Down
8 changes: 8 additions & 0 deletions .tx/config
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[main]
host = https://www.transifex.com

[xblocks.xblock-google-drive]
source_file = google_drive/translations/en/LC_MESSAGES/text.po
file_filter = google_drive/translations/<lang>/LC_MESSAGES/text.po
source_lang = en
type = PO
35 changes: 33 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: clean coverage docs help quality requirements selfcheck test test-all upgrade validate
.PHONY: clean coverage docs help quality requirements selfcheck test test-all upgrade validate dummy_translations extract_translations pull_translations push_translations

.DEFAULT_GOAL := help

Expand Down Expand Up @@ -61,7 +61,38 @@ test-all: ## run tests on every supported Python/Django combination
tox -e quality
tox

validate: quality test ## run tests and quality checks
validate: quality test validate_translations ## run tests and quality checks

## Localization targets
extract_translations: ## extract strings to be translated, outputting .po files
rm -rf docs/_build

# Extract Python and Django template strings
mkdir -p locale/en/LC_MESSAGES/
rm -f locale/en/LC_MESSAGES/{django,text}.po
django-admin makemessages -l en -v1 -d django
mv locale/en/LC_MESSAGES/django.po locale/en/LC_MESSAGES/text.po

compile_translations: ## compile translation files, outputting .mo files for each supported language
i18n_tool generate
make clean

detect_changed_source_translations: ## Determines if the source translation files are up-to-date, otherwise exit with a non-zero code.
i18n_tool changed

pull_translations: ## pull translations from Transifex
i18n_tool transifex pull
make compile_translations

push_translations: extract_translations ## push source translation files (.po) to Transifex
i18n_tool transifex push

dummy_translations: ## generate dummy translation (.po) files
i18n_tool dummy

build_dummy_translations: extract_translations dummy_translations compile_translations ## generate and compile dummy translation files

validate_translations: build_dummy_translations detect_changed_source_translations ## validate translations

selfcheck: ## check that the Makefile is well-formed
@echo "The Makefile is well-formed."
34 changes: 34 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,40 @@ might also have to prepend ``PYTHONPATH=".:/path/to/xblock"`` to the
command above. (``/path/to/xblock`` is the path to the xblock-sdk, where
the workbench resides).

Working with translations
-------------------------

For information about working with translations, see the `Internationalization Support`_ section of
the `Open edX XBlock Tutorial`_.

Prepare your virtualenv and ensure that the `Transifex authentication file`_
(``~/.transifexrc``) is properly set up.

Push new strings to Transifex:

.. code:: bash
$ make push_translations
To get the latest translations from Transifex:

.. code:: bash
$ make pull_translations
For testing purposes it's faster to avoid Transifex and work on dummy Esperanto translations:

.. code:: bash
$ make build_dummy_translations
.. _Internationalization Support: http://edx.readthedocs.io/projects/xblock-tutorial/en/latest/edx_platform/edx_lms.html#internationalization-support
.. _Open edX XBlock Tutorial: https://xblock-tutorial.readthedocs.io/en/latest/
.. _Transifex authentication file: https://openedx.atlassian.net/wiki/display/OpenOPS/Running+Fullstack

Changes to be documented
------------------------

Expand Down
41 changes: 27 additions & 14 deletions google_drive/google_calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
# Imports ###########################################################
import logging

from django import utils
from xblock.core import XBlock
from xblock.fields import Scope, String, Integer
from xblock.fragment import Fragment

from xblockutils.publish_event import PublishEventMixin
from xblockutils.resources import ResourceLoader

Expand All @@ -18,34 +18,42 @@
# Constants ###########################################################
DEFAULT_CALENDAR_ID = "edx.org_lom804qe3ttspplj1bgeu1l3ak@group.calendar.google.com"
DEFAULT_CALENDAR_URL = (
'https://www.google.com/calendar/embed?mode=Month&src={}&showCalendars=0'.format(DEFAULT_CALENDAR_ID))
'https://www.google.com/calendar/embed?mode=Month&src={}&showCalendars=0&hl=en-us'.format(DEFAULT_CALENDAR_ID))
CALENDAR_TEMPLATE = "/templates/html/google_calendar.html"
CALENDAR_EDIT_TEMPLATE = "/templates/html/google_calendar_edit.html"


def _(text):
"""
Dummy ugettext.
"""
return text


# Classes ###########################################################
class GoogleCalendarBlock(XBlock, PublishEventMixin): # pylint: disable=too-many-ancestors
@XBlock.needs("i18n") # pylint: disable=too-many-ancestors
class GoogleCalendarBlock(XBlock, PublishEventMixin):
"""
XBlock providing a google calendar view for a specific calendar
"""
display_name = String(
display_name="Display Name",
help="This name appears in the horizontal navigation at the top of the page.",
display_name=_("Display Name"),
help=_("This name appears in the horizontal navigation at the top of the page."),
scope=Scope.settings,
default="Google Calendar"
)
calendar_id = String(
display_name="Public Calendar ID",
help=(
display_name=_("Public Calendar ID"),
help=_(
"Google provides an ID for publicly available calendars. In the Google Calendar, "
"open Settings and copy the ID from the Calendar Address section into this field."
),
scope=Scope.settings,
default=DEFAULT_CALENDAR_ID
)
default_view = Integer(
display_name="Default View",
help="The calendar view that students see by default. A student can change this view.",
display_name=_("Default View"),
help=_("The calendar view that students see by default. A student can change this view."),
scope=Scope.settings,
default=1
)
Expand All @@ -58,11 +66,16 @@ def student_view(self, context): # pylint: disable=unused-argument
"""
fragment = Fragment()

fragment.add_content(RESOURCE_LOADER.render_template(CALENDAR_TEMPLATE, {
"mode": self.views[self.default_view][1],
"src": self.calendar_id,
"title": self.display_name,
}))
fragment.add_content(RESOURCE_LOADER.render_django_template(
CALENDAR_TEMPLATE,
context={
"mode": self.views[self.default_view][1],
"src": self.calendar_id,
"title": self.display_name,
"language": utils.translation.get_language(),
},
i18n_service=self.runtime.service(self, "i18n"),
))
fragment.add_css(RESOURCE_LOADER.load_unicode('public/css/google_calendar.css'))
fragment.add_javascript(RESOURCE_LOADER.load_unicode('public/js/google_calendar.js'))

Expand Down
28 changes: 20 additions & 8 deletions google_drive/google_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,20 +37,28 @@
DOCUMENT_EDIT_TEMPLATE = "/templates/html/google_docs_edit.html"


def _(text):
"""
Dummy ugettext.
"""
return text


# Classes ###########################################################
class GoogleDocumentBlock(XBlock, PublishEventMixin): # pylint: disable=too-many-ancestors
@XBlock.needs("i18n") # pylint: disable=too-many-ancestors
class GoogleDocumentBlock(XBlock, PublishEventMixin):
"""
XBlock providing a google document embed link
"""
display_name = String(
display_name="Display Name",
help="This name appears in the horizontal navigation at the top of the page.",
display_name=_("Display Name"),
help=_("This name appears in the horizontal navigation at the top of the page."),
scope=Scope.settings,
default="Google Document"
)
embed_code = String(
display_name="Embed Code",
help=(
display_name=_("Embed Code"),
help=_(
"Google provides an embed code for Drive documents. In the Google Drive document, "
"from the File menu, select Publish to the Web. Modify settings as needed, click "
"Publish, and copy the embed code into this field."
Expand All @@ -59,8 +67,8 @@ class GoogleDocumentBlock(XBlock, PublishEventMixin): # pylint: disable=too-man
default=DEFAULT_EMBED_CODE
)
alt_text = String(
display_name="Alternative Text",
help="Alternative text describes an image and appears if the image is unavailable.",
display_name=_("Alternative Text"),
help=_("Alternative text describes an image and appears if the image is unavailable."),
scope=Scope.settings,
default=""
)
Expand All @@ -72,7 +80,11 @@ def student_view(self, context): # pylint: disable=unused-argument
"""
fragment = Fragment()

fragment.add_content(RESOURCE_LOADER.render_template(DOCUMENT_TEMPLATE, {"self": self}))
fragment.add_content(RESOURCE_LOADER.render_django_template(
DOCUMENT_TEMPLATE,
context={"self": self},
i18n_service=self.runtime.service(self, 'i18n'),
))
fragment.add_css(RESOURCE_LOADER.load_unicode('public/css/google_docs.css'))
fragment.add_javascript(RESOURCE_LOADER.load_unicode('public/js/google_docs.js'))

Expand Down
2 changes: 1 addition & 1 deletion google_drive/templates/html/google_calendar.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div class="google-calendar-xblock-wrapper">
<input type="hidden" class="load_event_complete">
<iframe src="https://www.google.com/calendar/embed?mode={{mode}}&amp;src={{src}}&amp;showCalendars=0" title="{{title}}"></iframe>
<iframe src="https://www.google.com/calendar/embed?mode={{mode}}&amp;src={{src}}&amp;showCalendars=0&amp;hl={{language}}" title="{{title}}"></iframe>
</div>
4 changes: 2 additions & 2 deletions google_drive/templates/html/google_calendar_edit.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ <h2 class="alert_title">{% trans "Invalid Google Calendar" %}</h2>
<input class="input setting-input edit-display-name" id="edit_display_name" value="{{ self.display_name }}" type="text" data-default-value="{{defaultName}}">
<button class="action setting-clear clear-display-name" type="button" name="setting-clear">
<i class="icon fa fa-undo"></i>
<span class="sr">"<%= gettext("Clear Value") %>"</span>
<span class="sr">{% trans "Clear Value" %}</span>
</button>
</div>
<span class="tip setting-help">{% trans "This name appears in the horizontal navigation at the top of the page." %}</span>
Expand All @@ -29,7 +29,7 @@ <h2 class="alert_title">{% trans "Invalid Google Calendar" %}</h2>
<input class="input setting-input edit-calendar-id" id="edit_calendar_id" value="{{ self.calendar_id }}" type="text" data-default-value="{{defaultID}}">
<button class="action setting-clear clear-calendar-id" type="button" name="setting-clear">
<i class="icon fa fa-undo"></i>
<span class="sr">"<%= gettext("Clear Value") %>"</span>
<span class="sr">{% trans "Clear Value" %}</span>
</button>
</div>
<span class="tip setting-help">{% trans "Google provides an ID for publicly available calendars. In the Google Calendar, open Settings and copy the ID from the Calendar Address section into this field. You can " %}
Expand Down
2 changes: 1 addition & 1 deletion google_drive/templates/html/google_docs_edit.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ <h2 class="alert_title">{% trans "Invalid Google Document" %}</h2>
<input class="input setting-input edit-display-name" id="edit_display_name" value="{{ self.display_name }}" type="text" data-default-value="{{defaultName}}">
<button class="action setting-clear clear-display-name" type="button" name="setting-clear">
<i class="icon fa fa-undo"></i>
<span class="sr">"<%= gettext("Clear Value") %>"</span>
<span class="sr">{% trans "Clear Value" %}</span>
</button>
</div>
<span class="tip setting-help">{% trans "This name appears in the horizontal navigation at the top of the page." %}</span>
Expand Down
49 changes: 43 additions & 6 deletions google_drive/tests/unit/test_calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@
import json
import unittest
import cgi
import ddt
from mock import Mock

from django.utils.translation import override as override_language
from nose.tools import assert_equals, assert_in
from workbench.runtime import WorkbenchRuntime
from xblock.runtime import KvsFieldData, DictKeyValueStore

from google_drive import GoogleCalendarBlock
from google_drive.google_calendar import DEFAULT_CALENDAR_URL
from google_drive.google_calendar import DEFAULT_CALENDAR_ID, DEFAULT_CALENDAR_URL
from google_drive.tests.unit.test_utils import generate_scope_ids, make_request
from google_drive.tests.test_const import STUDIO_EDIT_WRAPPER, VALIDATION_WRAPPER, USER_INPUTS_WRAPPER, BUTTONS_WRAPPER
from google_drive.tests.test_const import RESULT_SUCCESS, RESULT_ERROR, RESULT_MISSING_EVENT_TYPE
Expand All @@ -40,8 +42,28 @@
'displayed_in': 'iframe'
}

CALENDAR_LANGUAGES = [
{
'override': False,
'activate_lang': None,
'expected_lang': 'en-us',
}, {
'override': True,
'activate_lang': 'en',
'expected_lang': 'en',
}, {
'override': True,
'activate_lang': 'eo',
'expected_lang': 'eo',
}, {
'override': True,
'activate_lang': 'jp-ja',
'expected_lang': 'jp-ja',
}
]

# Classes ###########################################################
@ddt.ddt
class TestGoogleCalendarBlock(unittest.TestCase):
""" Tests for GoogleCalendarBlock """

Expand All @@ -54,18 +76,33 @@ def make_calendar_block(cls):
ids = generate_scope_ids(runtime, 'google_calendar')
return GoogleCalendarBlock(runtime, db_model, scope_ids=ids)

def test_calendar_template_content(self): # pylint: disable=no-self-use
""" Test content of GoogleCalendarBlock's rendered views """
def _render_calendar_block(self): # pylint: disable=no-self-use
block = TestGoogleCalendarBlock.make_calendar_block()
block.usage_id = Mock()

student_fragment = block.render('student_view', Mock())
studio_fragment = block.render('studio_view', Mock())
return block, student_fragment, studio_fragment

@ddt.data(*CALENDAR_LANGUAGES)
@ddt.unpack
def test_calendar_template_content(self, override, activate_lang, expected_lang):
""" Test content of GoogleCalendarBlock's rendered views """
# pylint: disable=no-value-for-parameter
if override:
with override_language(activate_lang):
block, student_fragment, studio_fragment = self._render_calendar_block()
else:
block, student_fragment, studio_fragment = self._render_calendar_block()

src_url = 'https://www.google.com/calendar/embed?mode=Month&src={id}&showCalendars=0&hl={lang}'.format(
id=DEFAULT_CALENDAR_ID,
lang=expected_lang,
)

assert_in('<div class="google-calendar-xblock-wrapper">', student_fragment.content)
assert_in(cgi.escape(DEFAULT_CALENDAR_URL), student_fragment.content)
assert_in(cgi.escape(src_url), student_fragment.content)
assert_in('Google Calendar', student_fragment.content)

studio_fragment = block.render('studio_view', Mock())
assert_in(STUDIO_EDIT_WRAPPER, studio_fragment.content)
assert_in(VALIDATION_WRAPPER, studio_fragment.content)
assert_in(USER_INPUTS_WRAPPER, studio_fragment.content)
Expand Down
20 changes: 20 additions & 0 deletions google_drive/translations/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Configuration for i18n workflow.

locales:
- en # English - Source Language
- eo # Esperanto
- ar # Arabic
- es_419 # Spanish (Latin America)
- ja_JP # Japanese
- fr # French
- he # Hebrew
- hi # Hindi
- ko_KR # Korean (Korea)
- pt_BR # Portuguese (Brazil)
- ru # Russian
- zh_CN # Chinese (China)

# The locales used for fake-accented English, for testing.
dummy_locales:
- eo

Binary file added google_drive/translations/en/LC_MESSAGES/text.mo
Binary file not shown.
Loading

0 comments on commit db709c5

Please sign in to comment.