#!/usr/bin/env python3# -*- coding: utf-8 -*-# pip3 install googletrans# https://github.com/ssut/py-googletransfrom tkinter import *
from tkinter import ttk, messagebox
from locale import getdefaultlocale
from os import listdir, path
import re
import googletrans
import json
PLUGINS_PATH = '../plugins'
LANGS_PATH = '../core/lang'
DEFAULT_LANG = 'en'
KEY_PATTERN = re.compile(r'''('|")(\w+)\1\s*=>\s*(\'[^\']*\'|"[^"]*"|\w+)''')
DEFAULT_LANG_WARNING = 'You are using %s as default language. It\' a better way to use english(en) instead.'classInterface(Frame):defgetPlugins(self):'''Look for every plugin'''
result = [f for f in listdir(PLUGINS_PATH) if
path.isdir(path.sep.join([PLUGINS_PATH, f])) and
f[-4:] != '.bak'and
path.isfile(path.sep.join([PLUGINS_PATH, f, f + '.php']))
]
result.sort()
return result
defgetCore(self):''' Look for every file in core for translation in the default language'''
result = [f for f in listdir(path.sep.join([LANGS_PATH, DEFAULT_LANG])) if
path.isfile(path.sep.join([LANGS_PATH, DEFAULT_LANG, f])) and
f[-4:] == '.php'
]
result.sort()
return result
defgetLangs(self):'''Look for every language in the core of PluXml. Default and locale languages are setting first'''
locale_full, charset = getdefaultlocale()
locale = locale_full[0:2]
prefix = ['en'] if locale == 'en'and locale notin googletrans.LANGUAGES else ['en', locale]
result = [f for f in listdir(LANGS_PATH) if
path.isdir(path.sep.join([LANGS_PATH, f])) and f notin prefix
]
result.sort()
return prefix + result
defgetKeys(self, filename, lang):'''Look for every key in a file'''# print(filename)
buf = {}
if path.isfile(filename):
with open(filename, encoding='utf-8') as f:
prevKey = ''
content = f.read().replace("\\'", '##').replace('\\"', '#_#')
for enclosure, key, value in KEY_PATTERN.findall(content):
buf[key] = value[1:-1].replace('##', "'").replace('#_#', '"')
if key notin self.keys:
if len(prevKey) == 0or self.plugins.get():
# for plugin
self.keys.append(key)
else:
# for core
self.keys.insert(self.keys.index(prevKey) + 1, key)
prevKey = key
self.items[lang]['translations'] = buf
defgetKeysCore(self, context):''' Scan each translation file for the core of PluXml. No sort !'''
enLang = False
lastLang = ''for lang in self.langs:
if lang == 'en':
enLang = Trueelse:
lastLang = lang
buf = {}
self.getKeys(path.sep.join([LANGS_PATH, lang, context]), lang)
if enLang:
lastLang = 'en'
self.plxLangDefault.set(lastLang)
defgetKeysPlugin(self, pluginName):if path.isdir(path.sep.join([PLUGINS_PATH, pluginName, 'lang'])):
enLang = False
lastLang = ''for lang in self.langs:
if lang == 'en':
enLang = Trueelse:
lastLang = lang
buf = {}
self.getKeys(path.sep.join([PLUGINS_PATH, pluginName, 'lang', lang + '.php']), lang)
if enLang:
lastLang = 'en'
self.plxLangDefault.set(lastLang)
if len(self.keys) > 0:
self.keys.sort()
else:
for lang in self.langs:
self.items[lang]['translations'] = {}
deftranslationsDisplay(self, name, index, mode):
position = self.keyOptionMenu.current()
self.keysIndex = position
self.statusbar.set('%3d/%d keys' % (position + 1, len(self.keys)))
key = self.plxKey.get()
for lang, item in self.items.items():
if key in item['translations']:
item['var'].set(item['translations'][key])
else:
item['var'].set('')
deftargetsUpdate(self, name, index, mode):if self.plugins.get():
options = self.getPlugins()
else:
options = self.getCore()
menu = self.targetOptionMenu['menu']
menu.delete(0, END)
for v in options:
menu.add_command(label=v, command=lambda value=v : self.plxTarget.set(value))
self.plxTarget.set(options[0])
defsetKey(self, position):
maxPos = len(self.keys)
if maxPos > 0:
if position < 0:
position = 0if position > maxPos - 1:
position = maxPos - 1
self.keysIndex = position
self.plxKey.set(self.keys[position])
# self.statusbar.set('%3d/%d keys' % (position + 1, len(self.keys)))else:
position = 0
self.keysIndex = position
self.statusbar.set('No key')
state = DISABLED if position <= 0else NORMAL
self.keysBtns['first']['state'] = state
self.keysBtns['prev']['state'] = state
state = DISABLED if position >= maxPos-1else NORMAL
self.keysBtns['next']['state'] = state
self.keysBtns['last']['state'] = state
state = DISABLED if maxPos == 0else NORMAL
self.keysBtns['empty']['state'] = state
deffirstKey(self):
self.setKey(0)
defprevKey(self):
self.setKey(self.keysIndex - 1)
defnextKey(self):
self.setKey(self.keysIndex + 1)
deflastKey(self):
self.setKey(99999)
defnewKey(self):
print('Coming soon')
defkeysUpdate(self, name, index, mode):
self.keys = []
target = self.plxTarget.get()
if self.plugins.get():
# print('Plugin: ' + target)
self.getKeysPlugin(target)
else:
# print('Core: ' + target)
self.getKeysCore(target)
self.statusbar.set('%d keys' % len(self.keys))
if len(self.keys) > 0:
self.keyOptionMenu['values'] = self.keys
# self.plxKey.set(self.keys[0])else:
self.keyOptionMenu['values'] = ''
self.setKey(0)
self.nextEmptyKey()
defnextEmptyKey(self):
iMax = len(self.keys)
if iMax > 0:
x = self.keysIndex + 1for i in range(x, iMax):
key = self.keys[i]
missing = Falsefor lang in self.langs:
# Don't check for any unused languageif len(self.items[lang]['translations']) > 0and key notin self.items[lang]['translations']:
missing = Truebreakif missing:
x = i
break
self.setKey(x)
else:
self.setKey(0)
defdelKey(self):
print('coming soon')
passdefjsonToClipboard(self):if self.plugins.get():
buf = {}
for lang in self.langs:
buf[lang] = self.items[lang]['translations']
else:
buf = self.lastTranslations
# https://stackoverflow.com/questions/579687/how-do-i-copy-a-string-to-the-clipboard-on-windows-using-python
clp = Tk()
clp.withdraw()
clp.clipboard_clear()
clp.clipboard_append(json.dumps(buf, ensure_ascii=False, sort_keys=True, indent=4, separators=(',', ': ')))
clp.update()
clp.destroy()
defsentenceUpdate(self, name, index, mode):
lang = name[-2:]
entry = self.items[lang]
value = entry['var'].get()
if len(value.strip()) > 0:
key = self.plxKey.get()
if key notin entry['translations'] or entry['translations'][key] != value:
entry['translations'][key] = value
entry['updated'] = True
self.saveBtn['state'] = NORMAL
ifnot self.plugins.get():
if lang notin self.lastTranslations:
self.lastTranslations[lang] = {}
self.lastTranslations[lang][key] = value
deftranslate(self):'''Download translation for each empty string for each language, except default language.'''
defaultLang = self.plxLangDefault.get()
if defaultLang == 'en'or messagebox.askokcancel(
'Bad default language',
message=(DEFAULT_LANG_WARNING % googletrans.LANGUAGES[defaultLang])
) == 'ok' :
# checks if not empty key in default language
missingKeys = []
for key in self.keys:
if key notin self.items[defaultLang]['translations'] or len(self.items[defaultLang]['translations'][key].strip()) == 0:
missingKeys.append(key)
if len(missingKeys) == 0:
# OK! We have every key in default language# loop for each foreign language
self.lastTranslations = {}
for lang in self.langs:
if lang != defaultLang:
if lang in googletrans.LANGUAGES:
keys = []
sentences = []
for k in self.keys:
if k notin self.items[lang]['translations'] or len(self.items[lang]['translations'][k].strip()) == 0:
sentence = self.items[defaultLang]['translations'][k].strip()
# print('%s => %s' % (k, sentence))
keys.append(k)
sentences.append(sentence) # convert HTML entitiesif len(sentences) > 0:
# print('%d sentences to translate to %s' % (len(sentences), lang))
self.statusbar.set('%d sentences for translating into %s ...' % (len(sentences), googletrans.LANGUAGES[lang]))
self.top.update()
translator = googletrans.Translator()
translations = translator.translate(sentences, dest=lang, src=defaultLang)
self.lastTranslations[lang] = {}
for i in range(len(keys)):
k = keys[i]
value = translations[i].text
self.items[lang]['translations'][k] = value
self.lastTranslations[lang][k] = value
# print('%s -> %s' % (k, translations[i].text))
self.items[lang]['missingKeys'] = keys # Required for core in self::langsSave()
self.items[lang]['updated'] = True
self.saveBtn['state'] = NORMAL
else:
self.statusbar.set('Translation not required for ' + googletrans.LANGUAGES[lang])
else:
print('The %s language is unknown' % lang)
self.statusbar.set('Done')
self.setKey(self.keysIndex)
else:
messagebox.showerror('Missing keys', message='The following keys are missing in the default language %s ' % googletrans.LANGUAGES[defaultLang] + '\n'.join(missingKeys))
self.statusbar.set('Some keys are missing in the default language ' + googletrans.LANGUAGES[defaultLang])
deflangsSave(self):'''Save any update after downloading or manual input'''
print('We are saving translations in :')
for lang in self.langs:
print(lang)
if self.items[lang]['updated']:
self.statusbar.set('Saving for ' + lang)
if self.plugins.get():
lines = []
for key in self.keys:
if key in self.items[lang]['translations'] and len(self.items[lang]['translations'][key].strip()) > 0:
value = self.items[lang]['translations'][key].strip().replace('"', '\\"').replace('\n', '\\n')
tabs = '\t' * int((33-len(key)) / 4)
lines.append("'%s'%s=> \"%s\"" % (key, tabs, value))
if len(lines) > 0:
if self.plugins.get():
# for plugin
folder = path.sep.join([PLUGINS_PATH, self.plxTarget.get(), 'lang'])
ifnot path.isdir(folder):
path.mkdir(folder)
buf = '''<?php
$LANG = array(
'''
buf += ",\n\t".join(lines)
buf += '''
);
?>
'''
filename = path.sep.join([folder, lang + '.php'])
print(filename)
with open(filename, 'w', encoding='utf-8') as f:
f.write(buf)
else:
# for core.
context = self.plxTarget.get()
for lang in self.lastTranslations.keys():
filename = path.sep.join([LANGS_PATH, lang, context])
content = ''with open(filename, encoding='utf-8') as f:
content = f.read()
for k in range(len(self.items[lang]['missingKeys'])-1, -1, -1):
key = self.items[lang]['missingKeys'][k]
# Tester si la clé existe déjàif key in self.keys:
value = self.lastTranslations[lang][key].replace("'", "\'")
tabs = "\t" * int((37 - len(key)) / 4)
# we suppose : one key, one line
mask = "'%s'\s*=>.*,?\s$" % key
if re.match(mask, content):
# the key already exists. Replace his value
replace = "'%s'%s=> '%s'," % (key, tabs, value)
content = re.sub(mask, replace, content)
else:
# insert the new key. Look for the next key in the content for inserting before it
i = self.keys.index(key)
nextKeyContent = self.keys[i+1]
replace = "'%s'%s=> '%s',\n'%s'" % (key, tabs, value, nextKeyContent)
content = re.sub("'%s'" % nextKeyContent, replace, content)
with open(filename, 'w', encoding='utf-8') as f:
f.write(content)
self.lastTranslations = {} # re-init
self.items[lang]['updated'] = False
self.saveBtn['state'] = DISABLED
def__init__(self, top):
self.top = top
self.plugins = BooleanVar()
self.plugins.trace('w', self.targetsUpdate)
self.plxTarget = StringVar(name='target')
self.plxTarget.trace('w', self.keysUpdate)
self.plxKey = StringVar(name='currentKey')
self.plxKey.trace('w', self.translationsDisplay)
self.plxLangDefault = StringVar(name='defaultLang')
self.statusbar = StringVar(name='statusbar')
self.lastTranslations = {}
# target dropbox
targetFrame = Frame(top)
Label(targetFrame, text='Target', width=5).pack(side=LEFT)
self.targetOptionMenu = OptionMenu(targetFrame, self.plxTarget, ' ')
self.targetOptionMenu.pack(side=LEFT, fill=X, expand=True)
# select between core or pluginsfor text, value in [('Core', False), ('Plugins', True)]:
Radiobutton(targetFrame,
text=text,
variable=self.plugins,
value=value
).pack(side=LEFT)
targetFrame.pack(fill=X)
# key dropbox
keyFrame = Frame(top)
Label(keyFrame, text='Key', width=5).pack(side=LEFT)
widthBtn = 3
self.keysBtns = {}
for name, caption, command1 in [
('first', '<<', self.firstKey),
('prev', '<', self.prevKey),
('next', '>', self.nextKey),
('last', '>>', self.lastKey),
('empty', '?', self.nextEmptyKey),
('del', 'Del', self.delKey) ,
('new', 'New', self.newKey)
]:
if name == 'next' :
# self.keyOptionMenu = ttk.Combobox(keyFrame, self.plxKey, ' ')
self.keyOptionMenu = ttk.Combobox(
keyFrame, textvariable=self.plxKey,
height=30,
state='readonly'
)
self.keyOptionMenu.pack(side=LEFT, fill=X, expand=True)
btn = Button(keyFrame, text=caption, command=command1, width=widthBtn)
btn.pack(side=LEFT)
self.keysBtns[name] = btn
keyFrame.pack(fill=X)
# translation for each language and default language
items = {}
langs = self.getLangs()
for lang in langs:
f = Frame(top)
v = StringVar(name='text-' + lang)
v.trace('w', self.sentenceUpdate)
Radiobutton(f,
text=lang, variable=self.plxLangDefault, value=lang, width=3,
anchor=W).pack(side=LEFT
)
entry = Entry(f, textvariable=v).pack(side=LEFT, fill=X, expand=True)
f.pack(fill=X)
items[lang] = {
'var' : v,
'translations' : {},
'updated' : False
}
self.langs = langs # For preserving the order of languages and it's faster than self.langs
self.items = items
# toolbox
bottomFrame = Frame(top)
Button(bottomFrame, text='Exit', command=top.destroy).pack(side=LEFT, fill=X, expand=True)
Button(bottomFrame, text='Copy (JSON)', command=self.jsonToClipboard).pack(side=LEFT, fill=X, expand=True)
Button(bottomFrame, text='Translate', command=self.translate).pack(side=LEFT, fill=X, expand=True)
btn = Button(bottomFrame, text='Save', command=self.langsSave)
btn.pack(side=LEFT, fill=X, expand=True)
self.saveBtn = btn
bottomFrame.pack(fill=X)
Label(top, textvariable=self.statusbar, anchor=W, relief="sunken").pack(fill=X)
top.title('Translator for PluXml')
# update and display target list for plugins
self.plxLangDefault.set(DEFAULT_LANG)
self.plugins.set(True)
defmain():
tk = Tk()
tk.minsize(width=800, height=150)
interface = Interface(tk)
tk.mainloop()
if __name__ == '__main__':
main()