Commit c0278d0f by Steve Strassmann

refactor config file; fix duplicate merge

parent 03b9a9e2
{"locales" : ["en"]} {
"locales" : ["en"],
"dummy-locale" : "fr"
}
import os, json
# BASE_DIR is the working directory to execute django-admin commands from.
# Typically this should be the 'mitx' directory.
BASE_DIR = os.path.normpath(os.path.dirname(os.path.abspath(__file__))+'/..')
# LOCALE_DIR contains the locale files.
# Typically this should be 'mitx/conf/locale'
LOCALE_DIR = os.path.join(BASE_DIR, 'conf', 'locale')
class Configuration:
"""
# Reads localization configuration in json format
"""
_source_locale = 'en'
def __init__(self, filename):
self.filename = filename
self.config = self.get_config(self.filename)
def get_config(self, filename):
"""
Returns data found in config file (as dict), or raises exception if file not found
"""
if not os.path.exists(filename):
raise Exception("Configuration file cannot be found: %s" % filename)
with open(filename) as stream:
return json.load(stream)
def get_locales(self):
"""
Returns a list of locales declared in the configuration file,
e.g. ['en', 'fr', 'es']
Each locale is a string.
"""
return self.config['locales']
def get_source_locale(self):
"""
Returns source language.
Source language is English.
"""
return self._source_locale
def get_dummy_locale(self):
"""
Returns a locale to use for the dummy text, e.g. 'fr'.
Throws exception if no dummy-locale is declared.
The locale is a string.
"""
dummy = self.config.get('dummy-locale', None)
if not dummy:
raise Exception('Could not read dummy-locale from configuration file.')
return dummy
def get_messages_dir(self, locale):
"""
Returns the name of the directory holding the po files for locale.
Example: mitx/conf/locale/fr/LC_MESSAGES
"""
return os.path.join(LOCALE_DIR, locale, 'LC_MESSAGES')
def get_source_messages_dir(self):
"""
Returns the name of the directory holding the source-language po files (English).
Example: mitx/conf/locale/en/LC_MESSAGES
"""
return self.get_messages_dir(self.get_source_locale())
CONFIGURATION = Configuration(os.path.normpath(os.path.join(LOCALE_DIR, 'config')))
import os, subprocess, logging, json import os, subprocess, logging
from config import CONFIGURATION, BASE_DIR
def init_module():
"""
Initializes module parameters
"""
global BASE_DIR, LOCALE_DIR, CONFIG_FILENAME, SOURCE_MSGS_DIR, SOURCE_LOCALE, LOG
# BASE_DIR is the working directory to execute django-admin commands from.
# Typically this should be the 'mitx' directory.
BASE_DIR = os.path.normpath(os.path.dirname(os.path.abspath(__file__))+'/..')
# Source language is English
SOURCE_LOCALE = 'en'
# LOCALE_DIR contains the locale files.
# Typically this should be 'mitx/conf/locale'
LOCALE_DIR = BASE_DIR + '/conf/locale'
# CONFIG_FILENAME contains localization configuration in json format
CONFIG_FILENAME = LOCALE_DIR + '/config'
# SOURCE_MSGS_DIR contains the English po files.
SOURCE_MSGS_DIR = messages_dir(SOURCE_LOCALE)
# Default logger.
LOG = get_logger()
def messages_dir(locale):
"""
Returns the name of the directory holding the po files for locale.
Example: mitx/conf/locale/en/LC_MESSAGES
"""
return os.path.join(LOCALE_DIR, locale, 'LC_MESSAGES')
def get_logger(): def get_default_logger():
"""Returns a default logger""" """Returns a default logger"""
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
log.setLevel(logging.INFO) log.setLevel(logging.INFO)
...@@ -43,8 +11,8 @@ def get_logger(): ...@@ -43,8 +11,8 @@ def get_logger():
log.addHandler(log_handler) log.addHandler(log_handler)
return log return log
# Run this after defining messages_dir and get_logger, because it depends on these. LOG = get_default_logger()
init_module()
def execute (command, working_directory=BASE_DIR, log=LOG): def execute (command, working_directory=BASE_DIR, log=LOG):
""" """
...@@ -69,17 +37,6 @@ def call(command, working_directory=BASE_DIR, log=LOG): ...@@ -69,17 +37,6 @@ def call(command, working_directory=BASE_DIR, log=LOG):
out, err = p.communicate() out, err = p.communicate()
return (out, err) return (out, err)
def get_config():
"""Returns data found in config file, or returns None if file not found"""
config_path = os.path.normpath(CONFIG_FILENAME)
if not os.path.exists(config_path):
log.warn("Configuration file cannot be found: %s" % \
os.path.relpath(config_path, BASE_DIR))
return None
with open(config_path) as stream:
return json.load(stream)
def create_dir_if_necessary(pathname): def create_dir_if_necessary(pathname):
dirname = os.path.dirname(pathname) dirname = os.path.dirname(pathname)
if not os.path.exists(dirname): if not os.path.exists(dirname):
...@@ -98,4 +55,3 @@ def remove_file(filename, log=LOG, verbose=True): ...@@ -98,4 +55,3 @@ def remove_file(filename, log=LOG, verbose=True):
log.warn("File does not exist: %s" % os.path.relpath(filename, BASE_DIR)) log.warn("File does not exist: %s" % os.path.relpath(filename, BASE_DIR))
else: else:
os.remove(filename) os.remove(filename)
...@@ -18,9 +18,8 @@ See https://edx-wiki.atlassian.net/wiki/display/ENG/PO+File+workflow ...@@ -18,9 +18,8 @@ See https://edx-wiki.atlassian.net/wiki/display/ENG/PO+File+workflow
import os import os
from datetime import datetime from datetime import datetime
from polib import pofile from polib import pofile
from execute import execute, create_dir_if_necessary, remove_file, \ from config import BASE_DIR, LOCALE_DIR, CONFIGURATION
BASE_DIR, LOCALE_DIR, SOURCE_MSGS_DIR, LOG from execute import execute, create_dir_if_necessary, remove_file, LOG
# BABEL_CONFIG contains declarations for Babel to extract strings from mako template files # BABEL_CONFIG contains declarations for Babel to extract strings from mako template files
# Use relpath to reduce noise in logs # Use relpath to reduce noise in logs
...@@ -28,15 +27,19 @@ BABEL_CONFIG = os.path.relpath(LOCALE_DIR + '/babel.cfg', BASE_DIR) ...@@ -28,15 +27,19 @@ BABEL_CONFIG = os.path.relpath(LOCALE_DIR + '/babel.cfg', BASE_DIR)
# Strings from mako template files are written to BABEL_OUT # Strings from mako template files are written to BABEL_OUT
# Use relpath to reduce noise in logs # Use relpath to reduce noise in logs
BABEL_OUT = os.path.relpath(SOURCE_MSGS_DIR + '/mako.po', BASE_DIR) BABEL_OUT = os.path.relpath(CONFIGURATION.get_source_messages_dir() + '/mako.po', BASE_DIR)
SOURCE_WARN = 'This English source file is machine-generated. Do not check it into github'
def main (): def main ():
create_dir_if_necessary(LOCALE_DIR) create_dir_if_necessary(LOCALE_DIR)
generated_files = ('django-partial.po', 'djangojs.po', 'mako.po') source_msgs_dir = CONFIGURATION.get_source_messages_dir()
remove_file(os.path.join(source_msgs_dir, 'django.po'))
generated_files = ('django-partial.po', 'djangojs.po', 'mako.po')
for filename in generated_files: for filename in generated_files:
remove_file(os.path.join(SOURCE_MSGS_DIR, filename)) remove_file(os.path.join(source_msgs_dir, filename))
# Extract strings from mako templates # Extract strings from mako templates
babel_mako_cmd = 'pybabel extract -F %s -c "TRANSLATORS:" . -o %s' % (BABEL_CONFIG, BABEL_OUT) babel_mako_cmd = 'pybabel extract -F %s -c "TRANSLATORS:" . -o %s' % (BABEL_CONFIG, BABEL_OUT)
...@@ -52,13 +55,13 @@ def main (): ...@@ -52,13 +55,13 @@ def main ():
execute(make_django_cmd, working_directory=BASE_DIR) execute(make_django_cmd, working_directory=BASE_DIR)
# makemessages creates 'django.po'. This filename is hardcoded. # makemessages creates 'django.po'. This filename is hardcoded.
# Rename it to django-partial.po to enable merging into django.po later. # Rename it to django-partial.po to enable merging into django.po later.
os.rename(os.path.join(SOURCE_MSGS_DIR, 'django.po'), os.rename(os.path.join(source_msgs_dir, 'django.po'),
os.path.join(SOURCE_MSGS_DIR, 'django-partial.po')) os.path.join(source_msgs_dir, 'django-partial.po'))
execute(make_djangojs_cmd, working_directory=BASE_DIR) execute(make_djangojs_cmd, working_directory=BASE_DIR)
for filename in generated_files: for filename in generated_files:
LOG.info('Cleaning %s' % filename) LOG.info('Cleaning %s' % filename)
po = pofile(os.path.join(SOURCE_MSGS_DIR, filename)) po = pofile(os.path.join(source_msgs_dir, filename))
# replace default headers with edX headers # replace default headers with edX headers
fix_header(po) fix_header(po)
# replace default metadata with edX metadata # replace default metadata with edX metadata
...@@ -82,8 +85,8 @@ def fix_header(po): ...@@ -82,8 +85,8 @@ def fix_header(po):
po.metadata_is_fuzzy = [] # remove [u'fuzzy'] po.metadata_is_fuzzy = [] # remove [u'fuzzy']
header = po.header header = po.header
fixes = ( fixes = (
('SOME DESCRIPTIVE TITLE', 'edX translation file'), ('SOME DESCRIPTIVE TITLE', 'edX translation file\n' + SOURCE_WARN),
('Translations template for PROJECT.', 'edX translation file'), ('Translations template for PROJECT.', 'edX translation file\n' + SOURCE_WARN),
('YEAR', '%s' % datetime.utcnow().year), ('YEAR', '%s' % datetime.utcnow().year),
('ORGANIZATION', 'edX'), ('ORGANIZATION', 'edX'),
("THE PACKAGE'S COPYRIGHT HOLDER", "EdX"), ("THE PACKAGE'S COPYRIGHT HOLDER", "EdX"),
......
...@@ -16,15 +16,15 @@ ...@@ -16,15 +16,15 @@
import os import os
from polib import pofile from polib import pofile
from execute import execute, get_config, messages_dir, remove_file, \ from config import BASE_DIR, CONFIGURATION
BASE_DIR, LOG, SOURCE_LOCALE from execute import execute, remove_file, LOG
def merge(locale, target='django.po'): def merge(locale, target='django.po'):
""" """
For the given locale, merge django-partial.po, messages.po, mako.po -> django.po For the given locale, merge django-partial.po, messages.po, mako.po -> django.po
""" """
LOG.info('Merging locale={0}'.format(locale)) LOG.info('Merging locale={0}'.format(locale))
locale_directory = messages_dir(locale) locale_directory = CONFIGURATION.get_messages_dir(locale)
files_to_merge = ('django-partial.po', 'messages.po', 'mako.po') files_to_merge = ('django-partial.po', 'messages.po', 'mako.po')
validate_files(locale_directory, files_to_merge) validate_files(locale_directory, files_to_merge)
...@@ -62,15 +62,8 @@ def validate_files(dir, files_to_merge): ...@@ -62,15 +62,8 @@ def validate_files(dir, files_to_merge):
raise Exception("File not found: {0}".format(pathname)) raise Exception("File not found: {0}".format(pathname))
def main (): def main ():
configuration = get_config() for locale in CONFIGURATION.get_locales():
if configuration == None:
LOG.warn('Configuration file not found, using only English.')
locales = (SOURCE_LOCALE,)
else:
locales = configuration['locales']
for locale in locales:
merge(locale) merge(locale)
compile_cmd = 'django-admin.py compilemessages' compile_cmd = 'django-admin.py compilemessages'
execute(compile_cmd, working_directory=BASE_DIR) execute(compile_cmd, working_directory=BASE_DIR)
......
from test_config import TestConfiguration
from test_extract import TestExtract from test_extract import TestExtract
from test_generate import TestGenerate from test_generate import TestGenerate
from test_converter import TestConverter from test_converter import TestConverter
......
import os
from unittest import TestCase
from config import Configuration, LOCALE_DIR, CONFIGURATION
class TestConfiguration(TestCase):
"""
Tests functionality of i18n/config.py
"""
def test_config(self):
config_filename = os.path.normpath(os.path.join(LOCALE_DIR, 'config'))
config = Configuration(config_filename)
self.assertEqual(config.get_source_locale(), 'en')
def test_no_config(self):
config_filename = os.path.normpath(os.path.join(LOCALE_DIR, 'no_such_file'))
with self.assertRaises(Exception):
Configuration(config_filename)
def test_valid_configuration(self):
"""
Make sure we have a valid configuration file,
and that it contains an 'en' locale.
Also check values of dummy_locale and source_locale.
"""
self.assertIsNotNone(CONFIGURATION)
locales = CONFIGURATION.get_locales()
self.assertIsNotNone(locales)
self.assertIsInstance(locales, list)
self.assertIn('en', locales)
self.assertEqual('fr', CONFIGURATION.get_dummy_locale())
self.assertEqual('en', CONFIGURATION.get_source_locale())
...@@ -4,7 +4,7 @@ from nose.plugins.skip import SkipTest ...@@ -4,7 +4,7 @@ from nose.plugins.skip import SkipTest
from datetime import datetime, timedelta from datetime import datetime, timedelta
import extract import extract
from execute import SOURCE_MSGS_DIR from config import CONFIGURATION
# Make sure setup runs only once # Make sure setup runs only once
SETUP_HAS_RUN = False SETUP_HAS_RUN = False
...@@ -39,7 +39,7 @@ class TestExtract(TestCase): ...@@ -39,7 +39,7 @@ class TestExtract(TestCase):
Fails assertion if one of the files doesn't exist. Fails assertion if one of the files doesn't exist.
""" """
for filename in self.generated_files: for filename in self.generated_files:
path = os.path.join(SOURCE_MSGS_DIR, filename) path = os.path.join(CONFIGURATION.get_source_messages_dir(), filename)
exists = os.path.exists(path) exists = os.path.exists(path)
self.assertTrue(exists, msg='Missing file: %s' % filename) self.assertTrue(exists, msg='Missing file: %s' % filename)
if exists: if exists:
......
import os, string, random import os, string, random, re
from polib import pofile
from unittest import TestCase from unittest import TestCase
from datetime import datetime, timedelta from datetime import datetime, timedelta
import generate import generate
from execute import get_config, messages_dir, SOURCE_MSGS_DIR, SOURCE_LOCALE from config import CONFIGURATION
class TestGenerate(TestCase): class TestGenerate(TestCase):
""" """
...@@ -12,29 +13,16 @@ class TestGenerate(TestCase): ...@@ -12,29 +13,16 @@ class TestGenerate(TestCase):
generated_files = ('django-partial.po', 'djangojs.po', 'mako.po') generated_files = ('django-partial.po', 'djangojs.po', 'mako.po')
def setUp(self): def setUp(self):
self.configuration = get_config()
# Subtract 1 second to help comparisons with file-modify time succeed, # Subtract 1 second to help comparisons with file-modify time succeed,
# since os.path.getmtime() is not millisecond-accurate # since os.path.getmtime() is not millisecond-accurate
self.start_time = datetime.now() - timedelta(seconds=1) 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): def test_merge(self):
""" """
Tests merge script on English source files. Tests merge script on English source files.
""" """
filename = os.path.join(SOURCE_MSGS_DIR, random_name()) filename = os.path.join(CONFIGURATION.get_source_messages_dir(), random_name())
generate.merge(SOURCE_LOCALE, target=filename) generate.merge(CONFIGURATION.get_source_locale(), target=filename)
self.assertTrue(os.path.exists(filename)) self.assertTrue(os.path.exists(filename))
os.remove(filename) os.remove(filename)
...@@ -47,13 +35,35 @@ class TestGenerate(TestCase): ...@@ -47,13 +35,35 @@ class TestGenerate(TestCase):
after start of test suite) after start of test suite)
""" """
generate.main() generate.main()
for locale in self.configuration['locales']: for locale in CONFIGURATION.get_locales():
for filename in ('django.mo', 'djangojs.mo'): for filename in ('django', 'djangojs'):
path = os.path.join(messages_dir(locale), filename) mofile = filename+'.mo'
path = os.path.join(CONFIGURATION.get_messages_dir(locale), mofile)
exists = os.path.exists(path) exists = os.path.exists(path)
self.assertTrue(exists, msg='Missing file in locale %s: %s' % (locale, filename)) self.assertTrue(exists, msg='Missing file in locale %s: %s' % (locale, mofile))
self.assertTrue(datetime.fromtimestamp(os.path.getmtime(path)) >= self.start_time, self.assertTrue(datetime.fromtimestamp(os.path.getmtime(path)) >= self.start_time,
msg='File not recently modified: %s' % path) msg='File not recently modified: %s' % path)
self.assert_merge_headers(locale)
def assert_merge_headers(self, locale):
"""
This is invoked by test_main to ensure that it runs after
calling generate.main().
There should be exactly three merge comment headers
in our merged .po file. This counts them to be sure.
A merge comment looks like this:
# #-#-#-#-# django-partial.po (0.1a) #-#-#-#-#
"""
path = os.path.join(CONFIGURATION.get_messages_dir(locale), 'django.po')
po = pofile(path)
pattern = re.compile('^#-#-#-#-#', re.M)
match = pattern.findall(po.header)
self.assertEqual(len(match), 3,
msg="Found %s (should be 3) merge comments in the header for %s" % \
(len(match), path))
def random_name(size=6): def random_name(size=6):
"""Returns random filename as string, like test-4BZ81W""" """Returns random filename as string, like test-4BZ81W"""
......
...@@ -2,7 +2,8 @@ import os ...@@ -2,7 +2,8 @@ import os
from unittest import TestCase from unittest import TestCase
from nose.plugins.skip import SkipTest from nose.plugins.skip import SkipTest
from execute import call, LOCALE_DIR, LOG from config import LOCALE_DIR
from execute import call, LOG
class TestValidate(TestCase): class TestValidate(TestCase):
""" """
......
#!/usr/bin/python
import sys
from execute import execute
def push():
execute('tx push -s')
def pull():
execute('tx pull')
if __name__ == '__main__':
if len(sys.argv)<2:
raise Exception("missing argument: push or pull")
arg = sys.argv[1]
if arg == 'push':
push()
elif arg == 'pull':
pull()
else:
raise Exception("unknown argument: (%s)" % arg)
...@@ -551,14 +551,16 @@ namespace :i18n do ...@@ -551,14 +551,16 @@ namespace :i18n do
desc "Push source strings to Transifex for translation" desc "Push source strings to Transifex for translation"
task :push do task :push do
if validate_transifex_config() if validate_transifex_config()
sh("tx push -s") cmd = File.join(REPO_ROOT, "i18n", "transifex.py")
sh("#{cmd} push")
end end
end end
desc "Pull translated strings from Transifex" desc "Pull translated strings from Transifex"
task :pull do task :pull do
if validate_transifex_config() if validate_transifex_config()
sh("tx pull") cmd = File.join(REPO_ROOT, "i18n", "transifex.py")
sh("#{cmd} pull")
end end
end end
end end
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment