diff --git a/babel.cfg b/babel.cfg new file mode 100644 index 0000000..b6f5945 --- /dev/null +++ b/babel.cfg @@ -0,0 +1,6 @@ +[python: */**.py] +[jinja2: */**.jinja2] +extensions=jinja2.ext.autoescape, jinja2.ext.with_ + +[javascript: */**.js] +extract_messages = gettext, ngettext diff --git a/setup.py b/setup.py index 5b88ec6..9232222 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,6 @@ # coding=utf-8 -import setuptools +from setuptools import setup, Command +import os ######################################################################################################################## @@ -40,6 +41,28 @@ plugin_additional_data = [] ######################################################################################################################## +# I18N setup +I18N_MAPPING_FILE = "babel.cfg" +I18N_DOMAIN = "messages" +I18N_INPUT_DIRS = "." +I18N_OUTPUT_DIR_PY = os.path.join(plugin_package, "translations") +I18N_OUTPUT_DIR_JS = os.path.join(plugin_package, "static", "js", "i18n") +I18N_POT_FILE = os.path.join(I18N_OUTPUT_DIR_PY, "messages.pot") + +# Requirements for out application +INSTALL_REQUIRES = [ + "OctoPrint" +] + +# Requirements for developing etc +EXTRA_REQUIRES = dict( + develop=[ + # Translation dependencies + "babel", + "po2json" + ] +) + def package_data_dirs(source, sub_folders): import os dirs = [] @@ -56,9 +79,208 @@ def package_data_dirs(source, sub_folders): return dirs +def _recursively_handle_files(directory, file_matcher, folder_handler=None, file_handler=None): + applied_handler = False + + for filename in os.listdir(directory): + path = os.path.join(directory, filename) + + if file_handler is not None and file_matcher(filename): + file_handler(path) + applied_handler = True + + elif os.path.isdir(path): + sub_applied_handler = _recursively_handle_files(path, file_matcher, folder_handler=folder_handler, file_handler=file_handler) + if sub_applied_handler: + applied_handler = True + + if folder_handler is not None: + folder_handler(path, sub_applied_handler) + + return applied_handler + +class CleanCommand(Command): + description = "clean build artifacts" + user_options = [] + boolean_options = [] + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + import shutil + import glob + + # build folder + if os.path.exists('build'): + print "Deleting build directory" + shutil.rmtree('build') + + # eggs + eggs = glob.glob("*.egg-info") + for egg in eggs: + print "Deleting %s directory" % egg + shutil.rmtree(egg) + + # pyc files + def delete_folder_if_empty(path, applied_handler): + if not applied_handler: + return + if len(os.listdir(path)) == 0: + shutil.rmtree(path) + print "Deleted %s since it was empty" % path + + def delete_file(path): + os.remove(path) + print "Deleted %s" % path + + import fnmatch + _recursively_handle_files( + os.path.abspath(plugin_package), + lambda name: fnmatch.fnmatch(name.lower(), "*.pyc"), + folder_handler=delete_folder_if_empty, + file_handler=delete_file + ) + + # pyc files + def delete_folder_if_empty(path, applied_handler): + if not applied_handler: + return + if len(os.listdir(path)) == 0: + shutil.rmtree(path) + print "Deleted %s since it was empty" % path + + def delete_file(path): + os.remove(path) + print "Deleted %s" % path + + import fnmatch + _recursively_handle_files( + os.path.abspath(plugin_package), + lambda name: fnmatch.fnmatch(name.lower(), "*.pyc"), + folder_handler=delete_folder_if_empty, + file_handler=delete_file + ) + +class NewTranslation(Command): + description = "create a new translation" + user_options = [ + ('locale=', 'l', 'locale for the new translation'), + ] + boolean_options = [] + + def __init__(self, dist, **kw): + from babel.messages import frontend as babel + self.babel_init_messages = babel.init_catalog(dist) + Command.__init__(self, dist, **kw) + + def initialize_options(self): + self.locale = None + self.babel_init_messages.initialize_options() + + def finalize_options(self): + self.babel_init_messages.locale = self.locale + self.babel_init_messages.input_file = I18N_POT_FILE + self.babel_init_messages.output_dir = I18N_OUTPUT_DIR_PY + self.babel_init_messages.finalize_options() + + def run(self): + self.babel_init_messages.run() + +class ExtractTranslation(Command): + description = "extract translations" + user_options = [] + boolean_options = [] + + def __init__(self, dist, **kw): + from babel.messages import frontend as babel + self.babel_extract_messages = babel.extract_messages(dist) + Command.__init__(self, dist, **kw) + + def initialize_options(self): + self.babel_extract_messages.initialize_options() + + def finalize_options(self): + self.babel_extract_messages.mapping_file = I18N_MAPPING_FILE + self.babel_extract_messages.output_file = I18N_POT_FILE + self.babel_extract_messages.input_dirs = I18N_INPUT_DIRS + self.babel_extract_messages.msgid_bugs_address = plugin_author_email + self.babel_extract_messages.copyright_holder = plugin_author + self.babel_extract_messages.finalize_options() + + def run(self): + self.babel_extract_messages.run() + +class RefreshTranslation(Command): + description = "refresh translations" + user_options = [ + ('locale=', 'l', 'locale for the translation to refresh'), + ] + boolean_options = [] + + def __init__(self, dist, **kw): + from babel.messages import frontend as babel + self.babel_extract_messages = babel.extract_messages(dist) + self.babel_update_messages = babel.update_catalog(dist) + Command.__init__(self, dist, **kw) + + def initialize_options(self): + self.locale = None + self.babel_extract_messages.initialize_options() + self.babel_update_messages.initialize_options() + + def finalize_options(self): + self.babel_extract_messages.mapping_file = I18N_MAPPING_FILE + self.babel_extract_messages.output_file = I18N_POT_FILE + self.babel_extract_messages.input_dirs = I18N_INPUT_DIRS + self.babel_extract_messages.msgid_bugs_address = plugin_author_email + self.babel_extract_messages.copyright_holder = plugin_author + self.babel_extract_messages.finalize_options() + + self.babel_update_messages.input_file = I18N_POT_FILE + self.babel_update_messages.output_dir = I18N_OUTPUT_DIR_PY + self.babel_update_messages.locale = self.locale + + def run(self): + self.babel_extract_messages.run() + self.babel_update_messages.run() + +class CompileTranslation(Command): + description = "compile translations" + user_options = [] + boolean_options = [] + + def __init__(self, dist, **kw): + from babel.messages import frontend as babel + self.babel_compile_messages = babel.compile_catalog(dist) + Command.__init__(self, dist, **kw) + + def initialize_options(self): + self.babel_compile_messages.initialize_options() + + def finalize_options(self): + self.babel_compile_messages.directory = I18N_OUTPUT_DIR_PY + + def run(self): + self.babel_compile_messages.run() + + import po2json + + for lang_code in os.listdir(I18N_OUTPUT_DIR_PY): + full_path = os.path.join(I18N_OUTPUT_DIR_PY, lang_code) + + if os.path.isdir(full_path): + client_po_dir = os.path.join(full_path, "LC_MESSAGES") -def requirements(filename): - return filter(lambda line: line and not line.startswith("#"), map(lambda line: line.strip(), open(filename).read().split("\n"))) + po2json.update_js_file( + "%s/%s.po" % (client_po_dir, I18N_DOMAIN), + lang_code, + I18N_OUTPUT_DIR_JS, + I18N_DOMAIN + ) def params(): @@ -71,6 +293,15 @@ def params(): url = plugin_url license = plugin_license + # adding the new commands + cmdclass = { + 'clean': CleanCommand, + 'babel_new': NewTranslation, + 'babel_extract': ExtractTranslation, + 'babel_refresh': RefreshTranslation, + 'babel_compile': CompileTranslation + }; + # we only have our plugin package to install packages = [plugin_package] @@ -82,8 +313,14 @@ def params(): # this plugin is not zip_safe. zip_safe = False - # Read the requirements from our requirements.txt file - install_requires = requirements("requirements.txt") + install_requires = INSTALL_REQUIRES + extras_require = EXTRA_REQUIRES + + if os.environ.get('READTHEDOCS', None) == 'True': + # we can't tell read the docs to please perform a pip install -e .[develop], so we help + # it a bit here by explicitly adding the development dependencies, which include our + # documentation dependencies + install_requires = install_requires + extras_require['develop'] # Hook the plugin into the "octoprint.plugin" entry point, mapping the plugin_identifier to the plugin_package. # That way OctoPrint will be able to find the plugin and load it. @@ -93,4 +330,4 @@ def params(): return locals() -setuptools.setup(**params()) \ No newline at end of file +setup(**params()) \ No newline at end of file