Commits

Alexandre Macabies committed e6323d3

Initial commit

Comments (0)

Files changed (14)

+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+LANGUAGES_FROM = (('af', 'afrikaans'), ('sq', 'albanais'), ('de', 'allemand'), ('en', 'anglais'), ('ar', 'arabe'), ('hy', 'arménien'), ('az', 'azéri'), ('eu', 'basque'), ('bn', 'bengali'), ('be', 'biélorusse'), ('bg', 'bulgare'), ('ca', 'catalan'), ('zh-CN', 'chinois'), ('ko', 'coréen'), ('ht', 'créole haïtien'), ('hr', 'croate'), ('da', 'danois'), ('es', 'espagnol'), ('eo', 'espéranto'), ('et', 'estonien'), ('fi', 'finnois'), ('fr', 'français'), ('gl', 'galicien'), ('cy', 'gallois'), ('ka', 'géorgien'), ('el', 'grec'), ('gu', 'gujarati'), ('iw', 'hébreu'), ('hi', 'hindi'), ('hu', 'hongrois'), ('id', 'indonésien'), ('ga', 'irlandais'), ('is', 'islandais'), ('it', 'italien'), ('ja', 'japonais'), ('kn', 'kannada'), ('la', 'latin'), ('lv', 'letton'), ('lt', 'lituanien'), ('mk', 'macédonien'), ('ms', 'malaisien'), ('mt', 'maltais'), ('nl', 'néerlandais'), ('no', 'norvégien'), ('fa', 'persan'), ('pl', 'polonais'), ('pt', 'portugais'), ('ro', 'roumain'), ('ru', 'russe'), ('sr', 'serbe'), ('sk', 'slovaque'), ('sl', 'slovène'), ('sv', 'suédois'), ('sw', 'swahili'), ('tl', 'tagalog'), ('ta', 'tamul'), ('cs', 'tchèque'), ('te', 'telugu'), ('th', 'thaï'), ('tr', 'turc'), ('uk', 'ukrainien'), ('ur', 'urdu'), ('vi', 'vietnamien'), ('yi', 'yiddish'))
+LANGUAGES_TO = (('af', 'afrikaans'), ('sq', 'albanais'), ('de', 'allemand'), ('en', 'anglais'), ('ar', 'arabe'), ('hy', 'arménien'), ('az', 'azéri'), ('eu', 'basque'), ('bn', 'bengali'), ('be', 'biélorusse'), ('bg', 'bulgare'), ('ca', 'catalan'), ('zh-CN', 'chinois (simplifié)'), ('zh-TW', 'chinois (traditionnel)'), ('ko', 'coréen'), ('ht', 'créole haïtien'), ('hr', 'croate'), ('da', 'danois'), ('es', 'espagnol'), ('eo', 'espéranto'), ('et', 'estonien'), ('fi', 'finnois'), ('fr', 'français'), ('gl', 'galicien'), ('cy', 'gallois'), ('ka', 'géorgien'), ('el', 'grec'), ('gu', 'gujarati'), ('iw', 'hébreu'), ('hi', 'hindi'), ('hu', 'hongrois'), ('id', 'indonésien'), ('ga', 'irlandais'), ('is', 'islandais'), ('it', 'italien'), ('ja', 'japonais'), ('kn', 'kannada'), ('la', 'latin'), ('lv', 'letton'), ('lt', 'lituanien'), ('mk', 'macédonien'), ('ms', 'malaisien'), ('mt', 'maltais'), ('nl', 'néerlandais'), ('no', 'norvégien'), ('fa', 'persan'), ('pl', 'polonais'), ('pt', 'portugais'), ('ro', 'roumain'), ('ru', 'russe'), ('sr', 'serbe'), ('sk', 'slovaque'), ('sl', 'slovène'), ('sv', 'suédois'), ('sw', 'swahili'), ('tl', 'tagalog'), ('ta', 'tamul'), ('cs', 'tchèque'), ('te', 'telugu'), ('th', 'thaï'), ('tr', 'turc'), ('uk', 'ukrainien'), ('ur', 'urdu'), ('vi', 'vietnamien'), ('yi', 'yiddish'))
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+from lxml import html
+
+data_to = '''<select id='gt-tl' name='tl' class="jfk-button jfk-button-standard nje" tabindex='0'><option value='af'>afrikaans</option><option value='sq'>albanais</option><option value='de'>allemand</option><option value='en'>anglais</option><option value='ar'>arabe</option><option value='hy'>arménien</option><option value='az'>azéri</option><option value='eu'>basque</option><option value='bn'>bengali</option><option value='be'>biélorusse</option><option value='bg'>bulgare</option><option value='ca'>catalan</option><option value='zh-CN'>chinois (simplifié)</option><option value='zh-TW'>chinois (traditionnel)</option><option value='ko'>coréen</option><option value='ht'>créole haïtien</option><option value='hr'>croate</option><option value='da'>danois</option><option value='es'>espagnol</option><option value='eo'>espéranto</option><option value='et'>estonien</option><option value='fi'>finnois</option><option selected value='fr'>français</option><option value='gl'>galicien</option><option value='cy'>gallois</option><option value='ka'>géorgien</option><option value='el'>grec</option><option value='gu'>gujarati</option><option value='iw'>hébreu</option><option value='hi'>hindi</option><option value='hu'>hongrois</option><option value='id'>indonésien</option><option value='ga'>irlandais</option><option value='is'>islandais</option><option value='it'>italien</option><option value='ja'>japonais</option><option value='kn'>kannada</option><option value='la'>latin</option><option value='lv'>letton</option><option value='lt'>lituanien</option><option value='mk'>macédonien</option><option value='ms'>malaisien</option><option value='mt'>maltais</option><option value='nl'>néerlandais</option><option value='no'>norvégien</option><option value='fa'>persan</option><option value='pl'>polonais</option><option value='pt'>portugais</option><option value='ro'>roumain</option><option value='ru'>russe</option><option value='sr'>serbe</option><option value='sk'>slovaque</option><option value='sl'>slovène</option><option value='sv'>suédois</option><option value='sw'>swahili</option><option value='tl'>tagalog</option><option value='ta'>tamul</option><option value='cs'>tchèque</option><option value='te'>telugu</option><option value='th'>thaï</option><option value='tr'>turc</option><option value='uk'>ukrainien</option><option value='ur'>urdu</option><option value='vi'>vietnamien</option><option value='yi'>yiddish</option></select>'''
+data_from = '''<select id='gt-sl' name='sl' class="jfk-button jfk-button-standard nje" tabindex='0'><option value='auto'>Détecter la langue</option><option value='separator' disabled>&#8212;</option><option value='af'>afrikaans</option><option value='sq'>albanais</option><option value='de'>allemand</option><option selected value='en'>anglais</option><option value='ar'>arabe</option><option value='hy'>arménien</option><option value='az'>azéri</option><option value='eu'>basque</option><option value='bn'>bengali</option><option value='be'>biélorusse</option><option value='bg'>bulgare</option><option value='ca'>catalan</option><option value='zh-CN'>chinois</option><option value='ko'>coréen</option><option value='ht'>créole haïtien</option><option value='hr'>croate</option><option value='da'>danois</option><option value='es'>espagnol</option><option value='eo'>espéranto</option><option value='et'>estonien</option><option value='fi'>finnois</option><option value='fr'>français</option><option value='gl'>galicien</option><option value='cy'>gallois</option><option value='ka'>géorgien</option><option value='el'>grec</option><option value='gu'>gujarati</option><option value='iw'>hébreu</option><option value='hi'>hindi</option><option value='hu'>hongrois</option><option value='id'>indonésien</option><option value='ga'>irlandais</option><option value='is'>islandais</option><option value='it'>italien</option><option value='ja'>japonais</option><option value='kn'>kannada</option><option value='la'>latin</option><option value='lv'>letton</option><option value='lt'>lituanien</option><option value='mk'>macédonien</option><option value='ms'>malaisien</option><option value='mt'>maltais</option><option value='nl'>néerlandais</option><option value='no'>norvégien</option><option value='fa'>persan</option><option value='pl'>polonais</option><option value='pt'>portugais</option><option value='ro'>roumain</option><option value='ru'>russe</option><option value='sr'>serbe</option><option value='sk'>slovaque</option><option value='sl'>slovène</option><option value='sv'>suédois</option><option value='sw'>swahili</option><option value='tl'>tagalog</option><option value='ta'>tamul</option><option value='cs'>tchèque</option><option value='te'>telugu</option><option value='th'>thaï</option><option value='tr'>turc</option><option value='uk'>ukrainien</option><option value='ur'>urdu</option><option value='vi'>vietnamien</option><option value='yi'>yiddish</option></select>'''
+
+def parse(data):
+    data = html.fromstring(data)
+    for opt in data.findall('option'):
+        code = opt.get('value')
+        name = opt.text.strip()
+        if code in ('auto', 'separator'):
+            continue
+        yield code, name
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+from PySide import QtCore, QtGui, QtUiTools
+from const import LANGUAGES_FROM, LANGUAGES_TO
+from widgets import LoadingSpinner, TicksSpinner
+from translate import TranslateService
+import logging
+import resrc
+
+def setup_logger():
+    log = logging.getLogger('copytrans')
+    handler = logging.StreamHandler()
+    frmt = logging.Formatter('[%(name)s] %(asctime)s %(levelname)s -- %(message)s', datefmt='%H:%M:%S')
+    handler.setFormatter(frmt)
+    log.addHandler(handler)
+    return log
+
+log = setup_logger()
+log.setLevel(logging.INFO)
+
+class MainWidget(QtGui.QWidget):
+    def __init__(self):
+        super(MainWidget, self).__init__()
+        loader = QtUiTools.QUiLoader()
+        file = QtCore.QFile(":/main.ui")
+        file.open(QtCore.QFile.ReadOnly)
+        self.ui = loader.load(file, self)
+        file.close()
+
+        layout = QtGui.QVBoxLayout()
+        layout.addWidget(self.ui)
+        layout.setContentsMargins(4, 4, 4, 4)
+        self.setLayout(layout)
+        self.setWindowTitle("Copytrans")
+
+        self.ui.o_text.setAcceptRichText(False)
+
+        # vars
+        self.latest_data = self.getData()
+        self.need_update = True
+
+        # misc obj
+        self.settings = QtCore.QSettings(QtCore.QSettings.IniFormat, QtCore.QSettings.UserScope, "ZopiSoftware", "Copytrans", self)
+        self.clipboard = QtGui.QApplication.clipboard()
+        self.translate = TranslateService(self)
+
+        self.load_spinner = LoadingSpinner(TicksSpinner(size=64, color=QtCore.Qt.black, ticks=21, innerRadius=18), 10, .7, self.ui.t_text)
+
+        self.update_timer = QtCore.QTimer()
+        self.update_timer.setInterval(1500)
+        self.update_timer.setSingleShot(False)
+
+        # signals
+        self.update_timer.timeout.connect(self.updateNow)
+        self.ui.lng_from.currentIndexChanged.connect(self.needUpdate)
+        self.ui.lng_to.currentIndexChanged.connect(self.needUpdate)
+        self.ui.o_text.textChanged.connect(self.needUpdate)
+        self.ui.lng_switch.clicked.connect(self.switchLanguages)
+
+        self.ui.o_clear.clicked.connect(self.ui.o_text.clear)
+        self.ui.o_copy.clicked.connect(self.copyOriginal)
+        self.ui.o_paste.clicked.connect(self.pasteOriginal)
+        self.ui.t_copy.clicked.connect(self.copyTranslated)
+
+        self.ui.font_size.valueChanged.connect(self.updateFontSize)
+
+        # inits
+        log.debug('Init')
+        self.setupLanguageSelect()
+        self.readSettings()
+        self.update_timer.start()
+        self.translate.csi_load() # hello to gstatic
+
+#        self.selectItemByData(self.ui.lng_from, 'fr')
+#        self.selectItemByData(self.ui.lng_to, 'en')
+
+    def readSettings(self):
+        self.settings.beginGroup("main")
+#        self.resize(self.settings.value("size", QSize(400, 400)).toSize())
+#        self.move(self.settings.value("pos", QPoint(200, 200)).toPoint())
+        self.selectItemByData(self.ui.lng_from, self.settings.value("lng_from", 'fr'))
+        self.selectItemByData(self.ui.lng_to, self.settings.value("lng_to", 'en'))
+        self.ui.font_size.setValue(int(self.settings.value("font_size", self.ui.o_text.font().pointSize())))
+        self.settings.endGroup()
+
+    def writeSettings(self):
+        self.settings.beginGroup("main")
+        lng_from, lng_to, _ = self.getData()
+        self.settings.setValue("lng_from", lng_from)
+        self.settings.setValue("lng_to", lng_to)
+        self.settings.setValue("font_size", self.ui.font_size.value())
+        self.settings.endGroup()
+
+    def closeEvent(self, e):
+        self.writeSettings()
+        QtGui.QWidget.closeEvent(self, e)
+
+    def selectItemByData(self, input, data):
+        input.setCurrentIndex(input.findData(data))
+
+    def setupLanguageSelect(self):
+        def fill(editor, items):
+            editor.clear()
+            for code, name in items:
+                editor.addItem(name, userData=code)
+
+        fill(self.ui.lng_from, LANGUAGES_FROM)
+        fill(self.ui.lng_to, LANGUAGES_TO)
+
+    def getData(self):
+        lng_from = self.ui.lng_from.itemData(self.ui.lng_from.currentIndex())
+        lng_to = self.ui.lng_to.itemData(self.ui.lng_to.currentIndex())
+        original = self.ui.o_text.toPlainText().strip()
+        return lng_from, lng_to, original
+
+    def copyOriginal(self):
+        text = self.ui.o_text.toPlainText().strip()
+        if text:
+            self.clipboard.setText(text)
+
+    def copyTranslated(self):
+        text = self.ui.t_text.toPlainText().strip()
+        if text:
+            self.clipboard.setText(text)
+
+    def pasteOriginal(self):
+        content = self.clipboard.text().strip()
+        if content:
+            self.ui.o_text.setPlainText(content)
+
+    def switchLanguages(self):
+        lfrom, lto, _ = self.getData()
+        self.selectItemByData(self.ui.lng_from, lto)
+        self.selectItemByData(self.ui.lng_to, lfrom)
+        self.needUpdate()
+
+    def needUpdate(self, *args):
+        self.need_update = True
+        self.load_spinner.fadeIn()
+
+    def updateNow(self):
+        if not self.need_update:
+            self.load_spinner.fadeOut()
+            return
+        self.need_update = False
+
+        new_data = self.getData()
+        if new_data == self.latest_data:
+            self.load_spinner.fadeOut()
+            return
+        lng_from, lng_to, original = new_data
+        if not original: # original is empty, clean result, exit
+            self.ui.t_text.clear()
+            self.load_spinner.fadeOut()
+            return
+
+        log.debug('Updating now')
+        log.debug('Translating from %s to %s', lng_from, lng_to)
+        self.translate.get_translation(lng_from, lng_to, original, self.translationResult)
+
+    def translationResult(self, data, reply, _):
+        log.debug('Translation arrived')
+        translated = ""
+        was_n_before = True
+        for piece in data[4]:
+            translated += (' ' if piece[2] == 1 and not was_n_before else '') + piece[0]
+            was_n_before = piece[0].endswith('\n')
+
+        self.ui.t_text.setPlainText(translated.strip())
+        self.load_spinner.fadeOut()
+
+    def updateFontSize(self, value):
+        for input in (self.ui.o_text, self.ui.t_text):
+            f = input.font()
+            f.setPointSize(value)
+            input.setFont(f)
+
+def main():
+    import sys
+    app = QtGui.QApplication(sys.argv)
+    file = QtCore.QFile("res/style.css")
+    file.open(QtCore.QFile.ReadOnly)
+    app.setStyleSheet(str(file.readAll()))
+    file.close()
+    mainw = MainWidget()
+    mainw.show()
+    sys.exit(app.exec_())
+
+if __name__ == '__main__':
+    main()

