Initial
This commit is contained in:
699
stringslator.py
Executable file
699
stringslator.py
Executable file
@@ -0,0 +1,699 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import sys
|
||||
import sqlite3 # v3.3+
|
||||
import plistlib
|
||||
import codecs
|
||||
import re
|
||||
import argparse
|
||||
|
||||
scriptFilePath = os.path.realpath(__file__)
|
||||
scriptRoot = os.path.abspath(os.path.join(scriptFilePath, os.pardir))
|
||||
PATH_DB = os.path.join(scriptRoot, 'stringslator.db')
|
||||
|
||||
|
||||
def main():
|
||||
ARGSParser().parse()
|
||||
|
||||
# --------------------------------------------
|
||||
#
|
||||
# StringsDB
|
||||
#
|
||||
# --------------------------------------------
|
||||
|
||||
|
||||
class StringsDB(object):
|
||||
""" Communication with, and processing of stringslator db """
|
||||
db = None
|
||||
sql = None
|
||||
|
||||
def __init__(self):
|
||||
super(StringsDB, self).__init__()
|
||||
self.db = sqlite3.connect(PATH_DB)
|
||||
self.sql = self.db.cursor()
|
||||
self.createTablesIfNeeded()
|
||||
|
||||
def __del__(self):
|
||||
self.db.commit()
|
||||
self.db.close()
|
||||
|
||||
# --------------------------------------------
|
||||
# stringslator API
|
||||
# --------------------------------------------
|
||||
|
||||
def apiInfo(self, f_id, isComponent=False):
|
||||
""" Return 3-tuple (dbFetchFile(), dbFetchComponents(), dbFetchCounts())
|
||||
"""
|
||||
if isComponent:
|
||||
f_id = self.fetchFileFromComponent(f_id)
|
||||
file = self.fetchFile(f_id)
|
||||
if file is None:
|
||||
return None, None, None
|
||||
return file, self.fetchComponents(f_id), self.fetchCounts(f_id)
|
||||
|
||||
def apiSearch(self, term, titlesearch=False, langs=["en%"]):
|
||||
""" Argument takes search term (use '%' for ambiguous matching).
|
||||
If titlesearch = True, match all rows where title matches exactly.
|
||||
Return array of tuples (f_id, c_id, l_id, title, translation)
|
||||
"""
|
||||
if titlesearch:
|
||||
self.sql.execute("SELECT * FROM _trans WHERE key LIKE ?", [term])
|
||||
else:
|
||||
langIds = self.fetchLanguageIDs(langs)
|
||||
lParam = ','.join('?' * len(langIds))
|
||||
self.sql.execute('''SELECT * FROM _trans WHERE value LIKE ?
|
||||
AND lid IN (%s)''' % lParam, [term] + langIds)
|
||||
return self.sql.fetchall()
|
||||
|
||||
def apiExport(self, c_id, key):
|
||||
""" Return array of tuples (lang, translation) """
|
||||
self.sql.execute('''
|
||||
SELECT l.name, t.value
|
||||
FROM _trans t INNER JOIN _lang l ON l.id = t.lid
|
||||
WHERE cid = ? and key = ?
|
||||
ORDER BY l.name COLLATE NOCASE''', [c_id, key])
|
||||
return self.sql.fetchall()
|
||||
|
||||
def apiAdd(self, path, recursive=False):
|
||||
""" Add path to db by enumerating all string files (recursively) """
|
||||
for resPath in enumerateResourcePaths(path, recursive):
|
||||
self.insertResourceIntoDB(resPath)
|
||||
|
||||
def apiDelete(self, idOrPath, recursive=False):
|
||||
""" Delete an application with given file-id or path.
|
||||
Recursive = True is used for paths only.
|
||||
"""
|
||||
if idOrPath.isdigit():
|
||||
file = self.fetchFile(idOrPath)
|
||||
if not file:
|
||||
print("id %s does not exist." % idOrPath)
|
||||
else:
|
||||
yield self.deleteFile(idOrPath), file[1]
|
||||
else:
|
||||
for f_id, name in self.fetchFileIdsWithPath(idOrPath, recursive):
|
||||
yield self.deleteFile(f_id), name
|
||||
|
||||
def apiList(self, table, term=None):
|
||||
""" table is either 'file', 'comp', 'lang', or 'title'
|
||||
term is either row-id or name like %string%
|
||||
"""
|
||||
TB = {"file": "_file", "comp": "_comp", "lang": "_lang"}
|
||||
if table not in TB:
|
||||
return None
|
||||
tbl = TB[table]
|
||||
stmt = ""
|
||||
if term:
|
||||
if term.isdigit():
|
||||
stmt = "WHERE id = %d" % int(term)
|
||||
else:
|
||||
stmt = "WHERE name like '%%%s%%'" % term
|
||||
|
||||
self.sql.execute('''SELECT id,name FROM %s %s
|
||||
ORDER BY name COLLATE NOCASE''' % (tbl, stmt))
|
||||
return self.sql.fetchall()
|
||||
|
||||
def apiListTitles(self, f_id):
|
||||
""" Return a list of all keys for a given component-id """
|
||||
self.sql.execute('''SELECT cid,key FROM _trans WHERE fid=? GROUP BY key
|
||||
ORDER BY key COLLATE NOCASE''', [f_id])
|
||||
return self.sql.fetchall()
|
||||
|
||||
# --------------------------------------------
|
||||
# SQLite management helper
|
||||
# --------------------------------------------
|
||||
|
||||
def createTablesIfNeeded(self):
|
||||
""" Set schema if not already present """
|
||||
self.sql.execute('''CREATE TABLE IF NOT EXISTS _file (
|
||||
id integer NOT NULL PRIMARY KEY,
|
||||
name text,
|
||||
dir text
|
||||
)''')
|
||||
self.sql.execute('''CREATE TABLE IF NOT EXISTS _comp (
|
||||
id integer NOT NULL PRIMARY KEY,
|
||||
fid integer NOT NULL REFERENCES _file(id),
|
||||
name text
|
||||
)''')
|
||||
self.sql.execute('''CREATE TABLE IF NOT EXISTS _lang (
|
||||
id integer NOT NULL PRIMARY KEY,
|
||||
name text
|
||||
)''')
|
||||
self.sql.execute('''CREATE TABLE IF NOT EXISTS _trans (
|
||||
fid integer NOT NULL REFERENCES _file(id),
|
||||
cid integer NOT NULL REFERENCES _comp(id),
|
||||
lid integer NOT NULL REFERENCES _lang(id),
|
||||
key text,
|
||||
value text
|
||||
)''')
|
||||
|
||||
def fetchIdForTable(self, table, cols=[], vals=[]):
|
||||
""" Fetch row-id for given table, columns, and values """
|
||||
if len(cols) != len(vals):
|
||||
raise Exception("COLS and VALS are not of same length")
|
||||
cols = [x + "=?" for x in cols]
|
||||
self.sql.execute('SELECT id FROM %s WHERE %s' %
|
||||
(table, " AND ".join(cols)), vals)
|
||||
return self.sql.fetchone()
|
||||
|
||||
def insertIdIntoTable(self, table, cols=[], vals=[]):
|
||||
""" Insert new row into table with given values """
|
||||
if len(cols) != len(vals):
|
||||
raise Exception("COLS and VALS are not of same length")
|
||||
self.sql.execute('INSERT INTO %s(%s) VALUES (%s)' % (
|
||||
table, ','.join(cols), ','.join('?' * len(vals))), vals)
|
||||
return self.sql.lastrowid
|
||||
|
||||
def insertOrReturnRowID(self, table, cols, vals=[]):
|
||||
""" Return tuple (row-id, didExistBeforeFlag) """
|
||||
idn = self.fetchIdForTable(table, cols.split(','), vals)
|
||||
if idn is None:
|
||||
return self.insertIdIntoTable(table, cols.split(','), vals), False
|
||||
else:
|
||||
return idn[0], True
|
||||
|
||||
def insertFile(self, path, name):
|
||||
""" Return row id of _file table. Insert new one if necessary. """
|
||||
return self.insertOrReturnRowID('_file', 'name,dir', [name, path])
|
||||
|
||||
def insertComponent(self, f_id, comp):
|
||||
""" Return row id of _comp table. Insert new one if necessary. """
|
||||
return self.insertOrReturnRowID('_comp', 'fid,name', [f_id, comp])[0]
|
||||
|
||||
def insertLang(self, lang):
|
||||
""" Return row id of _lang table. Insert new one if necessary. """
|
||||
return self.insertOrReturnRowID('_lang', 'name', [lang])[0]
|
||||
|
||||
def fetchLanguageIDs(self, langs=[]):
|
||||
""" Return list of ids matching provided langs array """
|
||||
if not langs or type(langs) is not list or len(langs) == 0:
|
||||
raise Exception("fetchLanguageIDs arg is not a list or empty")
|
||||
self.sql.execute("SELECT id FROM _lang WHERE %s" %
|
||||
" OR ".join(["name LIKE ?"] * len(langs)), langs)
|
||||
return [x[0] for x in self.sql]
|
||||
|
||||
def fetchFileFromComponent(self, c_id):
|
||||
""" Return file-id with given component-id """
|
||||
self.sql.execute('SELECT fid FROM _comp WHERE id=?', [c_id])
|
||||
return self.sql.fetchone()[0]
|
||||
|
||||
def fetchFile(self, f_id):
|
||||
""" Return tuple (file-id, file-name, file-dir) """
|
||||
self.sql.execute('SELECT id,name,dir FROM _file WHERE id=?', [f_id])
|
||||
return self.sql.fetchone()
|
||||
|
||||
def fetchComponents(self, f_id):
|
||||
""" Return array of tuple (compunent-id, component-name) """
|
||||
self.sql.execute('''SELECT id,name FROM _comp WHERE fid=?
|
||||
ORDER BY name''', [f_id])
|
||||
return self.sql.fetchall()
|
||||
|
||||
def fetchCounts(self, f_id):
|
||||
""" Return tuple [languages, translations, total] """
|
||||
self.sql.execute('''SELECT count(*) FROM _trans WHERE fid=?
|
||||
GROUP BY lid''', [f_id])
|
||||
counts = [0, 0, 0]
|
||||
for x in self.sql:
|
||||
counts[0] += 1
|
||||
counts[1] = max(x[0], counts[1])
|
||||
counts[2] += x[0]
|
||||
return counts
|
||||
|
||||
def fetchFileIdsWithPath(self, path, recursive=False):
|
||||
""" Return (file-id, file-name) for matching rows.
|
||||
If recursive = True also match all subdirectories.
|
||||
"""
|
||||
path = os.path.abspath(path)
|
||||
if recursive:
|
||||
path = "%s%%" % path
|
||||
self.sql.execute('SELECT id, name FROM _file WHERE dir LIKE ?', [path])
|
||||
return self.sql.fetchall()
|
||||
|
||||
def deleteFile(self, f_id):
|
||||
""" Delete rows in _file, _comp, and _trans where f_id matches """
|
||||
self.sql.execute('DELETE FROM _file WHERE id=?', [f_id])
|
||||
self.sql.execute('DELETE FROM _comp WHERE fid=?', [f_id])
|
||||
self.sql.execute('DELETE FROM _trans WHERE fid=?', [f_id])
|
||||
return self.sql.rowcount # only translations are relevant
|
||||
|
||||
# --------------------------------------------
|
||||
# Insert new application
|
||||
# --------------------------------------------
|
||||
|
||||
def insertResourceIntoDB(self, path):
|
||||
""" Parse 'Resources' folder and insert all localizable strings to db.
|
||||
If path was processed before it will be skipped immediatelly.
|
||||
"""
|
||||
sfe = StringsFileEnumerator(self, path)
|
||||
if not sfe.validPath:
|
||||
print("ERROR: '%s' has no 'Resources' folder." % path)
|
||||
return False
|
||||
if sfe.existing:
|
||||
print("skip existing. '%s'" % sfe.appName)
|
||||
return False
|
||||
|
||||
print("processing '%s'" % sfe.appName)
|
||||
langs, trns = sfe.processResourcesFolder()
|
||||
|
||||
if len(trns) > 0 and len(langs) > 1:
|
||||
self.sql.executemany('INSERT INTO _trans VALUES (?,?,?,?,?)', trns)
|
||||
self.db.commit()
|
||||
print("added id %d '%s' (%d strings, %d languages)" %
|
||||
(sfe.fid, sfe.appName, len(trns), len(langs)))
|
||||
return True
|
||||
else:
|
||||
print("ignored, empty.")
|
||||
self.db.rollback()
|
||||
return False
|
||||
|
||||
|
||||
# --------------------------------------------
|
||||
#
|
||||
# StringsFileEnumerator
|
||||
#
|
||||
# --------------------------------------------
|
||||
|
||||
|
||||
class StringsFileEnumerator(object):
|
||||
""" Helper to find all .strings files in directory 'resPath'.
|
||||
Will return array of extracted languages and translations
|
||||
"""
|
||||
db = None
|
||||
resPath = None
|
||||
appName = None
|
||||
fid = 0
|
||||
existing = True
|
||||
validPath = False
|
||||
|
||||
def __init__(self, stringsDB, path):
|
||||
super(StringsFileEnumerator, self).__init__()
|
||||
self.db = stringsDB
|
||||
self.resPath = self.resourcesPathForPath(path)
|
||||
if self.resPath is not None:
|
||||
self.validPath = True
|
||||
appPath = self.appDirForResourcePath(self.resPath)
|
||||
self.appName = os.path.basename(appPath)
|
||||
self.fid, self.existing = self.db.insertFile(appPath, self.appName)
|
||||
|
||||
# --------------------------------------------
|
||||
# Process .strings files
|
||||
# --------------------------------------------
|
||||
|
||||
def processResourcesFolder(self):
|
||||
""" Enumerate .strings files for all languages (.lproj subfolders) """
|
||||
translations = list()
|
||||
languages = set()
|
||||
for f1, localePath in self.enumerateWithExt(self.resPath, "lproj"):
|
||||
l_id = self.db.insertLang(f1)
|
||||
for f2, locFile in self.enumerateWithExt(localePath, "strings"):
|
||||
languages.add(l_id)
|
||||
c_id = self.db.insertComponent(self.fid, f2)
|
||||
for key, val in self.processStringsFile(locFile):
|
||||
translations.append([self.fid, c_id, l_id, key, val])
|
||||
return languages, translations
|
||||
|
||||
def processStringsFile(self, stringsFile):
|
||||
""" Parse strings file (try XML, then C-source) """
|
||||
with open(stringsFile, 'rb') as fp:
|
||||
try: # try XML format first
|
||||
plist = plistlib.load(fp)
|
||||
for key, val in self.parseStringsFileXML(plist):
|
||||
yield key, val
|
||||
return
|
||||
except plistlib.InvalidFileException:
|
||||
pass
|
||||
try: # then try c-style formatting
|
||||
for key, val in self.parseStringsFileCSource(stringsFile):
|
||||
yield key, val
|
||||
return
|
||||
except Exception as e:
|
||||
print("ERROR: Couldn't read plist '%s'" % stringsFile)
|
||||
raise e
|
||||
|
||||
def parseStringsFileXML(self, xml, prefix=''):
|
||||
""" Parse XML style strings file with nested dicts """
|
||||
for key in xml:
|
||||
val = xml[key]
|
||||
if len(prefix) > 0:
|
||||
key = "%s.%s" % (prefix, key)
|
||||
if type(val) is dict:
|
||||
for key2, val2 in self.parseStringsFileXML(val, prefix=key):
|
||||
yield key2, val2
|
||||
else:
|
||||
yield key, val
|
||||
|
||||
def parseStringsFileCSource(self, filePath):
|
||||
""" Parse C-source-code style strings file.
|
||||
Regex will find assignments and ignore (block-)comments.
|
||||
"""
|
||||
prog = re.compile(r'(?:(?!\s*/\*)(.*?)=(.*?);)|(/\*)|(\*/)')
|
||||
enc = self.findFileEncoding(filePath)
|
||||
with open(filePath, 'r', encoding=enc) as fp:
|
||||
content = fp.read()
|
||||
|
||||
blockComment = False
|
||||
quotes = ["''", '""']
|
||||
for key, val, cmntA, cmntB in prog.findall(content):
|
||||
if cmntA:
|
||||
blockComment = True
|
||||
elif cmntB:
|
||||
blockComment = False
|
||||
elif not blockComment:
|
||||
key = key.strip()
|
||||
if key.startswith("//"): # single line comment
|
||||
continue
|
||||
val = val.strip()
|
||||
if key[0] + key[-1] in quotes:
|
||||
key = key[1:-1]
|
||||
if val[0] + val[-1] in quotes:
|
||||
val = val[1:-1]
|
||||
yield key, val
|
||||
|
||||
def findFileEncoding(self, path):
|
||||
""" Auto detect UTF-8/-16/-32 encoding with BOM """
|
||||
with open(path, 'rb') as fp:
|
||||
header = fp.read(4)
|
||||
for bom, encoding in (
|
||||
(codecs.BOM_UTF32_BE, "utf-32-be"),
|
||||
(codecs.BOM_UTF32_LE, "utf-32-le"),
|
||||
(codecs.BOM_UTF16_BE, "utf-16-be"),
|
||||
(codecs.BOM_UTF16_LE, "utf-16-le"),
|
||||
(codecs.BOM_UTF8, "utf-8")
|
||||
):
|
||||
if header.startswith(bom):
|
||||
break
|
||||
return encoding
|
||||
|
||||
# --------------------------------------------
|
||||
# Folder properties & enumeration
|
||||
# --------------------------------------------
|
||||
|
||||
def resourcesPathForPath(self, path):
|
||||
""" Always navigate into '../Contents/Resources/' folder """
|
||||
try:
|
||||
actual = os.path.basename(os.path.normpath(path))
|
||||
except Exception:
|
||||
return None
|
||||
if actual == "Resources":
|
||||
newPath = path
|
||||
elif actual == "Contents":
|
||||
newPath = os.path.join(path, "Resources")
|
||||
else:
|
||||
newPath = os.path.join(path, "Contents", "Resources")
|
||||
|
||||
if os.path.exists(newPath):
|
||||
return newPath
|
||||
return None
|
||||
|
||||
def appDirForResourcePath(self, resPath):
|
||||
""" Navigate to '../../' from Resources folder """
|
||||
parent = os.path.normpath(resPath)
|
||||
while os.path.basename(parent) in ["Contents", "Resources"]:
|
||||
parent = os.path.abspath(os.path.join(parent, os.pardir))
|
||||
return parent
|
||||
|
||||
def enumerateWithExt(self, resPath, extension):
|
||||
""" Enumerate all files and folders in resPath with given extension """
|
||||
for x in os.listdir(resPath):
|
||||
f, e = os.path.splitext(x)
|
||||
if e.endswith(extension):
|
||||
yield f, os.path.join(resPath, x)
|
||||
|
||||
|
||||
# --------------------------------------------
|
||||
#
|
||||
# UserIO
|
||||
#
|
||||
# --------------------------------------------
|
||||
|
||||
|
||||
class UserIO(object):
|
||||
""" Helper class for user CLI input / output """
|
||||
|
||||
def __init__(self):
|
||||
super(UserIO, self).__init__()
|
||||
|
||||
# https://stackoverflow.com/a/3041990
|
||||
def ask(self, question, default="yes"):
|
||||
"""Ask a yes/no question via raw_input() and return their answer.
|
||||
|
||||
"question" is a string that is presented to the user.
|
||||
"default" is the presumed answer if the user just hits <Enter>.
|
||||
It must be "yes" (the default), "no" or None (meaning
|
||||
an answer is required of the user).
|
||||
|
||||
The "answer" return value is True for "yes" or False for "no".
|
||||
"""
|
||||
valid = {"yes": True, "y": True, "ye": True,
|
||||
"no": False, "n": False}
|
||||
if default is None:
|
||||
prompt = " [y/n] "
|
||||
elif default == "yes":
|
||||
prompt = " [Y/n] "
|
||||
elif default == "no":
|
||||
prompt = " [y/N] "
|
||||
else:
|
||||
raise ValueError("invalid default answer: '%s'" % default)
|
||||
|
||||
while True:
|
||||
sys.stdout.write(question + prompt)
|
||||
choice = input().lower()
|
||||
if default is not None and choice == '':
|
||||
return valid[default]
|
||||
elif choice in valid:
|
||||
return valid[choice]
|
||||
else:
|
||||
sys.stdout.write("Please respond with 'yes' or 'no' "
|
||||
"(or 'y' or 'n').\n")
|
||||
|
||||
def printResults(self, arr, verbose=True):
|
||||
""" Print formatted results dict or 'Nothing found' message.
|
||||
If result array contains more than 100 entries, ask user beforehand.
|
||||
"""
|
||||
if arr is None or len(arr) == 0:
|
||||
print(" Nothing found.")
|
||||
return
|
||||
# s = len(arr)
|
||||
# if s > 100 and not ask("Found %d entries. Show complete list?" % s):
|
||||
# return
|
||||
if verbose:
|
||||
print()
|
||||
if len(arr[0]) == 2:
|
||||
for i, n in arr:
|
||||
print("%5d | %s" % (i, n))
|
||||
elif len(arr[0]) == 5:
|
||||
for f, c, l, key, value in arr:
|
||||
value = value.replace('\n', '\\n')
|
||||
print("%5d | %s --- ('%s')" % (c, value, key))
|
||||
if verbose:
|
||||
print("\n%d results.\n" % len(arr))
|
||||
|
||||
def printInfoForFile(self, file, components, counts):
|
||||
print('Info for file:')
|
||||
print(' id: %d' % file[0])
|
||||
print(" name: '%s'" % file[1])
|
||||
print(" path: '%s'" % file[2])
|
||||
print('components:')
|
||||
self.printResults(components, verbose=False)
|
||||
print("localizable strings:")
|
||||
print(" languages: %d" % counts[0])
|
||||
print(" translations: %d" % counts[1])
|
||||
print(" total: %d" % counts[2])
|
||||
|
||||
def printDeletingFiles(self, delFiles):
|
||||
print("Deleting:")
|
||||
for x in sorted(delFiles):
|
||||
print(" - %s" % x)
|
||||
print()
|
||||
|
||||
|
||||
def enumerateResourcePaths(anyPath, recursive=False):
|
||||
""" Find all subdirectories that contain '../Contents/Resources/'.
|
||||
If recursive = False just yield anyPath.
|
||||
"""
|
||||
if recursive:
|
||||
# if os.path.isdir(anyPath):
|
||||
for x in os.walk(anyPath):
|
||||
# make sure ../Contents/Resources/.. exists
|
||||
if os.path.basename(x[0]) != "Contents":
|
||||
continue
|
||||
if "Resources" in x[1]:
|
||||
yield x[0]
|
||||
else:
|
||||
yield anyPath
|
||||
|
||||
|
||||
# --------------------------------------------
|
||||
#
|
||||
# ARGSParser
|
||||
#
|
||||
# --------------------------------------------
|
||||
|
||||
|
||||
class ARGSParser(object):
|
||||
""" Handle CLI parameter parsing and command calls """
|
||||
parser = None
|
||||
|
||||
def __init__(self):
|
||||
super(ARGSParser, self).__init__()
|
||||
self.parser = self.initCLIParser(
|
||||
[self.cli_add, self.cli_delete, self.cli_list,
|
||||
self.cli_search, self.cli_export, self.cli_info])
|
||||
self.parser.epilog = '''
|
||||
examples:
|
||||
{0} add -r '/System/' '/Applications/baRSS.app'
|
||||
{0} search '% Update%'
|
||||
{0} export 714 kWDLocPerfSignalGraphToolTip
|
||||
|
||||
run <command> -h to show help for command arguments'''.format(__file__)
|
||||
|
||||
# ------------------------------------------------------
|
||||
# CLI interface
|
||||
# ------------------------------------------------------
|
||||
|
||||
def cli_add(self, args):
|
||||
""" Add new application to db """
|
||||
if not args:
|
||||
return 'add', ['a'], 'Add new application to db', {
|
||||
'--recursive': (bool, 'Repeat for subdirectories'),
|
||||
'path+': (str, '<path>', 'App or Resources directory'),
|
||||
}
|
||||
sdb = StringsDB()
|
||||
for path in args.path:
|
||||
sdb.apiAdd(path, args.recursive)
|
||||
|
||||
def cli_delete(self, args):
|
||||
""" Delete application from db """
|
||||
if not args:
|
||||
return 'delete', ['rm'], 'Delete application from db', {
|
||||
'--recursive': (bool, 'Delete apps in subdirectories as well'),
|
||||
'path+': (str, '<file-id|path>', 'Row-id or application path'),
|
||||
}
|
||||
sdb = StringsDB()
|
||||
Del = 0
|
||||
delFiles = []
|
||||
for path in args.path:
|
||||
for changes, filename in sdb.apiDelete(path, args.recursive):
|
||||
Del += changes
|
||||
delFiles.append(filename)
|
||||
|
||||
if len(delFiles) == 0 or Del == 0:
|
||||
print("Nothing to do.")
|
||||
return
|
||||
UserIO().printDeletingFiles(delFiles)
|
||||
if not UserIO().ask("Deleting %d translations. Continue?" % Del, None):
|
||||
sdb.db.rollback()
|
||||
|
||||
def cli_list(self, args):
|
||||
""" List files, components, languages, keys """
|
||||
if not args:
|
||||
return 'list', ['ls'], 'List files, components, languages, keys', {
|
||||
'mutually_exclusive': True,
|
||||
'-f?': (str, '<term>', 'list files'),
|
||||
'-c?': (str, '<term>', 'list components'),
|
||||
'-l?': (str, '<term>', 'list languages'),
|
||||
'-k': (int, '<file-id>', 'list translation keys'),
|
||||
}
|
||||
sdb = StringsDB()
|
||||
if hasattr(args, 'k'):
|
||||
UserIO().printResults(sdb.apiListTitles(args.k))
|
||||
else:
|
||||
for x, tbl in {'f': 'file', 'c': 'comp', 'l': 'lang'}.items():
|
||||
if hasattr(args, x):
|
||||
UserIO().printResults(sdb.apiList(tbl, getattr(args, x)))
|
||||
|
||||
def cli_search(self, args):
|
||||
""" Search db for translation or title-key """
|
||||
if not args:
|
||||
return 'search', ['s'], 'Search db for translation or title-key', {
|
||||
'--keys': (bool, 'search title-keys instead of translations'),
|
||||
'term': (str, '<search-term>',
|
||||
'Search pattern using %% and _ wildcards'),
|
||||
}
|
||||
sdb = StringsDB()
|
||||
UserIO().printResults(sdb.apiSearch(
|
||||
args.term, titlesearch=args.keys, langs=["en%", "de%", "Ger%"]))
|
||||
|
||||
def cli_export(self, args):
|
||||
""" Export translations for title-key """
|
||||
if not args:
|
||||
return 'export', ['e'], 'Export translations for title-key', {
|
||||
'id': (int, '<comp-id>', 'Row-id of a component'),
|
||||
'key': (str, '<title-key>',
|
||||
'Title-key within the same component'),
|
||||
}
|
||||
sdb = StringsDB()
|
||||
for lang, text in sdb.apiExport(args.id, args.key):
|
||||
print("%s|%s" % (lang, text))
|
||||
|
||||
def cli_info(self, args):
|
||||
""" Display info for file-id or component-id """
|
||||
if not args:
|
||||
return 'info', ['i'], 'Display info for file-id or component-id', {
|
||||
'id': (int, '<file-id|comp-id>',
|
||||
'Row id of file or component (-c)'),
|
||||
'--component': (bool, 'search component-id instead of file-id')
|
||||
}
|
||||
sdb = StringsDB()
|
||||
app, components, counts = sdb.apiInfo(args.id, args.component)
|
||||
if app is None:
|
||||
print("\nFile id does not exist. Try search for an id:")
|
||||
print(" %s list -f %%Finder%%\n" % os.path.basename(__file__))
|
||||
else:
|
||||
UserIO().printInfoForFile(app, components, counts)
|
||||
|
||||
# ------------------------------------------------------
|
||||
# argparse stuff
|
||||
# ------------------------------------------------------
|
||||
|
||||
def initCLIParser(self, methods):
|
||||
""" Initialize argparse with commands from method dictionary """
|
||||
parser = argparse.ArgumentParser(add_help=False)
|
||||
parser.formatter_class = argparse.RawTextHelpFormatter
|
||||
parser.set_defaults(func=lambda x: parser.print_help(sys.stderr))
|
||||
subPrs = parser.add_subparsers(title='commands', metavar=" " * 13)
|
||||
for fn in methods:
|
||||
info = fn(None) # call function w/o params to get info dict
|
||||
self.initCLICommand(subPrs, fn, *info)
|
||||
parser.usage = "%(prog)s <command>"
|
||||
return parser
|
||||
|
||||
def initCLICommand(self, parentParser, fn, name, alias, hlp, args):
|
||||
""" Add new command (e.g., add, delete, ...) to parser """
|
||||
cmd = parentParser.add_parser(name, aliases=alias, help=hlp)
|
||||
cmd.set_defaults(func=fn)
|
||||
if name == 'list':
|
||||
cmd.epilog = '<term> can be either row-id or search string.'
|
||||
|
||||
for param, options in args.items():
|
||||
if param.lower() == "mutually_exclusive":
|
||||
cmd = cmd.add_mutually_exclusive_group(required=True)
|
||||
continue
|
||||
self.initCLICommandArgument(cmd, param, options)
|
||||
|
||||
def initCLICommandArgument(self, commandParser, param, options):
|
||||
""" Add command argument (e.g., -k, path, ...) to given cli command """
|
||||
args = {'help': options[-1]}
|
||||
typ = options[0]
|
||||
if param[-1] in ['?', '*', '+']:
|
||||
args['nargs'] = param[-1]
|
||||
param = param[:-1]
|
||||
if typ == bool:
|
||||
args['action'] = 'store_true'
|
||||
else:
|
||||
args['type'] = typ
|
||||
args['metavar'] = options[1]
|
||||
# make short form and prepare for unpacking
|
||||
param = [param[1:3], param] if param.startswith('--') else [param]
|
||||
opt = commandParser.add_argument(*param, **args)
|
||||
if typ != bool:
|
||||
opt.default = argparse.SUPPRESS
|
||||
|
||||
def parse(self):
|
||||
""" Parse the args and call whatever function was selected """
|
||||
args = self.parser.parse_args()
|
||||
args.func(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user