diff --git a/commandLineTool/README.md b/commandLineTool/README.md new file mode 100644 index 0000000..a0fb1f3 --- /dev/null +++ b/commandLineTool/README.md @@ -0,0 +1,238 @@ +Smartling Python Translation Tool +======= + +Command line tool to upload, download, and import translation to Smartling + +Usage +---------- + +```bash +$ python smartlingTool.py -h +usage: smartlingTool.py [-h] [-k APIKEY] [-p PROJECTID] + [-c CONFIGFILE] + {upload,download,import} ... + +Smartling Translation Tool to upload, download, and import translation files + +positional arguments: + {upload,download,import} To see individual sub command help: 'subcommand -h' + upload Upload the (English) source + download Download the translations + import Import translations + +optional arguments: + -h, --help show this help message and exit + -k APIKEY, --apiKey APIKEY + Smartling API key (overrides configuration file value) + -p PROJECTID, --projectId PROJECTID + Smartling project ID (overrides configuration file + value) + -c CONFIGFILE, --config CONFIGFILE + Configuration file (default ./translation.cfg) +``` + +### Upload + +```bash +$ python smartlingTool.py upload -h +usage: smartlingTool.py upload [-h] -d DIR [-u URIPATH] [--run] + +optional arguments: + -h, --help show this help message and exit + -d DIR, --dir DIR Path to English source directory or file + -u URIPATH, --uriPath URIPATH + File URI path used in Smartling system + --run Run for real (default is noop) +``` + +### Download + +Any translation that is not 100% complete will be skipped. +Translation which are not 100% and are download will include English in their place. + +```bash +$ python smartlingTool.py download -h +usage: smartlingTool.py download [-h] -d DIR -o OUTPUTDIR [-u URIPATH] + [-l LOCALE] [-p] [-s] [--run] + +optional arguments: + -h, --help show this help message and exit + -d DIR, --dir DIR Path to English source directory of file + -o OUTPUTDIR, --outputDir OUTPUTDIR + Output directory where to save translated files. + Stores each translation in their own sub-directory. + -u URIPATH, --uriPath URIPATH + File URI path used in Smartling system + -l LOCALE, --locale LOCALE + Locale to download (default is all) + -p, --allowPartial Allow translation not 100% complete (default is false) + -s, --pseudo Download pseudo translations + --run Run for real (default is noop) +``` + +### Import + +```bash +$ python smartlingTool.py import -h +usage: smartlingTool.py import [-h] -d DIR [-u URIPATH] -l LOCALE + [-o] [--run] + +optional arguments: + -h, --help show this help message and exit + -d DIR, --dir DIR Path to translations directory or file + -u URIPATH, --uriPath URIPATH + File URI path used in Smartling system + -l LOCALE, --locale LOCALE + Locale to import + -o, --overwrite Overwrite previous translations + --run Run for real (default is noop) +``` + +Configuration File +---------- + +Default location for configuration file: `translation.cfg` + +``` +[smartling] +# Project API Key +apiKey = XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXX + +# Project ID +projectId = XXXXXXXX + +# Custom URI path (default "/files") +uriPath = /files/config-ui + +[directives] +# Smartling directives +# See: https://docs.smartling.com/display/docs/Supported+File+Types +translate_mode = all +source_key_paths = {*} +placeholder_format_custom = __\w+__|\{{2,2}[\w\.]+\}{2,2} +variants_enabled = true + +[locales] +# Locale Mapping - Use to map differences between project and Smartling locale codes +en = en-US +de = de-DE +es = es-ES +fr = fr-FR +ja = ja-JP +pt = pt-PT +pt_BR = pt-BR + + +[filters] +# Common delimited list of file extensions (default all files) +file_extensions = + + +[extensions] +# Extension Mapping - Use to map differences between file types and Smartling file types + +# Gettext .pot and .po files +gettext = pot,po + +# HTML files +html = html,htm + +# Java Properties +javaProperties = properties + +# Yaml files +yaml = yml + +# Supports .xlf, .xliff, and .xml files that use the XML Localization Interchange File Format (XLIFF) +xliff = xlf,xliff + +# Javascript files +json = json,js + +# Qt Linguist TS format files +qt = ts + +# MadCap Flare ZIP packages +madcap = zip +``` + +Examples +---------- + +### Upload + +```bash +$ python smartlingTool.py upload --dir /workspace/config-ui/static/locales/en +2014-11-03 10:44:06,208 - INFO - Uploading source files from: /workspace/config-ui/static/locales/en +2014-11-03 10:44:06,209 - INFO - Upload (noop): /workspace/config-ui/static/locales/en/badges.json -> /files/config-ui/badges.json +2014-11-03 10:44:06,209 - INFO - Upload (noop): /workspace/config-ui/static/locales/en/common.json -> /files/config-ui/common.json +2014-11-03 10:44:06,209 - INFO - Upload (noop): /workspace/config-ui/static/locales/en/container.json -> /files/config-ui/container.json +2014-11-03 10:44:06,209 - INFO - Upload (noop): /workspace/config-ui/static/locales/en/editCopy.json -> /files/config-ui/editCopy.json +2014-11-03 10:44:06,209 - INFO - Upload (noop): /workspace/config-ui/static/locales/en/preview.json -> /files/config-ui/preview.json +2014-11-03 10:44:06,209 - INFO - Upload (noop): /workspace/config-ui/static/locales/en/submission.json -> /files/config-ui/submission.json +``` + +### Download + +```bash +python smartlingTool.py download --dir /workspace/config-ui/static/locales/en --outputDir /workspace/config-ui/static/locales +2014-11-03 10:44:28,670 - INFO - Downloading translated files for: /workspace/config-ui/static/locales/en +2014-11-03 10:44:29,576 - INFO - Fetching translations (noop) for French (France) (fr-FR) +2014-11-03 10:44:30,172 - INFO - Translated 100% (fr-FR): /files/config-ui/badges.json -> /workspace/config-ui/static/locales/fr/badges.json +2014-11-03 10:44:30,734 - INFO - Translated 100% (fr-FR): /files/config-ui/common.json -> /workspace/config-ui/static/locales/fr/common.json +2014-11-03 10:44:31,228 - INFO - Translated 100% (fr-FR): /files/config-ui/container.json -> /workspace/config-ui/static/locales/fr/container.json +2014-11-03 10:44:32,506 - INFO - Translated 100% (fr-FR): /files/config-ui/editCopy.json -> /workspace/config-ui/static/locales/fr/editCopy.json +2014-11-03 10:44:32,965 - INFO - Translated 100% (fr-FR): /files/config-ui/preview.json -> /workspace/config-ui/static/locales/fr/preview.json +2014-11-03 10:44:33,774 - INFO - Translated 100% (fr-FR): /files/config-ui/submission.json -> /workspace/config-ui/static/locales/fr/submission.json +2014-11-03 10:44:33,774 - INFO - Fetching translations (noop) for German (Germany) (de-DE) +2014-11-03 10:44:34,455 - INFO - Translated 100% (de-DE): /files/config-ui/badges.json -> /workspace/config-ui/static/locales/de/badges.json +2014-11-03 10:44:34,993 - INFO - Translated 100% (de-DE): /files/config-ui/common.json -> /workspace/config-ui/static/locales/de/common.json +2014-11-03 10:44:35,563 - INFO - Translated 100% (de-DE): /files/config-ui/container.json -> /workspace/config-ui/static/locales/de/container.json +2014-11-03 10:44:36,065 - INFO - Translated 100% (de-DE): /files/config-ui/editCopy.json -> /workspace/config-ui/static/locales/de/editCopy.json +2014-11-03 10:44:36,488 - INFO - Translated 100% (de-DE): /files/config-ui/preview.json -> /workspace/config-ui/static/locales/de/preview.json +2014-11-03 10:44:37,085 - INFO - Translated 100% (de-DE): /files/config-ui/submission.json -> /workspace/config-ui/static/locales/de/submission.json +2014-11-03 10:44:37,085 - INFO - Fetching translations (noop) for Japanese (ja-JP) +2014-11-03 10:44:37,564 - INFO - Translated 100% (ja-JP): /files/config-ui/badges.json -> /workspace/config-ui/static/locales/ja/badges.json +2014-11-03 10:44:38,124 - INFO - Translated 100% (ja-JP): /files/config-ui/common.json -> /workspace/config-ui/static/locales/ja/common.json +2014-11-03 10:44:38,670 - INFO - Translated 100% (ja-JP): /files/config-ui/container.json -> /workspace/config-ui/static/locales/ja/container.json +2014-11-03 10:44:39,150 - INFO - Translated 100% (ja-JP): /files/config-ui/editCopy.json -> /workspace/config-ui/static/locales/ja/editCopy.json +2014-11-03 10:44:39,666 - INFO - Translated 100% (ja-JP): /files/config-ui/preview.json -> /workspace/config-ui/static/locales/ja/preview.json +2014-11-03 10:44:40,164 - INFO - Translated 100% (ja-JP): /files/config-ui/submission.json -> /workspace/config-ui/static/locales/ja/submission.json +2014-11-03 10:44:40,164 - INFO - Fetching translations (noop) for Portuguese (Portugal) (pt-PT) +2014-11-03 10:44:40,614 - INFO - Translated 100% (pt-PT): /files/config-ui/badges.json -> /workspace/config-ui/static/locales/pt/badges.json +2014-11-03 10:44:41,375 - INFO - Translated 100% (pt-PT): /files/config-ui/common.json -> /workspace/config-ui/static/locales/pt/common.json +2014-11-03 10:44:41,774 - INFO - Translated 100% (pt-PT): /files/config-ui/container.json -> /workspace/config-ui/static/locales/pt/container.json +2014-11-03 10:44:42,218 - INFO - Translated 100% (pt-PT): /files/config-ui/editCopy.json -> /workspace/config-ui/static/locales/pt/editCopy.json +2014-11-03 10:44:42,731 - INFO - Translated 100% (pt-PT): /files/config-ui/preview.json -> /workspace/config-ui/static/locales/pt/preview.json +2014-11-03 10:44:43,180 - INFO - Translated 100% (pt-PT): /files/config-ui/submission.json -> /workspace/config-ui/static/locales/pt/submission.json +2014-11-03 10:44:43,180 - INFO - Fetching translations (noop) for Spanish (Spain) (es-ES) +2014-11-03 10:44:43,864 - INFO - Translated 100% (es-ES): /files/config-ui/badges.json -> /workspace/config-ui/static/locales/es/badges.json +2014-11-03 10:44:44,288 - INFO - Translated 100% (es-ES): /files/config-ui/common.json -> /workspace/config-ui/static/locales/es/common.json +2014-11-03 10:44:44,739 - INFO - Translated 100% (es-ES): /files/config-ui/container.json -> /workspace/config-ui/static/locales/es/container.json +2014-11-03 10:44:45,142 - INFO - Translated 100% (es-ES): /files/config-ui/editCopy.json -> /workspace/config-ui/static/locales/es/editCopy.json +2014-11-03 10:44:45,528 - INFO - Translated 100% (es-ES): /files/config-ui/preview.json -> /workspace/config-ui/static/locales/es/preview.json +2014-11-03 10:44:45,968 - INFO - Translated 100% (es-ES): /files/config-ui/submission.json -> /workspace/config-ui/static/locales/es/submission.json +2014-11-03 10:44:45,968 - INFO - Successfully - all source files translated! +``` + +### Import Directory + +```bash +$ python smartlingTool.py import --dir /workspace/config-ui/static/locales/fr --locale fr +2014-11-03 10:46:39,653 - INFO - Importing translation file(s) from: /workspace/config-ui/static/locales/fr +2014-11-03 10:46:40,624 - INFO - Import translation (noop): /workspace/config-ui/static/locales/fr/badges.json -> /files/config-ui/badges.json +2014-11-03 10:46:41,091 - INFO - Import translation (noop): /workspace/config-ui/static/locales/fr/common.json -> /files/config-ui/common.json +2014-11-03 10:46:41,480 - INFO - Import translation (noop): /workspace/config-ui/static/locales/fr/container.json -> /files/config-ui/container.json +2014-11-03 10:46:41,951 - INFO - Import translation (noop): /workspace/config-ui/static/locales/fr/editCopy.json -> /files/config-ui/editCopy.json +2014-11-03 10:46:42,433 - INFO - Import translation (noop): /workspace/config-ui/static/locales/fr/preview.json -> /files/config-ui/preview.json +2014-11-03 10:46:42,927 - INFO - Import translation (noop): /workspace/config-ui/static/locales/fr/submission.json -> /files/config-ui/submission.json +``` + +### Import File (with overwrite) + +```bash +$ python smartlingTool.py import --dir /workspace/config-ui/static/locales/fr/badges.json --locale fr --overwrite +2014-11-03 10:47:22,401 - INFO - Importing translation file(s) from: /workspace/config-ui/static/locales/fr/badges.json +2014-11-03 10:47:22,910 - INFO - Import translation (noop): /workspace/config-ui/static/locales/fr/badges.json -> /files/config-ui/badges.json +``` + diff --git a/commandLineTool/smartlingTool.py b/commandLineTool/smartlingTool.py new file mode 100644 index 0000000..354c8f2 --- /dev/null +++ b/commandLineTool/smartlingTool.py @@ -0,0 +1,457 @@ +# +# Python command-line: Translation Tool +# Contributed by: Bazaarvoice +# Developers: Michael Goodnow +# + +import os +import sys +import argparse +import logging +import ConfigParser + +# allow to import ../smartlingApiSdk/SmartlingFileApi +lib_path = os.path.abspath(os.path.dirname(os.path.realpath(__file__)) + os.path.sep + os.path.pardir + os.path.sep) +sys.path.append(lib_path) + +from smartlingApiSdk.SmartlingFileApi import SmartlingFileApiFactory +from smartlingApiSdk.SmartlingDirective import SmartlingDirective +from smartlingApiSdk.UploadData import UploadData +from smartlingApiSdk.Constants import ReqMethod + + +class SmartlingApi: + def __init__(self, apiKey, projectId): + self.file_api = SmartlingFileApiFactory().getSmartlingTranslationApi(apiKey, projectId) + + # Upload a source file + def uploadFile(self, uploadData): + self.disableStdOut() + response, code = self.file_api.upload(uploadData) + self.enableStdOut() + if code == 200 and response.code == "SUCCESS": + return response.data + else: + raise IOError("Failed to upload ({0}), caused by: {1}".format(code, self._getMessages(response))) + + # Import a translation + # ApiResponse: {"response":{"data":{"wordCount":10,"translationImportErrors":[{"contentFileId":238103,"stringHashcode":"851194c88c080f24ef257383841eb757","messages":["Information about import key was not found"],"importKey":null}],"stringCount":5},"code":"SUCCESS","messages":[]}} + def importFile(self, uploadData, locale, overwrite=False): + self.disableStdOut() + response, code = self.file_api.import_call(uploadData, locale, translationState="PUBLISHED", overwrite=str(overwrite).lower()) + self.enableStdOut() + if code == 200 and response.code == "SUCCESS": + if response.data.translationImportErrors and len(response.data.translationImportErrors) > 0: + logging.error("Error Importing translation (%s): %s, Caused by:\n%s", + locale, + uploadData.uriPath + uploadData.name, + self._getStringFromArray(response.data.translationImportErrors)) + raise IOError("Failed to import translation file: {0}".format(uploadData.uriPath + uploadData.name)) + return response.data + else: + raise IOError("Failed to import ({0}), caused by: {1}".format(code, self._getMessages(response))) + + # Get a translated files + def getFile(self, fileUri, locale, isPseudo=False): + self.disableStdOut() + if isPseudo: + data, code = self.file_api.get(fileUri, locale, retrievalType="pseudo") + else: + data, code = self.file_api.get(fileUri, locale) + self.enableStdOut() + if code == 200: + return data + else: + raise IOError("Failed to get file ({0}): {1}".format(locale, fileUri)) + + # Get file status + # ApiResponse: {"response":{"data":{"fileUri":"common.json","wordCount":9,"fileType":"json","callbackUrl":null,"lastUploaded":"2014-10-30T19:09:36","stringCount":5,"approvedStringCount":0,"completedStringCount":0},"code":"SUCCESS","messages":[]}} + def getStatus(self, fileUri, locale): + self.disableStdOut() + response, code = self.file_api.status(fileUri, locale) + self.enableStdOut() + if code == 200 and response.code == "SUCCESS": + return response.data + else: + raise IOError("Failed to get file status ({0}): {1}, caused by: {2}".format(locale, fileUri, self._getMessages(response))) + + # Get list of the project locales + # ApiResponse: [{'locale': 'de-DE', 'translated': 'Deutsch', 'name': 'German (Germany)'}, {'locale': 'pl-PL', 'translated': 'Polski', 'name': 'Polish (Poland)'}] + def getProjectLocales(self): + self.disableStdOut() + response, code = self.file_api.command(ReqMethod.GET, "/v1/project/locale/list", params={}) + self.enableStdOut() + if code != 200 or response.code != "SUCCESS": + raise IOError("Failed to get project locales, caused by: {0}".format(self._getMessages(response))) + return response.data.locales + + def _getMessages(self, response): + if response and response.messages: + return self._getStringFromArray(response.messages) + + def _getStringFromArray(self, array): + message = "" + for m in array: + if len(message) > 0: + message += "\n" + message += str(m) + return message + + def disableStdOut(self): + self._stdout = sys.stdout + null = open(os.devnull, 'wb') + sys.stdout = null + + def enableStdOut(self): + if self._stdout: + sys.stdout = self._stdout + + +class SmartlingTranslations: + def __init__(self, args): + self.args = args + self.api = SmartlingApi(args.apiKey, args.projectId) + + # + # Upload Source + # + def uploadSource(self, directives=None): + args = self.args + logging.info("Uploading source file(s) from: %s", args.dir) + if not os.path.isdir(args.dir) and not os.path.isfile(args.dir): + raise ValueError("Invalid source directory/file: {0}".format(args.dir)) + + if os.path.isfile(args.dir): + # Upload single file + self._uploadSourceFile(os.path.dirname(args.dir), os.path.basename(args.dir), args.uriPath, directives) + else: + # Loop through files in directory recursively + for root, dirs, files in os.walk(args.dir): + for name in files: + if self._processFile(name): + relativeUri = args.uriPath + self._getRelativeUri(args.dir, root) + self._uploadSourceFile(root, name, relativeUri, directives) + + def _uploadSourceFile(self, path, fileName, uriPath, directives=None): + absFile = os.path.join(path, fileName) + logging.debug("Uploading: %s", absFile) + + if not path.endswith(os.path.sep): + path += os.path.sep + + if self.args.run: + uploadData = UploadData(path, fileName, self._getFileType(fileName)) + uploadData.setUri(uriPath + fileName) + + if directives: + for directive, value in directives: + uploadData.addDirective(SmartlingDirective(directive, value)) + + try: + stats = self.api.uploadFile(uploadData) + logging.info("Uploaded: %s -> %s (Word count = %s New source = %s)", + absFile, + uriPath + fileName, + stats.wordCount, + "No" if stats.overWritten else "Yes") + except IOError as ex: + if "No source strings found" in str(ex): + logging.warn("No source strings found: %s", absFile) + else: + raise ex + else: + logging.info("Upload (noop): %s -> %s", os.path.join(path, fileName), uriPath + fileName) + + # + # Import Translations + # + def importTranslations(self, directives=None): + args = self.args + logging.info("Importing translation file(s) from: %s", args.dir) + if not os.path.isdir(args.dir) and not os.path.isfile(args.dir): + raise ValueError("Invalid translation directory/file: {0}".format(args.dir)) + + if os.path.isfile(args.dir): + # Upload single file + self._importTranslationFile(os.path.dirname(args.dir), os.path.basename(args.dir), args.uriPath, args.locale, directives, args.overwrite) + else: + # Loop through files in directory recursively + for root, dirs, files in os.walk(args.dir): + for name in files: + if self._processFile(name): + relativeUri = args.uriPath + self._getRelativeUri(args.dir, root) + self._importTranslationFile(root, name, relativeUri, args.locale, directives, args.overwrite) + + def _importTranslationFile(self, path, fileName, uriPath, locale, directives=None, overwrite=False): + smartlingLocale = self._getSmartlingLocale(locale) + + # Test if English source exists + try: + self.api.getStatus(uriPath + fileName, smartlingLocale) + except IOError: + logging.error("Failed to import translation as English source has not been uploaded: %s", uriPath + fileName) + raise ValueError("No English source: {0}".format(uriPath + fileName)) + + absFile = os.path.join(path, fileName) + if self.args.run: + logging.debug("Importing translation (%s): %s", smartlingLocale, absFile) + if not path.endswith(os.path.sep): + path += os.path.sep + uploadData = UploadData(path, fileName, self._getFileType(fileName), uriPath) + uploadData.uri = uriPath + fileName + if directives: + for directive, value in directives: + uploadData.addDirective(SmartlingDirective(directive, value)) + stats = self.api.importFile(uploadData, smartlingLocale, overwrite) + logging.info("Imported translation (%s): %s -> %s (Word count = %s)", + smartlingLocale, + absFile, + uriPath + fileName, + stats.wordCount) + else: + logging.info("Import translation (noop): %s -> %s", absFile, uriPath + fileName) + + # + # Download Translations + # + def downloadTranslations(self): + args = self.args + logging.info("Downloading translated file(s) for: %s", args.dir) + if not os.path.isdir(args.dir) and not os.path.isfile(args.dir): + raise ValueError("Invalid English source directory/file: {}".format(args.dir)) + + if not os.path.isdir(args.outputDir): + logging.debug("Creating output directory: %s", args.outputDir) + os.makedirs(args.outputDir) + + projectLocales = self.api.getProjectLocales() + + allComplete = True + for l in projectLocales: + smartlingLocale = l['locale'] + if args.locale and args.locale != self._getLocaleFromSmartlingLocale(smartlingLocale): + logging.debug("Skipping locale: %s", smartlingLocale) + continue + logging.info("Fetching translations%s for %s (%s)", "" if args.run else " (noop)", l['name'], smartlingLocale) + if os.path.isfile(args.dir): + # Get single file + if not self._getTranslationsFile(args.outputDir, "", args.uriPath, os.path.basename(args.dir), smartlingLocale): + allComplete = False + else: + # Loop through files in directory recursively and get their translations from the server respectfully + for root, dirs, files in os.walk(args.dir): + for name in files: + if self._processFile(name): + relativePath = self._getRelativePath(args.dir, root) + try: + if not self._getTranslationsFile(args.outputDir, relativePath, args.uriPath, name, smartlingLocale): + allComplete = False + except IOError: + logging.warning("File hasn't been uploaded yet: " + args.uriPath + relativePath.replace('\\', '/') + name) + allComplete = False + + if allComplete: + logging.info("Successfully - all source files translated!") + else: + logging.warn("Not all source files are translated!") + + def _getTranslationsFile(self, outputDir, relativePath, uriPath, fileName, smartlingLocale): + args = self.args + sourceFile = uriPath + relativePath.replace('\\', '/') + fileName + outputFile = outputDir + os.path.sep + self._getLocaleFromSmartlingLocale(smartlingLocale) + os.path.sep + relativePath + fileName + + status = self.api.getStatus(sourceFile, smartlingLocale) + percentComplete = self._getPercent(status.completedStringCount, status.stringCount) + + if percentComplete < 100 and not self.args.allowPartial: + logging.info("Translated %s%% (%s): %s -> %s (skipping)", percentComplete, smartlingLocale, sourceFile, outputFile) + return False + + logging.info("Translated %s%% (%s): %s -> %s", percentComplete, smartlingLocale, sourceFile, outputFile) + if args.run: + fileData = self.api.getFile(sourceFile, smartlingLocale, args.pseudo) + if not os.path.isdir(os.path.dirname(outputFile)): + os.makedirs(os.path.dirname(outputFile)) + f = open(outputFile, 'w') + f.write(fileData) + + return percentComplete == 100 + + # Get percentage + def _getPercent(self, count, totalCount): + if count == 0 and totalCount == 0: + return 100 + if totalCount > 0: + return int ((count / float(totalCount)) * 100) + return 0 + + # Get Smartling locale from locale + def _getSmartlingLocale(self, locale): + if hasattr(self.args, 'localeMap'): + if self.args.localeMap[locale]: + return self.args.localeMap[locale] + return locale + + # Get locale from a Smartling locale + def _getLocaleFromSmartlingLocale(self, smartlingLocale): + if hasattr(self.args, 'localeMap'): + for locale, slLocale in self.args.localeMap.iteritems(): + if slLocale == smartlingLocale: + return locale + return smartlingLocale + + # Get relative path + # rootDir = /root/path + # fileDir = /root/path/subdir/path/file + # returns subdir/path/file + def _getRelativePath(self, rootDir, filePath): + relativePath = filePath.replace(rootDir, "", 1) + if relativePath.startswith(os.path.sep): + relativePath = relativePath[1:] + if len(relativePath) > 0 and not relativePath.endswith(os.path.sep): + relativePath += os.path.sep + return relativePath + + def _getRelativeUri(self, rootDir, filePath): + return self._getRelativePath(rootDir, filePath).replace('\\', '/') + + # Determine if file should be processed based on file filters applied in configuration + # returns boolean + def _processFile(self, name): + if not hasattr(self.args, 'filterFileExtensions'): + return True + extension = os.path.splitext(name)[1][1:] + return extension in self.args.filterFileExtensions + + # Determine Smartling file type + def _getFileType(self, filename): + extension = os.path.splitext(filename)[1][1:] + if hasattr(self.args, 'extensionMap'): + for key, value in self.args.extensionMap.iteritems(): + if extension in value.split(","): + return key + return extension + + +def uploadSource(args, directives=None): + tool = SmartlingTranslations(args) + tool.uploadSource(directives) + + +def downloadTranslations(args): + tool = SmartlingTranslations(args) + tool.downloadTranslations() + + +def importTranslations(args, directives=None): + tool = SmartlingTranslations(args) + tool.importTranslations(directives) + + +def main(): + logging.basicConfig(format="%(asctime)s - %(levelname)s - %(message)s", level=logging.INFO) + + parser = argparse.ArgumentParser(description="Smartling Translation Tool to upload, download, and import translation files") + parser.add_argument("-k", "--apiKey", dest="apiKey", help="Smartling API key (overrides configuration file value)") + parser.add_argument("-p", "--projectId", dest="projectId", + help="Smartling project ID (overrides configuration file value)") + parser.add_argument("-c", "--config", dest="configFile", default="translation.cfg", help="Configuration file (default ./translation.cfg)") + + subparsers = parser.add_subparsers(dest="sub_parser", help="To see individual sub command help: 'subcommand -h'") + + parser_upload = subparsers.add_parser("upload", help="Upload the (English) source") + parser_upload.add_argument("-d", "--dir", dest="dir", required=True, help="Path to English source directory or file") + parser_upload.add_argument("-u", "--uriPath", dest="uriPath", help="File URI path used in Smartling system") + parser_upload.add_argument("--run", dest="run", action="store_true", help="Run for real (default is noop)") + + parser_download = subparsers.add_parser("download", help="Download the translations") + parser_download.add_argument("-d", "--dir", dest="dir", required=True, help="Path to English source directory of file") + parser_download.add_argument("-o", "--outputDir", dest="outputDir", required=True, + help="Output directory where to save translated files. Stores each translation in their own sub-directory.") + parser_download.add_argument("-u", "--uriPath", dest="uriPath", help="File URI path used in Smartling system") + parser_download.add_argument("-l", "--locale", dest="locale", help="Locale to download (default is all)") + parser_download.add_argument("-p", "--allowPartial", dest="allowPartial", action="store_true", help="Allow translation not 100%% complete (default is false)") + parser_download.add_argument("-s", "--pseudo", dest="pseudo", action="store_true", help="Download pseudo translations") + parser_download.add_argument("--run", dest="run", action="store_true", help="Run for real (default is noop)") + + parser_import = subparsers.add_parser("import", help="Import translations") + parser_import.add_argument("-d", "--dir", dest="dir", required=True, help="Path to translations directory or file") + parser_import.add_argument("-u", "--uriPath", dest="uriPath", help="File URI path used in Smartling system") + parser_import.add_argument("-l", "--locale", dest="locale", required=True, help="Locale to import") + parser_import.add_argument("-o", "--overwrite", dest="overwrite", action="store_true", help="Overwrite previous translations") + parser_import.add_argument("--run", dest="run", action="store_true", help="Run for real (default is noop)") + + args = parser.parse_args() + + config = ConfigParser.ConfigParser() + # Allow case sensitive configuration keys + config.optionxform = str + config.read(args.configFile) + + if not args.uriPath and config.has_section("smartling"): + args.uriPath = config.get("smartling", "uriPath") + + # Default uriPath + if not args.uriPath: + args.uriPath = "/files" + + # Fix URI path to include trailing slash (expected) + if not args.uriPath.endswith('/'): + args.uriPath += "/" + + # Get API Key + if not args.apiKey and config.has_section("smartling"): + args.apiKey = config.get("smartling", "apiKey") + + # Get Project ID + if not args.projectId and config.has_section("smartling"): + args.projectId = config.get("smartling", "projectId") + if not args.apiKey or not args.projectId: + raise ValueError("Smartling API Key and Project ID are required") + + # Get Locales + if config.has_section("locales"): + locales = {} + for name, value in config.items("locales"): + locales[name] = value + args.localeMap = locales + + if config.has_section("extensions"): + extensions = {} + for name, value in config.items("extensions"): + extensions[name] = value + args.extensionMap = extensions + + # Get file extension filters + if config.has_section("filters") and config.has_option("filters", "file_extensions"): + fileExtensions = config.get("filters", "file_extensions") + if fileExtensions and len(fileExtensions) > 0: + fileExtensionArray = fileExtensions.split(",") + if len(fileExtensionArray) > 0: + args.filterFileExtensions = fileExtensionArray + + + # Upload Command + if args.sub_parser == "upload": + directives = None + if config.has_section("directives"): + directives = config.items("directives") + uploadSource(args, directives) + + # Get Command + if args.sub_parser == "download": + # Pseudo argument + if args.pseudo: + args.allowPartial = True + downloadTranslations(args) + + # Import Command + if args.sub_parser == "import": + directives = None + if config.has_section("directives"): + directives = config.items("directives") + importTranslations(args, directives) + + +main() + diff --git a/commandLineTool/translation.cfg b/commandLineTool/translation.cfg new file mode 100644 index 0000000..30cca2b --- /dev/null +++ b/commandLineTool/translation.cfg @@ -0,0 +1,62 @@ +[smartling] +# Project API Key +apiKey = XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXX + +# Project ID +projectId = XXXXXXXX + +# Custom URI path (default "/files") +uriPath = /files/config-ui + + +[directives] +# Smartling directives +# See: https://docs.smartling.com/display/docs/Supported+File+Types +translate_mode = all +source_key_paths = {*} +placeholder_format_custom = __\w+__|\{{2,2}[\w\.]+\}{2,2} +variants_enabled = true + + +[locales] +# Locale Mapping - Use to map differences between project and Smartling locale codes +en = en-US +de = de-DE +es = es-ES +fr = fr-FR +ja = ja-JP +pt = pt-PT +pt_BR = pt-BR + + +[filters] +# Common delimited list of file extensions (default all files) +file_extensions = + + +[extensions] +# Extension Mapping - Use to map differences between file types and Smartling file types + +# Gettext .pot and .po files +gettext = pot,po + +# HTML files +html = html,htm + +# Java Properties +javaProperties = properties + +# Yaml files +yaml = yml + +# Supports .xlf, .xliff, and .xml files that use the XML Localization Interchange File Format (XLIFF) +xliff = xlf,xliff + +# Javascript files +json = json,js + +# Qt Linguist TS format files +qt = ts + +# MadCap Flare ZIP packages +madcap = zip diff --git a/smartlingApiSdk/FileApiBase.py b/smartlingApiSdk/FileApiBase.py index 47e715c..094f732 100644 --- a/smartlingApiSdk/FileApiBase.py +++ b/smartlingApiSdk/FileApiBase.py @@ -118,6 +118,10 @@ def commandImport(self, uploadData, locale, **kw): kw[Params.LOCALE] = locale self.addApiKeys(kw) + if (uploadData.directives): + for index, directive in enumerate(uploadData.directives): + kw[directive.sl_prefix + directive.name] = directive.value + return self.uploadMultipart(Uri.IMPORT, kw) def commandStatus(self, fileUri, locale, **kw):