res/clear.png

Added
New image

res/copy.png

Added
New image
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>Form</class>
+ <widget class="QWidget" name="Form">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>569</width>
+    <height>441</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Copytrans</string>
+  </property>
+  <layout class="QHBoxLayout" name="horizontalLayout_4">
+   <item>
+    <widget class="QSplitter" name="splitter">
+     <property name="orientation">
+      <enum>Qt::Vertical</enum>
+     </property>
+     <widget class="QWidget" name="layoutWidget">
+      <layout class="QVBoxLayout" name="verticalLayout">
+       <item>
+        <layout class="QHBoxLayout" name="horizontalLayout" stretch="1,0,0,0">
+         <item>
+          <widget class="QLabel" name="label">
+           <property name="text">
+            <string>Original</string>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QPushButton" name="o_clear">
+           <property name="text">
+            <string>Effacer</string>
+           </property>
+           <property name="icon">
+            <iconset resource="res.qrc">
+             <normaloff>:/img/clear.png</normaloff>:/img/clear.png</iconset>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QPushButton" name="o_copy">
+           <property name="text">
+            <string>Copier</string>
+           </property>
+           <property name="icon">
+            <iconset resource="res.qrc">
+             <normaloff>:/img/copy.png</normaloff>:/img/copy.png</iconset>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QPushButton" name="o_paste">
+           <property name="text">
+            <string>Coller</string>
+           </property>
+           <property name="icon">
+            <iconset resource="res.qrc">
+             <normaloff>:/img/paste.png</normaloff>:/img/paste.png</iconset>
+           </property>
+          </widget>
+         </item>
+        </layout>
+       </item>
+       <item>
+        <widget class="QTextEdit" name="o_text">
+         <property name="textInteractionFlags">
+          <set>Qt::LinksAccessibleByMouse|Qt::TextEditable|Qt::TextEditorInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+         </property>
+        </widget>
+       </item>
+      </layout>
+     </widget>
+     <widget class="QWidget" name="layoutWidget">
+      <layout class="QVBoxLayout" name="verticalLayout_2">
+       <item>
+        <layout class="QHBoxLayout" name="horizontalLayout_2" stretch="1,0">
+         <item>
+          <widget class="QLabel" name="label_2">
+           <property name="text">
+            <string>Traduit</string>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QPushButton" name="t_copy">
+           <property name="text">
+            <string>Copier</string>
+           </property>
+           <property name="icon">
+            <iconset resource="res.qrc">
+             <normaloff>:/img/copy.png</normaloff>:/img/copy.png</iconset>
+           </property>
+          </widget>
+         </item>
+        </layout>
+       </item>
+       <item>
+        <widget class="QTextEdit" name="t_text">
+         <property name="undoRedoEnabled">
+          <bool>false</bool>
+         </property>
+         <property name="readOnly">
+          <bool>true</bool>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <layout class="QHBoxLayout" name="horizontalLayout_3">
+         <item>
+          <widget class="QComboBox" name="lng_from"/>
+         </item>
+         <item>
+          <widget class="QPushButton" name="lng_switch">
+           <property name="icon">
+            <iconset resource="res.qrc">
+             <normaloff>:/img/switch.png</normaloff>:/img/switch.png</iconset>
+           </property>
+           <property name="flat">
+            <bool>true</bool>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QComboBox" name="lng_to"/>
+         </item>
+         <item>
+          <spacer name="horizontalSpacer">
+           <property name="orientation">
+            <enum>Qt::Horizontal</enum>
+           </property>
+           <property name="sizeHint" stdset="0">
+            <size>
+             <width>40</width>
+             <height>20</height>
+            </size>
+           </property>
+          </spacer>
+         </item>
+         <item>
+          <widget class="QLabel" name="label_3">
+           <property name="text">
+            <string>Taille</string>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QDial" name="font_size">
+           <property name="maximumSize">
+            <size>
+             <width>30</width>
+             <height>29</height>
+            </size>
+           </property>
+           <property name="minimum">
+            <number>8</number>
+           </property>
+           <property name="maximum">
+            <number>72</number>
+           </property>
+          </widget>
+         </item>
+        </layout>
+       </item>
+      </layout>
+     </widget>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources>
+  <include location="res.qrc"/>
+ </resources>
+ <connections/>
+</ui>

