diff --git a/i18n/tests/__init__.py b/i18n/tests/__init__.py
new file mode 100644
index 0000000000..d60515c712
--- /dev/null
+++ b/i18n/tests/__init__.py
@@ -0,0 +1,4 @@
+from test_extract import TestExtract
+from test_generate import TestGenerate
+from test_converter import TestConverter
+from test_dummy import TestDummy
diff --git a/i18n/tests/test_converter.py b/i18n/tests/test_converter.py
new file mode 100644
index 0000000000..4dd5f02e3f
--- /dev/null
+++ b/i18n/tests/test_converter.py
@@ -0,0 +1,42 @@
+import os
+from unittest import TestCase
+
+import converter
+
+class UpcaseConverter (converter.Converter):
+ """
+ Converts a string to uppercase. Just used for testing.
+ """
+ def inner_convert_string(self, string):
+ return string.upper()
+
+
+class TestConverter(TestCase):
+ """
+ Tests functionality of i18n/converter.py
+ """
+
+ def test_converter(self):
+ """
+ Tests with a simple converter (converts strings to uppercase).
+ Assert that embedded HTML and python tags are not converted.
+ """
+ c = UpcaseConverter()
+ test_cases = (
+ # no tags
+ ('big bad wolf', 'BIG BAD WOLF'),
+ # one html tag
+ ('big bad wolf', 'BIG BAD WOLF'),
+ # two html tags
+ ('big bad wolf', 'BIG BAD WOLF'),
+ # one python tag
+ ('big %(adjective)s wolf', 'BIG %(adjective)s WOLF'),
+ # two python tags
+ ('big %(adjective)s %(noun)s', 'BIG %(adjective)s %(noun)s'),
+ # both kinds of tags
+ ('big %(adjective)s %(noun)s',
+ 'BIG %(adjective)s %(noun)s'),
+ )
+ for (source, expected) in test_cases:
+ result = c.convert(source)
+ self.assertEquals(result, expected)
diff --git a/i18n/tests/test_dummy.py b/i18n/tests/test_dummy.py
new file mode 100644
index 0000000000..88addb5a95
--- /dev/null
+++ b/i18n/tests/test_dummy.py
@@ -0,0 +1,50 @@
+import os, string, random
+from unittest import TestCase
+from polib import POEntry
+
+import dummy
+
+
+class TestDummy(TestCase):
+ """
+ Tests functionality of i18n/dummy.py
+ """
+
+ def setUp(self):
+ self.converter = dummy.Dummy()
+
+ def test_dummy(self):
+ """
+ Tests with a dummy converter (adds spurious accents to strings).
+ Assert that embedded HTML and python tags are not converted.
+ """
+ test_cases = (("hello my name is Bond, James Bond",
+ u'h\xe9ll\xf6 my n\xe4m\xe9 \xefs B\xf6nd, J\xe4m\xe9s B\xf6nd Lorem i#'),
+
+ ('don\'t convert tag ids',
+ u'd\xf6n\'t \xe7\xf6nv\xe9rt t\xe4g \xefds Lorem ipsu#'),
+
+ ('don\'t convert %(name)s tags on %(date)s',
+ u"d\xf6n't \xe7\xf6nv\xe9rt %(name)s t\xe4gs \xf6n %(date)s Lorem ips#")
+ )
+ for (source, expected) in test_cases:
+ result = self.converter.convert(source)
+ self.assertEquals(result, expected)
+
+ def test_singular(self):
+ entry = POEntry()
+ entry.msgid = 'A lovely day for a cup of tea.'
+ expected = u'\xc0 l\xf6v\xe9ly d\xe4y f\xf6r \xe4 \xe7\xfcp \xf6f t\xe9\xe4. Lorem i#'
+ self.converter.convert_msg(entry)
+ self.assertEquals(entry.msgstr, expected)
+
+ def test_plural(self):
+ entry = POEntry()
+ entry.msgid = 'A lovely day for a cup of tea.'
+ entry.msgid_plural = 'A lovely day for some cups of tea.'
+ expected_s = u'\xc0 l\xf6v\xe9ly d\xe4y f\xf6r \xe4 \xe7\xfcp \xf6f t\xe9\xe4. Lorem i#'
+ expected_p = u'\xc0 l\xf6v\xe9ly d\xe4y f\xf6r s\xf6m\xe9 \xe7\xfcps \xf6f t\xe9\xe4. Lorem ip#'
+ self.converter.convert_msg(entry)
+ result = entry.msgstr_plural
+ self.assertEquals(result['0'], expected_s)
+ self.assertEquals(result['1'], expected_p)
diff --git a/i18n/tests/test_extract.py b/i18n/tests/test_extract.py
new file mode 100644
index 0000000000..b14ae9872d
--- /dev/null
+++ b/i18n/tests/test_extract.py
@@ -0,0 +1,85 @@
+import os, polib
+from unittest import TestCase
+from nose.plugins.skip import SkipTest
+from datetime import datetime, timedelta
+
+import extract
+from execute import SOURCE_MSGS_DIR
+
+# Make sure setup runs only once
+SETUP_HAS_RUN = False
+
+class TestExtract(TestCase):
+ """
+ Tests functionality of i18n/extract.py
+ """
+ generated_files = ('django-partial.po', 'djangojs.po', 'mako.po')
+
+ def setUp(self):
+ # Skip this test because it takes too long (>1 minute)
+ # TODO: figure out how to declare a "long-running" test suite
+ # and add this test to it.
+ raise SkipTest()
+
+ global SETUP_HAS_RUN
+
+ # Subtract 1 second to help comparisons with file-modify time succeed,
+ # since os.path.getmtime() is not millisecond-accurate
+ self.start_time = datetime.now() - timedelta(seconds=1)
+ super(TestExtract, self).setUp()
+ if not SETUP_HAS_RUN:
+ # Run extraction script. Warning, this takes 1 minute or more
+ extract.main()
+ SETUP_HAS_RUN = True
+
+ def get_files (self):
+ """
+ This is a generator.
+ Returns the fully expanded filenames for all extracted files
+ Fails assertion if one of the files doesn't exist.
+ """
+ for filename in self.generated_files:
+ path = os.path.join(SOURCE_MSGS_DIR, filename)
+ exists = os.path.exists(path)
+ self.assertTrue(exists, msg='Missing file: %s' % filename)
+ if exists:
+ yield path
+
+ def test_files(self):
+ """
+ Asserts that each auto-generated file has been modified since 'extract' was launched.
+ Intended to show that the file has been touched by 'extract'.
+ """
+
+ for path in self.get_files():
+ self.assertTrue(datetime.fromtimestamp(os.path.getmtime(path)) > self.start_time,
+ msg='File not recently modified: %s' % os.path.basename(path))
+
+ def test_is_keystring(self):
+ """
+ Verifies is_keystring predicate
+ """
+ entry1 = polib.POEntry()
+ entry2 = polib.POEntry()
+ entry1.msgid = "_.lms.admin.warning.keystring"
+ entry2.msgid = "This is not a keystring"
+ self.assertTrue(extract.is_key_string(entry1.msgid))
+ self.assertFalse(extract.is_key_string(entry2.msgid))
+
+ def test_headers(self):
+ """Verify all headers have been modified"""
+ for path in self.get_files():
+ po = polib.pofile(path)
+ header = po.header
+ self.assertEqual(header.find('edX translation file'), 0,
+ msg='Missing header in %s:\n"%s"' % \
+ (os.path.basename(path), header))
+
+ def test_metadata(self):
+ """Verify all metadata has been modified"""
+ for path in self.get_files():
+ po = polib.pofile(path)
+ metadata = po.metadata
+ value = metadata['Report-Msgid-Bugs-To']
+ expected = 'translation_team@edx.org'
+ self.assertEquals(expected, value)
diff --git a/i18n/tests/test_generate.py b/i18n/tests/test_generate.py
new file mode 100644
index 0000000000..fc22988251
--- /dev/null
+++ b/i18n/tests/test_generate.py
@@ -0,0 +1,61 @@
+import os, string, random
+from unittest import TestCase
+from datetime import datetime, timedelta
+
+import generate
+from execute import get_config, messages_dir, SOURCE_MSGS_DIR, SOURCE_LOCALE
+
+class TestGenerate(TestCase):
+ """
+ Tests functionality of i18n/generate.py
+ """
+ generated_files = ('django-partial.po', 'djangojs.po', 'mako.po')
+
+ def setUp(self):
+ self.configuration = get_config()
+
+ # Subtract 1 second to help comparisons with file-modify time succeed,
+ # since os.path.getmtime() is not millisecond-accurate
+ self.start_time = datetime.now() - timedelta(seconds=1)
+
+ def test_configuration(self):
+ """
+ Make sure we have a valid configuration file,
+ and that it contains an 'en' locale.
+ """
+ self.assertIsNotNone(self.configuration)
+ locales = self.configuration['locales']
+ self.assertIsNotNone(locales)
+ self.assertIsInstance(locales, list)
+ self.assertIn('en', locales)
+
+ def test_merge(self):
+ """
+ Tests merge script on English source files.
+ """
+ filename = os.path.join(SOURCE_MSGS_DIR, random_name())
+ generate.merge(SOURCE_LOCALE, target=filename)
+ self.assertTrue(os.path.exists(filename))
+ os.remove(filename)
+
+ def test_main(self):
+ """
+ Runs generate.main() which should merge source files,
+ then compile all sources in all configured languages.
+ Validates output by checking all .mo files in all configured languages.
+ .mo files should exist, and be recently created (modified
+ after start of test suite)
+ """
+ generate.main()
+ for locale in self.configuration['locales']:
+ for filename in ('django.mo', 'djangojs.mo'):
+ path = os.path.join(messages_dir(locale), filename)
+ exists = os.path.exists(path)
+ self.assertTrue(exists, msg='Missing file in locale %s: %s' % (locale, filename))
+ self.assertTrue(datetime.fromtimestamp(os.path.getmtime(path)) >= self.start_time,
+ msg='File not recently modified: %s' % path)
+
+def random_name(size=6):
+ """Returns random filename as string, like test-4BZ81W"""
+ chars = string.ascii_uppercase + string.digits
+ return 'test-' + ''.join(random.choice(chars) for x in range(size))