res/paste.png

Added
New image
+<!DOCTYPE RCC><RCC version="1.0">
+<qresource>
+    <file>main.ui</file>
+    <file>style.css</file>
+</qresource>
+<qresource prefix="img">
+    <file>clear.png</file>
+    <file>copy.png</file>
+    <file>paste.png</file>
+    <file>switch.png</file>
+</qresource>
+</RCC>
+/*
+QTextEdit, QPlainTextEdit {
+    font-size: 16px;
+}
+*/

res/switch.png

Added
New image
+# -*- coding: utf-8 -*-
+
+# Resource object code
+#
+# Created: dim. 8. avr. 00:56:11 2012
+#      by: The Resource Compiler for PySide (Qt v4.7.4)
+#
+# WARNING! All changes made in this file will be lost!
+
+from PySide import QtCore
+
+qt_resource_data = b"\x00\x00\x15\x8a<?xml version=\x221.0\x22 encoding=\x22UTF-8\x22?>\x0d\x0a<ui version=\x224.0\x22>\x0d\x0a <class>Form</class>\x0d\x0a <widget class=\x22QWidget\x22 name=\x22Form\x22>\x0d\x0a  <property name=\x22geometry\x22>\x0d\x0a   <rect>\x0d\x0a    <x>0</x>\x0d\x0a    <y>0</y>\x0d\x0a    <width>569</width>\x0d\x0a    <height>441</height>\x0d\x0a   </rect>\x0d\x0a  </property>\x0d\x0a  <property name=\x22windowTitle\x22>\x0d\x0a   <string>Copytrans</string>\x0d\x0a  </property>\x0d\x0a  <layout class=\x22QHBoxLayout\x22 name=\x22horizontalLayout_4\x22>\x0d\x0a   <item>\x0d\x0a    <widget class=\x22QSplitter\x22 name=\x22splitter\x22>\x0d\x0a     <property name=\x22orientation\x22>\x0d\x0a      <enum>Qt::Vertical</enum>\x0d\x0a     </property>\x0d\x0a     <widget class=\x22QWidget\x22 name=\x22layoutWidget\x22>\x0d\x0a      <layout class=\x22QVBoxLayout\x22 name=\x22verticalLayout\x22>\x0d\x0a       <item>\x0d\x0a        <layout class=\x22QHBoxLayout\x22 name=\x22horizontalLayout\x22 stretch=\x221,0,0,0\x22>\x0d\x0a         <item>\x0d\x0a          <widget class=\x22QLabel\x22 name=\x22label\x22>\x0d\x0a           <property name=\x22text\x22>\x0d\x0a            <string>Original</string>\x0d\x0a           </property>\x0d\x0a          </widget>\x0d\x0a         </item>\x0d\x0a         <item>\x0d\x0a          <widget class=\x22QPushButton\x22 name=\x22o_clear\x22>\x0d\x0a           <property name=\x22text\x22>\x0d\x0a            <string>Effacer</string>\x0d\x0a           </property>\x0d\x0a           <property name=\x22icon\x22>\x0d\x0a            <iconset resource=\x22res.qrc\x22>\x0d\x0a             <normaloff>:/img/clear.png</normaloff>:/img/clear.png</iconset>\x0d\x0a           </property>\x0d\x0a          </widget>\x0d\x0a         </item>\x0d\x0a         <item>\x0d\x0a          <widget class=\x22QPushButton\x22 name=\x22o_copy\x22>\x0d\x0a           <property name=\x22text\x22>\x0d\x0a            <string>Copier</string>\x0d\x0a           </property>\x0d\x0a           <property name=\x22icon\x22>\x0d\x0a            <iconset resource=\x22res.qrc\x22>\x0d\x0a             <normaloff>:/img/copy.png</normaloff>:/img/copy.png</iconset>\x0d\x0a           </property>\x0d\x0a          </widget>\x0d\x0a         </item>\x0d\x0a         <item>\x0d\x0a          <widget class=\x22QPushButton\x22 name=\x22o_paste\x22>\x0d\x0a           <property name=\x22text\x22>\x0d\x0a            <string>Coller</string>\x0d\x0a           </property>\x0d\x0a           <property name=\x22icon\x22>\x0d\x0a            <iconset resource=\x22res.qrc\x22>\x0d\x0a             <normaloff>:/img/paste.png</normaloff>:/img/paste.png</iconset>\x0d\x0a           </property>\x0d\x0a          </widget>\x0d\x0a         </item>\x0d\x0a        </layout>\x0d\x0a       </item>\x0d\x0a       <item>\x0d\x0a        <widget class=\x22QTextEdit\x22 name=\x22o_text\x22>\x0d\x0a         <property name=\x22textInteractionFlags\x22>\x0d\x0a          <set>Qt::LinksAccessibleByMouse|Qt::TextEditable|Qt::TextEditorInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>\x0d\x0a         </property>\x0d\x0a        </widget>\x0d\x0a       </item>\x0d\x0a      </layout>\x0d\x0a     </widget>\x0d\x0a     <widget class=\x22QWidget\x22 name=\x22layoutWidget\x22>\x0d\x0a      <layout class=\x22QVBoxLayout\x22 name=\x22verticalLayout_2\x22>\x0d\x0a       <item>\x0d\x0a        <layout class=\x22QHBoxLayout\x22 name=\x22horizontalLayout_2\x22 stretch=\x221,0\x22>\x0d\x0a         <item>\x0d\x0a          <widget class=\x22QLabel\x22 name=\x22label_2\x22>\x0d\x0a           <property name=\x22text\x22>\x0d\x0a            <string>Traduit</string>\x0d\x0a           </property>\x0d\x0a          </widget>\x0d\x0a         </item>\x0d\x0a         <item>\x0d\x0a          <widget class=\x22QPushButton\x22 name=\x22t_copy\x22>\x0d\x0a           <property name=\x22text\x22>\x0d\x0a            <string>Copier</string>\x0d\x0a           </property>\x0d\x0a           <property name=\x22icon\x22>\x0d\x0a            <iconset resource=\x22res.qrc\x22>\x0d\x0a             <normaloff>:/img/copy.png</normaloff>:/img/copy.png</iconset>\x0d\x0a           </property>\x0d\x0a          </widget>\x0d\x0a         </item>\x0d\x0a        </layout>\x0d\x0a       </item>\x0d\x0a       <item>\x0d\x0a        <widget class=\x22QTextEdit\x22 name=\x22t_text\x22>\x0d\x0a         <property name=\x22undoRedoEnabled\x22>\x0d\x0a          <bool>false</bool>\x0d\x0a         </property>\x0d\x0a         <property name=\x22readOnly\x22>\x0d\x0a          <bool>true</bool>\x0d\x0a         </property>\x0d\x0a        </widget>\x0d\x0a       </item>\x0d\x0a       <item>\x0d\x0a        <layout class=\x22QHBoxLayout\x22 name=\x22horizontalLayout_3\x22>\x0d\x0a         <item>\x0d\x0a          <widget class=\x22QComboBox\x22 name=\x22lng_from\x22/>\x0d\x0a         </item>\x0d\x0a         <item>\x0d\x0a          <widget class=\x22QPushButton\x22 name=\x22lng_switch\x22>\x0d\x0a           <property name=\x22icon\x22>\x0d\x0a            <iconset resource=\x22res.qrc\x22>\x0d\x0a             <normaloff>:/img/switch.png</normaloff>:/img/switch.png</iconset>\x0d\x0a           </property>\x0d\x0a           <property name=\x22flat\x22>\x0d\x0a            <bool>true</bool>\x0d\x0a           </property>\x0d\x0a          </widget>\x0d\x0a         </item>\x0d\x0a         <item>\x0d\x0a          <widget class=\x22QComboBox\x22 name=\x22lng_to\x22/>\x0d\x0a         </item>\x0d\x0a         <item>\x0d\x0a          <spacer name=\x22horizontalSpacer\x22>\x0d\x0a           <property name=\x22orientation\x22>\x0d\x0a            <enum>Qt::Horizontal</enum>\x0d\x0a           </property>\x0d\x0a           <property name=\x22sizeHint\x22 stdset=\x220\x22>\x0d\x0a            <size>\x0d\x0a             <width>40</width>\x0d\x0a             <height>20</height>\x0d\x0a            </size>\x0d\x0a           </property>\x0d\x0a          </spacer>\x0d\x0a         </item>\x0d\x0a         <item>\x0d\x0a          <widget class=\x22QLabel\x22 name=\x22label_3\x22>\x0d\x0a           <property name=\x22text\x22>\x0d\x0a            <string>Taille</string>\x0d\x0a           </property>\x0d\x0a          </widget>\x0d\x0a         </item>\x0d\x0a         <item>\x0d\x0a          <widget class=\x22QDial\x22 name=\x22font_size\x22>\x0d\x0a           <property name=\x22maximumSize\x22>\x0d\x0a            <size>\x0d\x0a             <width>30</width>\x0d\x0a             <height>29</height>\x0d\x0a            </size>\x0d\x0a           </property>\x0d\x0a           <property name=\x22minimum\x22>\x0d\x0a            <number>8</number>\x0d\x0a           </property>\x0d\x0a           <property name=\x22maximum\x22>\x0d\x0a            <number>72</number>\x0d\x0a           </property>\x0d\x0a          </widget>\x0d\x0a         </item>\x0d\x0a        </layout>\x0d\x0a       </item>\x0d\x0a      </layout>\x0d\x0a     </widget>\x0d\x0a    </widget>\x0d\x0a   </item>\x0d\x0a  </layout>\x0d\x0a </widget>\x0d\x0a <resources>\x0d\x0a  <include location=\x22res.qrc\x22/>\x0d\x0a </resources>\x0d\x0a <connections/>\x0d\x0a</ui>\x0d\x0a\x00\x00\x00</*\x0d\x0aQTextEdit, QPlainTextEdit {\x0d\x0a    font-size: 16px;\x0d\x0a}\x0d\x0a*/\x00\x00\x02\x8a\x89PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\x00\x00\x10\x00\x00\x00\x10\x08\x06\x00\x00\x00\x1f\xf3\xffa\x00\x00\x00\x19tEXtSoftware\x00Adobe ImageReadyq\xc9e<\x00\x00\x02,IDATx\xda\xc4S;k\x14Q\x14>s\xe7\xb1\x0fIV\x11\xb6\x98\xa4qq\x89\x83\x12\x8b\x891Z\x9af\x05\xc1g-\xa2h\xd0\xbf\xe0\x16\x16\xdb\xa6X,d\x036\x82\xc5\x0ab\x13a\x9b\x94)\x0c;\x0aK\x0c\xba\xc8\x06\x85\x88:>v\xb3;\xaf\x9d\x97\xe7\x5c3S\xd8X\xa4\xf0\xc07s\xcf\x99\xef|\xf7\xdcs\xcf\x08q\x1c\xc3~\x8c\xc1>M:\xfd\xb0\x07r&\x03\x92,W\xd0\xaf!\xaaQ\x14\xb5\x18c\x0d\x5c\xeb\x7f\xf1}\xc4&\xe2\x01V\xbe\xc3\x05x8\x8e+\x81\xef\xd7\xae_\x98\xd2\x9f\xac\xee\xd4\xd0\x87\x98\xb1\x15$5\xae\x9dS\xf5\xe4\x94\xb2\x10G\xdd\xcf\xf6\x91\xd7\xef\x06\xe4\xde\xa6\x870_\xffP\x01A\xa8\xdd\xbaZJw{\xfc\xbcg\xa0H\x95\x89\xa2\x89\xd54\x92\xf8)\xad8SRs\xf9\xe6\xda\xc7\x0dt\xcf\xf0\x1e\xf8\xe3\xf1\xe5\xb1\xeb\xc2\xa3\xa7[\x86i\xfa\xfcM>\xc5\x83 0\xe2(\x9a#\x84A\xb0\xb4\xfe\xe6\xd3\xfb\xa1\x132\xfc.\x13\x87 a\x99K\xa1\xef\x03\x92\xdb\xae\xeb\x83\xe78 I\xd2\x9c(\xcb\xe9\xc11\x19\x1c\xcb2hm\xdb\x1e\xe7\xa4Mtm\x0b\xc6\x8e\xcb\x1dg8\x02\xcf\xb6\xc1\xc3\xb5\x92\xcbB\x7f{\x1b\xf2\x93\x93|'%\x9b\xe5\x1ck\xb0\xcb9d\xbc\x82\xe1\xcf_\xa9\xda\x08\x95\x13\xf5\x81i\x026\x96v\x06YQ\xe8HtS\xb0k\xfd\xe1\xd07\xaa\x5cJ\xd4xR,ru,Y\x8f\xc2\xd0H\xe2t\x1bH\xd6)\xa1\x8f\x17\xb7\x97\xd3\xc6\xb8\xc1\xfa\xab\xf7\xb9\x22\xc2\xf72\xb9H9ql\x06\xd5\x1b\xd8\xfd6\x81\x12}\xcf{A\x9c+7\x17\xf5L\xf1\x10\x5c\xbc\xb1\xa8\x93Oq1\x1a\x99\x10~}\x0b0\xbd\xb0 NLL\xab\xc7K\x07\xf3\xb3\xdaT\xe1\xa4\xa6\x16f5\xf5{\xbb\xa3c\x83\x9b\xc1\x97\xadW\x9d\xce7\xadxVW_\xd6\x9b\x06\xfaUoc\xa5%P\xbf\x10\x05\xe1p\xf9(\x9b\xbf{\x0f\xe4\x03\xe5t\xc0\x12\xfb\xd1\xed\x86\xeb\xcb\xcbL\xbb\xa4\x0b\xe5\xf3w\xa0\xb7V\x0f7\x9f\xb5\xe8\xd4$ \x22\xa8\xc59\x1a6\x1a\xae\x7f\x8c\x7f\xbc7\xd2\xd4mW\xf8\xef\x7f\xe3o\x01\x06\x00,\x91E\x85\xca\xfd\x0au\x00\x00\x00\x00IEND\xaeB`\x82\x00\x00\x02^\x89PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\x00\x00\x10\x00\x00\x00\x10\x08\x06\x00\x00\x00\x1f\xf3\xffa\x00\x00\x00\x19tEXtSoftware\x00Adobe ImageReadyq\xc9e<\x00\x00\x02\x00IDATx\xda\x8c\x93\xbfk\x14A\x1c\xc5\xdf\xcc\xed]\x90\x035E\x10\xe3\x05\x1b\x03b `\xa3\x11\xd2\x04\x85\x83\x83\x98F\xb0\xb0W\xfc\x1b\x84\x10H#\xd8\xca\xa5\x16\x09\xd8X\xda\xa4\x90\x10$\xa9\x22{\xc5%\x84\xfc*.\x9dd\xef\x12oc\xf6\xf6\x97\xef\xbb\xee,\x9b\xe5\x0a\xbf\xf0\x18fv\xbfo\xdegvV\xbdX\x5c\x84Rj\x1e\xc0m\x0c\xaf\xefa\x18\x1eDa\x88r\xa5\x92,x\xed\x1f\x08v\xb7\xb0\xff\xcb\x87\x15E\x91\xac\xdd\xf9\xbc\xb4\xb4\xe2\x05\x01\x06\x9c\xfb|9\x8ac\xd8\xb6\x8dOkko\xf9\x5cQ\xfb\xc6\xd1\xba\xff\x04z\xf2\x11\xc6~nB\x8b3\xa5b6F4\x88\x06\x03\x0c./\xe1\xba.\xfa\xfd>\xbe./\xaf\xf0\xf93\xf6M\x16\xa3\x8d=\xa4Q\xc0&\x8a\x14\x0a%\xaabY8\xeat\xd0\xda\xdbC\xfb\xf8\x18\xef\x9aM<\xa8\xd5\x9ag\xdd\xee\x07\xf6\xbc\xa6\xee\xe5M,1`i\xcdf\xad5\xc0$\xbf\xb9\xfb\xf3\xd9Y\xd4gf2\xa47q\xbc@\xa4\x85\x22\x92\x0e|\x1f\x94\xb4&\x09\x12\x13\xf2\xff/Rf\xa0\xd2f1\x91\x03\xcc#]+\x971\xc2\xd1q\x1c|\x5c]\xc5\xd3\xa9\xa9f\xdb\xb6_\xee\xb4ZD\xa0A\x86@!\xc5\xc8#\x89\x89T\xa3^\x87|\xa9j\xb5\x8ao\xdb\xdb\xbd\xe4\x0c\xfc\xd4@\xa5\x08R\xf2i\x0d\x12\x0a&\xffv\xd3I\xea\xab\x06|9\x89\x9d\x1a\xa8\x5c\x82\xa2\xc9U\x03\xcf\xcb\x12Hl\xb9Vr7\x8aHy\x93R\xa9\x94\x19\xe8\x83\xf5u\xf4\x1cgD&fW^]\x18$\x9d;\x5c3\x8a\x98\x5cKz\xebO\xb7{\xd7999\xbf>=\xfd\xde0\xde\x1a\x1f\x7f\xfc\xaa\xd1\x983Hi\xee,\x89l\xed\x1b\x04\xea\xcc=<\xfc\x92^\x8e\xa4j\x13\x131's\x06\xa9h\x22\xebD\xd7\xee\xe9ib\xd0Ke\xeaf\xdfu\xbd\x0c\x89\x8a\xe5^\xc8\xd9\xc8\x05\x13\x13J\xb0;\x1b\x1b\xa3\xd6\x90\xdf\xf7F\x11iXE\x17\x17\xe7\x1cF\xff\x0a0\x00/\x1c\x22<\xdf\xc8\x0e%\x00\x00\x00\x00IEND\xaeB`\x82\x00\x00\x02L\x89PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\x00\x00\x10\x00\x00\x00\x10\x08\x06\x00\x00\x00\x1f\xf3\xffa\x00\x00\x00\x19tEXtSoftware\x00Adobe ImageReadyq\xc9e<\x00\x00\x01\xeeIDATx\xda\xa4\x93\xcfJ\x1bQ\x14\xc6\xbf\x99\xb9\xb9\xd4\x8c\x96\xa2\xa6\x0bM\xb56J\xc4M@T\x88\xb8\xc8\xcem\x17}\x07\xa1\xef\xe2\x03t\xe7BD\xc1E\xb6E(\x0aB\x02]u\xe3`\x89\xa8\xb4teCI&\x93\xc9d\xfe\xf4\x9c\x9b\xdc\x10\xad\x12\xa4\x03\xdf\xcc=\xe7\xe6\xf7\x9d3gn\x8c$I\xf0?\x9782\x0c\x18\xc08i\x87\xe2m\xd2\xe4\x08\xa6N\xfaLe?\x91\x5c\xc1\x19Z\xec\xcc\xad\xac|\x5c.\x16\xb32\x9d\x96O\x91\xdcm\xe0y\x81S\xa9\xe4~8\x0e\xa7vE\xd4\xdb\xdb^\xda\xd8\xc8\xba\xbe/\xe1\xfb\xa3\xba\x96\xefVW\xb37\x8e\xc3\xdd\xee\x9al@\x9a\x8cMS\xba\xae\x0bVv\x7f\x1fz\xfdX\xae\x9b$\x92\x19f\xcd\x90n,\x9f*7\x9bM,\x97\xcb\xaa\x0c?9~,\xc7&\x9a\x13a\xbf/\xcf\xf3\xd0j\xb5p\xbc\xb8\x88\x0f\xb5\x9a2\x9c;8P\xf9\x1a\xc5\xa6i\xe2\xcb\xda\x1aR\xa9\x142\x99\x0c470\x08\x82\x00a\x18b\xc2\xb6\xb17=\x8d\xc2\xc9\x09,\xcbR\x80\x94\x12g\x04\xab\xe9v\xbbh7\x1a\xff\x1a0\x1cP{^\xbd\x0e;\x8e\xef\xc1\xac\xf1\xe1oO&\x9a3\xbblJ\xea\x10\x98\xdc\xdda,\x8a\x90?=\xbd\x07\x0b!P\xba\xbcD\x9a~\xc7\x92TLs\x83!\x1ad\x90\xa6\xef\xfcf\x08\xfe\xb6\xbe\x8e\xaf\x85\x822`m^])\x83\x17\xd4\xa1\xe6\x06\x1dH\x9a\x01o\xde\x96J\x0a\xbe x\x96bV5\x9fW\xed\x9e\xe7r\xbd.\xa8\x90\xe6D\xa7\x7f<#\x9a\xe2\x18\xf9\xccS\xf0\x9d\xe0\xf9\xa1w\xe6u\x95`\x9dk\xc7q\xd0\xe9\x1diX\xd7t{\x0b\xbcnGQ~F\x08\xdb6\x0c\x8b\xff\x0c\xe2\x81t\x8e\xe1\xaa\xef\xff\xfc\x15E\x87\x87@\xc5\xe0\xbd\x97\xc0\xc2\x16\xf0~\x0a(R\xfcj\xc4Q\xfe\xf3\x9b\xc0s\xa0\xdc\x00\xae\xd9\x80:\xc7D\xbf\xc0s.\x9ea\xf3\xaf\x00\x03\x00\xf6\xf4\xee\x1b\xb3\x12\x9f\xd1\x00\x00\x00\x00IEND\xaeB`\x82\x00\x00\x02\xb5\x89PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\x00\x00\x10\x00\x00\x00\x10\x08\x06\x00\x00\x00\x1f\xf3\xffa\x00\x00\x00\x19tEXtSoftware\x00Adobe ImageReadyq\xc9e<\x00\x00\x02WIDATx\xda\x8cSKk\x13Q\x14\xfe\xe6\x91\xc9\xc4G\x13\x04c\x8b\xa5\x18\xa4\x01\x11D\x88\x16W\xedB\xb4\x9b\x22X\xb0\x0b\x97\x05\xd1\x95\xe0^\xfc\x0d\xf5\x81U\x17\xa2\x0b]\xd5\x8d\x94\x82 hu\xa1\x0b]\x95\xd2\xa1\xa5\xc4\x16\xb5\xb1\xc2\xb4\xcdL\xe7\x95yx\xce\xcdk\xb2\xf3\x84/\xf7\xdc\xb9\xf7\xfb\xce7\xe7\xde\x91\x1e\x8dA\x84$\xa1\x13\xd5\xdc)\xed\xc7\xc1\xd3\x13\x94\x1em=\xfa{b\x7fy\xbe\xe4\xae\x04\xed=I\xd2\x1c\xd5.\x0dO\x09\x15N\x8e\xd4V\xd43\xe3\x97\x8a\x17\xa6o\x0f\xf0\xfc\xeb\xf3\x07[?\xdf\xcd\xddC\x1eak\xefw\xc2MQ\xf8\xe1XG\xe0\xdb\xe5\xa9\xe9\x8aa\x18(^\x7f\x02]\xd7\x90\xcf\x1f\x12\x0b{{6</\xc0\xf6\xeb[(\x97\xcbx?\xf7\x82\x05\xce\x09\x07Q\xdc\xb5\x10\x04\x01\xea\xf5:\xb6g&\xa1i\x9a\x80,\xcbh4\x1ab-\x9b\xcd\x8a1\xcdQ\xa3\xa8;\x89hR*\x95`Y\x96\x98\x9b\xf9!\xf8Z\xd3\x85LPt\x1d\xeb$\x1a\x16\x06\xb3\xde\xc9\x11Da\xd4\xeb\x80\x05\xb8\x9aN\x1b9\xe2\x03yL\xdd\x99AH\xcf\x838\x86\x1f\x86X\xafV\xb1\xbc\xb4\xd4\xe7V\xae\x0e\x93\x9b5\x99\x1d\xb4\xc1\xe44\x82\xa0\x81\x84\x88!\xe5\x09!&\xfb\xb6m\xe3\xee\xe3\xf9!\x22_L\x92dX\x0e\xc9A\x07T\xa1Mv]\x17\x16m\x96\xe9|\x15\xea\x03C\xcfd`\x9a&^-,\xe0l\x7f\xff\xec\xa7\xc5\xc5\x095L\xf5\x80\x89\x9e\xe7\xc1q\x1c\xf8\xbe\x0f[*v\x04\xda1>:\x0a\x8f\xf6\xc9\xd4\xd0\x8f\x86\xe1\xaba\xd8\x15\xe0\xaa|\x0a1\xd9\x96\x88\x18S\x83D\xf3\xf8\x96\xa5D\xd8\x89\xa4(\xf0\x5cW\xeeq\xc0U\x99\xa8\xd0\xa2h*\x09\xb1\x03>JR\xed\x11Q)w\x5cWR\x1b)\x01\xbe\x9dr\x8b\xdc>\x15!\xd8v\x90\x12\xe1\x7f\xd7q$u\xc3\x06\x8e\xe7\xba\x17<\xfd\xbe>u\x9d?\x11v \xd1Z\xccn\xf8\x95(g\xe1\x1d\xd3T\xd47\xbf\x81+\xc7\x80\x01\xadIRR\x0e$\xab\x8e\x1b\x93#H\x87D?c\xad\xf6\xcb\xe8\x1b\xbc\xaf\xaa\xea\x16\x7fL\xb9\xb7\x7fp\xf8Z\xa1Y)C\x0d\xa2\xf3\x158_HZ\xc6z\xc7\xdd\x0f\x9b\xb5\xcf\xbb\x9b/)\xb5\x84\x00\xa1\xb8\xb1\x8f\xd5\xd9g_\xf0?\xb1\x93`\x959\x84\xf0\x9f\x00\x03\x00\xb7\x90-\xe2\x97\x1a,\xf4\x00\x00\x00\x00IEND\xaeB`\x82"
+qt_resource_name = b"\x00\x07\x03\x80\x15Y\x00m\x00a\x00i\x00n\x00.\x00u\x00i\x00\x09\x00(\xbf#\x00s\x00t\x00y\x00l\x00e\x00.\x00c\x00s\x00s\x00\x03\x00\x00p7\x00i\x00m\x00g\x00\x0a\x0a\x94\x0bG\x00s\x00w\x00i\x00t\x00c\x00h\x00.\x00p\x00n\x00g\x00\x08\x06|Z\x07\x00c\x00o\x00p\x00y\x00.\x00p\x00n\x00g\x00\x09\x0b\x85\x83\x07\x00c\x00l\x00e\x00a\x00r\x00.\x00p\x00n\x00g\x00\x09\x0a\xa8\xbaG\x00p\x00a\x00s\x00t\x00e\x00.\x00p\x00n\x00g"
+qt_resource_struct = b"\x00\x00\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00,\x00\x02\x00\x00\x00\x04\x00\x00\x00\x04\x00\x00\x00\x14\x00\x00\x00\x00\x00\x01\x00\x00\x15\x8e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00R\x00\x00\x00\x00\x00\x01\x00\x00\x18\x5c\x00\x00\x008\x00\x00\x00\x00\x00\x01\x00\x00\x15\xce\x00\x00\x00\x80\x00\x00\x00\x00\x00\x01\x00\x00\x1d\x0e\x00\x00\x00h\x00\x00\x00\x00\x00\x01\x00\x00\x1a\xbe"
+def qInitResources():
+    QtCore.qRegisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data)
+
+def qCleanupResources():
+    QtCore.qUnregisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data)
+
+qInitResources()
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+from PySide import QtCore, QtNetwork
+from PySide.QtCore import QUrl
+import json
+from collections import deque, defaultdict
+from functools import partial
+import time
+import random
+from utils import parse_eval
+
+import logging
+log = logging.getLogger('copytrans')
+
+void = lambda *_: _
+
+USER_AGENT = 'Mozilla/5.0 (Windows NT 6.0; rv:11.0) Gecko/20100101 Firefox/11.0'
+MAX_DELAY = 3000
+REQUEST_DELAY = defaultdict(lambda: 500)
+REQUEST_DELAY[('translate.google.fr', 80)] = 200
+REQUEST_DELAY[('csi.gstatic.com', 80)] = 300
+
+def build_csi(d):
+    d.update({'v':'3', 's': 'translate', 'tran': '16', 'e': '17259,gbar2'})
+    url = QUrl('/csi')
+    url.setQueryItems(list(d.items()))
+    return {'host': 'csi.gstatic.com', 'port': 80, 'important': False, 'priority': False, 'path': url.toString(), 'handler': void}
+
+class TranslateService(QtCore.QObject):
+    def __init__(self, parent=None):
+        super(TranslateService, self).__init__(parent)
+        self.manager = QtNetwork.QNetworkAccessManager()
+        self.manager.finished.connect(self._process_reply)
+        self.manager.authenticationRequired.connect(self._site_authenticate)
+        self.manager.proxyAuthenticationRequired.connect(self._proxy_authenticate)
+        self._last_request_times = {}
+        self._active_requests = {}
+        self._hosts = []
+        self._high_priority_queues = {}
+        self._low_priority_queues = {}
+        self._timer = QtCore.QTimer(self)
+        self._timer.setSingleShot(True)
+        self._timer.timeout.connect(self._run_next_task)
+        self._request_methods = {
+            "GET": self.manager.get,
+            "POST": self.manager.post,
+            "PUT": self.manager.put,
+            "DELETE": self.manager.deleteResource
+        }
+    def _start_request(self, method, host, port, path, data, handler, is_json, http_auth=None, morehandlers=None):
+        log.debug("%s http://%s:%d%s", method, host, port, path)
+        url = QUrl.fromEncoded("http://%s:%d%s" % (host, port, path))
+        if http_auth is not None:
+            url.setUserName(http_auth[0])
+            url.setPassword(http_auth[1])
+        request = QtNetwork.QNetworkRequest(url)
+        request.setRawHeader("User-Agent", USER_AGENT)
+        request.setAttribute(QtNetwork.QNetworkRequest.CacheLoadControlAttribute, QtNetwork.QNetworkRequest.PreferCache)
+        if data is not None:
+            request.setHeader(QtNetwork.QNetworkRequest.ContentTypeHeader, "application/json" if is_json else "application/x-www-form-urlencoded")
+        send = self._request_methods[method]
+        reply = send(request, data) if data is not None else send(request)
+        key = (host, port)
+        self._last_request_times[key] = time.time()
+        self._active_requests[reply] = (request, handler, is_json)
+        if morehandlers is not None:
+            for handName, handHand in morehandlers:
+                getattr(reply, handName).connect(partial(handHand, reply))
+        return True
+
+
+    @staticmethod
+    def urls_equivalent(leftUrl, rightUrl):
+        return leftUrl.port(80) == rightUrl.port(80) and \
+            leftUrl.toString(QUrl.FormattingOption(QUrl.RemovePort)) == rightUrl.toString(QUrl.FormattingOption(QUrl.RemovePort))
+
+    def _process_reply(self, reply):
+        try:
+            request, handler, is_json = self._active_requests.pop(reply)
+        except KeyError:
+            logging.error("Error: Request not found for %s" % str(reply.request().url().toString()))
+            return
+        error = int(reply.error())
+        redirect = reply.attribute(QtNetwork.QNetworkRequest.RedirectionTargetAttribute)
+        logging.debug("Received reply for %s: HTTP %d (%s)",
+                       reply.request().url().toString(),
+                       reply.attribute(QtNetwork.QNetworkRequest.HttpStatusCodeAttribute),
+                       reply.attribute(QtNetwork.QNetworkRequest.HttpReasonPhraseAttribute))
+        if handler is not None:
+            if error:
+                log.error("Network request error for %s: %s (QT code %d, HTTP code %d)",
+                              reply.request().url().toString(),
+                              reply.errorString(),
+                              error,
+                              reply.attribute(QtNetwork.QNetworkRequest.HttpStatusCodeAttribute))
+
+            # Redirect if found and not infinite
+            if redirect is not None and not TranslateService.urls_equivalent(redirect, reply.request().url()):
+                log.debug("Redirect to %s requested", redirect.toString())
+                self.get(str(redirect.host()),
+                         redirect.port(80),
+                         # retain path, query string and anchors from redirect URL
+                         redirect.toString(QUrl.FormattingOption(QUrl.RemoveAuthority | QUrl.RemoveScheme)),
+                         handler, is_json)
+            elif is_json:
+                data = str(reply.readAll())
+                try:
+                    handler(json.loads(data), reply, error)
+                except ValueError:
+                    handler(parse_eval(data), reply, error)
+            else:
+                handler(reply.readAll(), reply, error)
+        reply.close()
+
+    def get(self, host, port, path, handler, is_json=False, priority=False, important=False, http_auth=None, morehandlers=None):
+        func = partial(self._start_request, "GET", host, port, path, None, handler, is_json, http_auth, morehandlers)
+        return self.add_task(func, host, port, priority, important=important)
+
+    def post(self, host, port, path, data, handler, is_json=False, priority=True, important=True, http_auth=None, morehandlers=None):
+        logging.debug("POST-DATA %r", data)
+        func = partial(self._start_request, "POST", host, port, path, data, handler, is_json, http_auth, morehandlers)
+        return self.add_task(func, host, port, priority, important=important)
+
+    def put(self, host, port, path, data, handler, priority=True, important=True, http_auth=None, morehandlers=None):
+        func = partial(self._start_request, "PUT", host, port, path, data, handler, False, http_auth, morehandlers)
+        return self.add_task(func, host, port, priority, important=important)
+
+    def delete(self, host, port, path, handler, priority=True, important=True, http_auth=None, morehandlers=None):
+        func = partial(self._start_request, "DELETE", host, port, path, None, handler, False, http_auth, morehandlers)
+        return self.add_task(func, host, port, priority, important=important)
+    def _site_authenticate(self, reply, authenticator):
+        self.emit(QtCore.SIGNAL('authentication_required'), reply, authenticator)
+
+    def _proxy_authenticate(self, proxy, authenticator):
+        self.emit(QtCore.SIGNAL('proxyAuthentication_required'), proxy, authenticator)
+
+    def stop(self):
+        self._high_priority_queues = {}
+        self._low_priority_queues = {}
+        for reply in self._active_requests.keys():
+            reply.abort()
+
+    def _run_next_task(self):
+        delay = 999999
+        for key in self._hosts:
+            queue = self._high_priority_queues.get(key) or self._low_priority_queues.get(key)
+            if not queue:
+                continue
+            now = time.time()
+            last = self._last_request_times.get(key)
+            request_delay = REQUEST_DELAY[key]
+            last_ms = (now - last) * 1000 if last is not None else request_delay
+            if last_ms >= request_delay:
+                log.debug("Last request to %s was %d ms ago, starting another one", key, last_ms)
+                d = request_delay
+                queue.popleft()()
+            else:
+                d = request_delay - last_ms
+                log.debug("Waiting %d ms before starting another request to %s", d, key)
+            if d < delay:
+                delay = d
+        if delay < MAX_DELAY:
+            self._timer.start(delay)
+
+    def add_task(self, func, host, port, priority, important=False):
+        key = (host, port)
+        if key not in self._hosts:
+            self._hosts.append(key)
+        if priority:
+            queues = self._high_priority_queues
+        else:
+            queues = self._low_priority_queues
+        queues.setdefault(key, deque())
+        if important:
+            queues[key].appendleft(func)
+        else:
+            queues[key].append(func)
+        if not self._timer.isActive():
+            self._timer.start(0)
+        return key, func, priority
+
+    def remove_task(self, task):
+        key, func, priority = task
+        if priority:
+            queue = self._high_priority_queues[key]
+        else:
+            queue = self._low_priority_queues[key]
+        try:
+            queue.remove(func)
+        except ValueError:
+            pass
+
+    # real methods
+    def get_translation(self, lng_from, lng_to, original, callback):
+        qs = QUrl('/translate_a/t')
+        qs.setQueryItems(list({
+            'client': 't',
+            'hl': 'fr',
+            'multires': '1',
+            'otf': '2' if random.random() < 0.91 else '1', # automatic translation (key down...)
+            'ssel': '6',
+            'tsel': '3',
+            'sc': '1',
+            'sl': lng_from,
+            'tl': lng_to,
+            'text': original,
+        }.items()))
+        self.csi_translation(lng_from, lng_to, len(original))
+        return self.get('translate.google.fr', 80, qs.toString(), is_json=True, priority=True, important=True, handler=callback)
+
+    def csi_load(self):
+        return self.get(**build_csi({
+            'rt': 'cl.65,rsw.114,rsl.114,rtl.114,jbl.150,jl.175,br.178,prt.186,ol.242', 'srt': str(random.randint(100, 200)), 'action': 't'}))
+
+    def csi_translation(self, lng_from, lng_to, length):
+        ri = random.randint
+        return self.get(**build_csi({
+            'rt': 'prt.%d,ol.%d' % (ri(10, 100), ri(10, 100)), 'it': 'st.%d' % ri(10, 210), 'action': 'at', 'sl': lng_from, 'tl': lng_to, 'size': str(length)}))
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+import json, re
+
+#from pyparsing import *
+#def setup_eval_parser():
+#    TRUE = Keyword("true").setParseAction(replaceWith(True))
+#    FALSE = Keyword("false").setParseAction(replaceWith(False))
+#    NULL = Keyword("null").setParseAction(replaceWith(None))
+#
+#    jsonString = dblQuotedString.setParseAction(removeQuotes)
+#    jsonNumber = Combine(
+#        Optional('-') + ('0' | Word('123456789', nums)) +
+#        Optional('.' + Word(nums)) +
+#        Optional(Word('eE', exact=1) + Word(nums + '+-', nums)))
+#
+#    jsonObject = Forward()
+#    jsonValue = Forward()
+#    # black magic begins
+#    commaToNull = Word(',,', exact=1).setParseAction(replaceWith(None))
+#    jsonElements = ZeroOrMore(commaToNull) + Optional(jsonValue) + ZeroOrMore((Suppress(',') + jsonValue) | commaToNull)
+#    # black magic ends
+#    jsonArray = Group(Suppress('[') + Optional(jsonElements) + Suppress(']'))
+#    jsonValue << (jsonString | jsonNumber | Group(jsonObject) | jsonArray | TRUE | FALSE | NULL)
+#    memberDef = Group(jsonString + Suppress(':') + jsonValue)
+#    jsonMembers = delimitedList(memberDef)
+#    jsonObject << Dict(Suppress('{') + Optional(jsonMembers) + Suppress('}'))
+#
+#    jsonComment = cppStyleComment
+#    jsonObject.ignore(jsonComment)
+#
+#    def convertNumbers(s, l, toks):
+#        n = toks[0]
+#        try:
+#            return int(n)
+#        except ValueError:
+#            return float(n)
+#
+#    jsonNumber.setParseAction(convertNumbers)
+#
+#    return jsonValue
+
+def parse_eval(s):
+    return json.loads(re.sub(r'(?<=,)\s*,', ' null,', s))
+
+def test():
+    tests = (
+        '{"foo":["nar",1,2,,3]}',
+        '[1,2]',
+        '[1,,2]',
+    )
+    for test in tests:
+        res = parse_eval(test)
+        print(res)
+
+if __name__ == '__main__':
+    test()
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+from PySide.QtCore import *
+from PySide.QtGui import *
+import math
+
+CORNER_ROUNDNESS = 6
+FADING_DURATION = 300
+SPINNER_FADING_DURATION = 200
+FONT_SIZE = 11
+OPACITY = 0.7
+
+class LoadingSpinner(QWidget):
+    def __init__(self, spinner, bg_padding=8, bg_opacity=.1, parent=None):
+        super().__init__(parent)
+        self.m_bg_padding = bg_padding
+        self.m_bg_opacity = bg_opacity
+        self.m_spinner = spinner
+        self.resize(self.m_spinner.size()+bg_padding, self.m_spinner.size()+bg_padding)
+
+        self.m_showHide = QTimeLine()
+        self.m_showHide.setDuration(SPINNER_FADING_DURATION)
+        self.m_showHide.setStartFrame(0)
+        self.m_showHide.setEndFrame(300)
+        self.m_showHide.setUpdateInterval(20)
+
+        self.m_spinner.frameChanged.connect(self.update)
+
+        self.m_showHide.frameChanged.connect(self.update)
+        self.m_showHide.finished.connect(self.hideFinished)
+
+        self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
+        self.hide()
+
+    def fadeIn(self):
+        if self.isVisible():
+            return
+
+        self.show()
+
+        self.m_spinner.start()
+        self.m_showHide.setDirection(QTimeLine.Forward)
+
+        if self.m_showHide.state() != QTimeLine.Running:
+            self.m_showHide.start()
+
+    def fadeOut(self):
+        self.m_showHide.setDirection(QTimeLine.Backward)
+
+        if self.m_showHide.state() != QTimeLine.Running:
+            self.m_showHide.start()
+
+    def hideFinished(self):
+        if self.m_showHide.direction() == QTimeLine.Backward:
+            self.hide()
+            self.m_spinner.stop()
+
+    def paintEvent(self, ev):
+        if not self.parentWidget():
+            return
+
+        center = QPoint((self.parentWidget().width() / 2) - (self.width() / 2), (self.parentWidget().height() / 2) - (self.height() / 2))
+        if center != self.pos():
+            self.move(center)
+            return
+
+        p = QPainter(self)
+        r = QRect(self.contentsRect())
+
+        if self.m_showHide.state() == QTimeLine.Running:
+            p.setOpacity(self.m_showHide.currentValue())
+
+        r_,g_,b_,a_ = QColor(self.m_spinner.m_color).getRgbF()
+        p.setRenderHint(QPainter.Antialiasing)
+        p.setPen(Qt.NoPen)
+        p.setBrush(QColor.fromRgbF(1-r_, 1-g_, 1-b_, self.m_bg_opacity))
+        p.drawRoundedRect(r, 6, 6)
+        pixmap = self.m_spinner.getFrame()
+        p.drawPixmap(QRect(self.m_bg_padding/2, self.m_bg_padding/2, self.m_spinner.size(), self.m_spinner.size()), pixmap)
+
+class SpinnerPainter(QObject):
+    frameChanged = Signal()
+
+    def __init__(self, size=64, color=Qt.black, innerRadius=18, duration=1000, speed=300, tailLength=1, clockWise=True):
+        #super().__init__(self) # wtf? not working
+        QObject.__init__(self)
+
+        self.m_size = size
+        self.m_innerRadius = innerRadius
+        self.m_speed = speed
+        self.m_duration = duration
+        self.m_tailLength = tailLength
+        self.m_color = color
+        self.m_clockWise = clockWise
+
+        self.m_anim = QTimeLine()
+        self.m_anim.setDuration(self.m_duration)
+        self.m_anim.setLoopCount(0)
+
+        self.m_anim.frameChanged.connect(lambda: self.frameChanged.emit())
+
+        self.m_key = self.hashSoup(self.__class__.__name__, size, innerRadius, color, duration, speed, tailLength, clockWise)
+
+    def hashSoup(self, *params):
+        return '/'.join(map(str, params))
+
+    def rad2deg(self, rad):
+        return rad * 180 / math.pi
+
+    def start(self):
+        self.m_anim.start()
+
+    def stop(self):
+        self.m_anim.stop()
+
+    def running(self):
+        return self.m_anim.state() == QTimeLine.Running
+
+    def size(self):
+        return self.m_size
+
+    def paintFrame(self, pixmap, framenum):
+        raise NotImplementedError
+
+    def getFrame(self):
+        pixmap = QPixmap()
+        if self.m_clockWise:
+            framenum = self.m_anim.currentFrame()
+        else:
+            framenum = self.m_anim.endFrame() - self.m_anim.currentFrame()
+
+        key = self.hashSoup(self.m_key, framenum)
+        if not QPixmapCache.find(key, pixmap):
+            pixmap = QPixmap(self.size(), self.size())
+            pixmap.fill(Qt.transparent)
+            self.paintFrame(pixmap, framenum)
+            QPixmapCache.insert(key, pixmap)
+
+        return pixmap
+
+class TicksSpinner(SpinnerPainter):
+    def __init__(self, ticks=16, tickWidth=2, **kwargs):
+        super().__init__(**kwargs)
+
+        self.m_outerRadius = self.size() / 2
+        self.m_tickWidth = tickWidth
+        self.m_ticks = ticks
+
+        self.m_key = self.hashSoup(self.m_key, ticks, tickWidth)
+
+        self._anglediff = 2 * math.pi / self.m_ticks
+        trans = QColor(self.m_color)
+        trans.setAlphaF(0)
+        self._grad = QConicalGradient()
+        self._grad.setStops((
+            (0, self.m_color),
+            (self.m_tailLength, trans),
+        ))
+        self._pen = QPen(self._grad, self.m_tickWidth, Qt.SolidLine, Qt.RoundCap)
+        self.m_anim.setCurveShape(QTimeLine.LinearCurve)
+        self.m_anim.setStartFrame(0)
+        self.m_anim.setEndFrame(self.m_ticks)
+        self.m_anim.setUpdateInterval(self.m_speed / self.m_ticks)
+
+    def paintFrame(self, pixmap, frame):
+        pp = QPainter(pixmap)
+        pp.setRenderHint(QPainter.Antialiasing)
+        pp.translate(self.m_outerRadius, self.m_outerRadius)
+
+        a = ((self.m_ticks - frame) % self.m_ticks) * self._anglediff
+        self._grad.setAngle(self.rad2deg(a + self._anglediff / 2)) # offset prevents discontinuity to be shown
+        self._pen.setBrush(self._grad)
+        pp.setPen(self._pen)
+
+        for n in range(self.m_ticks):
+            otr = self.m_outerRadius - self.m_tickWidth # prevents painting out the pixmap
+            innerPoint = QPointF(
+                self.m_innerRadius * math.cos(n * self._anglediff),
+                self.m_innerRadius * math.sin(n * self._anglediff)
+            )
+            outerPoint = QPointF(
+                otr * math.cos(n * self._anglediff),
+                otr * math.sin(n * self._anglediff)
+            )
+            pp.drawLine(innerPoint, outerPoint) # draw the nth tick of this frame
+
+        pp.end()
+
+class RingSpinner(SpinnerPainter):
+    def __init__(self, step=20, **kwargs):
+        super().__init__(**kwargs)
+
+        self.m_step = step
+        self.m_outerRadius = self.size() / 2
+
+        self.m_key = self.hashSoup(self.m_key, self.m_step)
+
+        self._anglediff = 2 * math.pi / self.m_step
+        trans = QColor(self.m_color)
+        trans.setAlphaF(0)
+        self._grad = QConicalGradient()
+        self._grad.setStops((
+            (0, self.m_color),
+            (self.m_tailLength, trans),
+        ))
+        self._pen = QPen(self._grad, self.m_outerRadius - self.m_innerRadius, Qt.SolidLine, Qt.RoundCap)
+        self._r = self.m_innerRadius + (self.m_outerRadius - self.m_innerRadius) / 2
+        self.m_anim.setCurveShape(QTimeLine.LinearCurve)
+        self.m_anim.setStartFrame(0)
+        self.m_anim.setEndFrame(self.m_step)
+        self.m_anim.setUpdateInterval(self.m_speed / self.m_step)
+
+    def paintFrame(self, pixmap, frame):
+        pp = QPainter(pixmap)
+        pp.setRenderHint(QPainter.Antialiasing)
+        pp.translate(self.m_outerRadius, self.m_outerRadius)
+
+        a = ((self.m_step - frame) % self.m_step) * self._anglediff
+        self._grad.setAngle(self.rad2deg(a)) # offset prevents discontinuity to be shown
+
+        self._pen.setBrush(self._grad)
+        pp.setPen(self._pen)
+        pp.drawEllipse(QPoint(0, 0), self._r, self._r)
+
+        pp.end()